@reliability-design/web 1.4.8 → 1.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -188,11 +188,23 @@ RelAccordion.styles = import_lit.css`
188
188
  pointer-events: none;
189
189
  }
190
190
 
191
+ /* No CSS border property here, deliberately. Figma's Plain stroke is
192
+ strokeAlign=INSIDE, which OVERLAYS the content: verified against the
193
+ live file, the Head frame is 539 wide inside a 539 component in every
194
+ one of the 24 variants, Plain and Shadow alike. A CSS border insets
195
+ its children instead, which pushed the header in by 1px on all four
196
+ sides — so the hover wash stopped 1px short of the card edge and a
197
+ ring of untinted --rel-bg-neutral survived around it. That ring is
198
+ what read as "a border appears on hover", and it appeared on EVERY
199
+ variant because the border lived on this shared base; Shadow merely
200
+ set border-color: transparent, which still reserves the 1px.
201
+ An outline with a negative offset reproduces strokeAlign=INSIDE
202
+ exactly: no layout inset, painted on top, follows border-radius, and
203
+ not clipped by this element's own overflow: hidden. */
191
204
  .accordion {
192
205
  background: var(--rel-bg-neutral);
193
206
  box-sizing: border-box;
194
207
  border-radius: var(--_acc-radius, var(--rel-border-radius-xs));
195
- border: 1px solid var(--rel-border-subtle);
196
208
  overflow: hidden;
197
209
  box-shadow: var(--rel-shadows-sm);
198
210
  }
@@ -201,8 +213,9 @@ RelAccordion.styles = import_lit.css`
201
213
  --_acc-radius: var(--rel-border-radius-xs);
202
214
  }
203
215
 
216
+ /* Live Figma: Style=Shadow carries NO stroke at all (strokes: []). */
204
217
  :host([appearance="shadow"]) .accordion {
205
- border-color: transparent;
218
+ outline: none;
206
219
  box-shadow: var(--rel-shadows-sm);
207
220
  }
208
221
 
@@ -210,11 +223,32 @@ RelAccordion.styles = import_lit.css`
210
223
  --_acc-radius: 0px;
211
224
  }
212
225
 
226
+ /* Live Figma: Style=Plain is strokeWeight 1, strokeAlign INSIDE. */
213
227
  :host([appearance="plain"]) .accordion {
214
- border-color: var(--rel-border-subtle);
228
+ outline: 1px solid var(--rel-border-subtle);
229
+ outline-offset: -1px;
215
230
  box-shadow: none;
216
231
  }
217
232
 
233
+ /* box-shadow is NOT painted in forced-colors mode (Windows High Contrast)
234
+ but outline is — the same UA behaviour styles/focus-ring.ts documents,
235
+ and the reason its outer ring is an outline rather than a third shadow
236
+ layer. It matters more here than there: appearance="shadow" has no
237
+ stroke in Figma, so once the shadow is dropped the card has NO boundary
238
+ at all and the header, body and page all read as one surface.
239
+ This used to work by accident — while the base rule declared a 1px solid
240
+ transparent border, the UA forced that transparent border to a visible
241
+ colour. Removing the border to fix the hover inset took the accident
242
+ away with it, so the edge is restored deliberately.
243
+ CanvasText rather than a token: in forced-colors the user's palette is
244
+ the whole point. Plain already has an outline and simply re-asserts it. */
245
+ @media (forced-colors: active) {
246
+ .accordion {
247
+ outline: 1px solid CanvasText;
248
+ outline-offset: -1px;
249
+ }
250
+ }
251
+
218
252
  /* ── Header ───────────────────────────────────────────────── */
219
253
 
220
254
  .header {
@@ -635,8 +669,12 @@ RelAlert.styles = import_lit2.css`
635
669
  gap: var(--rel-spacing-lg);
636
670
  padding: var(--rel-spacing-md);
637
671
  border-radius: var(--rel-border-radius-sm);
638
- min-width: 360px;
639
672
  box-sizing: border-box;
673
+ /* No min-width. Figma's Alert is a fixed 400 with minWidth UNSET on all
674
+ twelve variants (verified on the live file), so a 360px floor was
675
+ code-invented. It stopped the alert fitting any column narrower than
676
+ 360px and is what forced the docs Variants grid to minmax(360px, 1fr).
677
+ A consumer who wants a floor can set one on the host. */
640
678
  }
641
679
 
642
680
  /* Figma "Frame 10": the leading icon and the content column, 12px apart.
@@ -649,10 +687,17 @@ RelAlert.styles = import_lit2.css`
649
687
  min-width: 0;
650
688
  }
651
689
 
690
+ /* Figma "Content": layoutGrow 1, layoutSizingHorizontal FILL — it takes the
691
+ width left over beside the leading icon. Without flex it hugged its text,
692
+ so a short message left the column narrow and a long one wrapped earlier
693
+ than it needed to. min-width: 0 is the companion every flex child needs
694
+ before it is allowed to shrink below its content. */
652
695
  .rel-content {
653
696
  display: flex;
654
697
  flex-direction: column;
655
698
  gap: var(--rel-spacing-3xs);
699
+ flex: 1;
700
+ min-width: 0;
656
701
  }
657
702
 
658
703
  .rel-message1 {
@@ -736,9 +781,16 @@ RelAlert.styles = import_lit2.css`
736
781
  /* COLORS — semantic status tokens carry both light and dark modes.
737
782
  Base classes are the SUBTLE style (Figma Style=Subtle): tone light fill,
738
783
  no border, tone-colored title. */
739
- .rel-alert {
740
- border: 1px solid transparent;
741
- }
784
+ /* No border here. Only Style=Outline has a stroke in Figma — Solid and
785
+ Subtle report strokes: NONE on the live file — and Outline's is
786
+ strokeAlign: INSIDE, which OVERLAYS the content rather than insetting it.
787
+ A CSS border does inset, so declaring a transparent one on the shared
788
+ base made every variant's content box 1px tighter than Figma and
789
+ reserved a pixel that only one appearance ever paints in.
790
+ The outline block below draws the real thing with outline-offset: -1px,
791
+ which reproduces inside-aligned stroke exactly: no layout inset, painted
792
+ on top, and it follows border-radius. Same fix as rel-accordion, which
793
+ had the identical bug. */
742
794
 
743
795
  .alert-neutral {
744
796
  background: var(--rel-bg-neutral);
@@ -835,26 +887,45 @@ RelAlert.styles = import_lit2.css`
835
887
  /* Outline style (Figma Style=Outline): no fill, tone border, tone title */
836
888
  :host([appearance="outline"]) .rel-alert {
837
889
  background: transparent;
890
+ outline: 1px solid transparent;
891
+ outline-offset: -1px;
838
892
  }
839
893
 
840
894
  :host([appearance="outline"]) .alert-neutral {
841
- border-color: var(--rel-border-default);
895
+ outline-color: var(--rel-border-default);
842
896
  }
843
897
 
844
898
  :host([appearance="outline"]) .alert-info {
845
- border-color: var(--rel-border-info);
899
+ outline-color: var(--rel-border-info);
846
900
  }
847
901
 
848
902
  :host([appearance="outline"]) .alert-success {
849
- border-color: var(--rel-border-success);
903
+ outline-color: var(--rel-border-success);
850
904
  }
851
905
 
852
906
  :host([appearance="outline"]) .alert-warning {
853
- border-color: var(--rel-border-warning);
907
+ outline-color: var(--rel-border-warning);
854
908
  }
855
909
 
856
910
  :host([appearance="outline"]) .alert-destructive {
857
- border-color: var(--rel-border-danger);
911
+ outline-color: var(--rel-border-danger);
912
+ }
913
+
914
+ /* Every appearance keeps a boundary in forced-colors mode (Windows High
915
+ Contrast), where the UA overrides fills and a Solid or Subtle alert would
916
+ otherwise be indistinguishable from the page. This used to work by
917
+ accident: while the shared base declared a 1px transparent border, the UA
918
+ forced it to a visible colour. Removing that border to fix the content
919
+ inset took the accident with it, so the edge is restored deliberately —
920
+ outline rather than border because outline is painted in that mode and
921
+ because it does not re-introduce the inset. CanvasText, not a token: in
922
+ forced-colors the user's palette is the point. Outline appearance simply
923
+ re-asserts what it already has. Same treatment as rel-accordion. */
924
+ @media (forced-colors: active) {
925
+ .rel-alert {
926
+ outline: 1px solid CanvasText;
927
+ outline-offset: -1px;
928
+ }
858
929
  }
859
930
 
860
931
  /* Close-icon colour on Subtle and Outline: the tone colour, matching the
@@ -1210,16 +1281,28 @@ var RelTextfield = class extends import_lit3.LitElement {
1210
1281
  </div>`;
1211
1282
  const belowTemplate = import_lit3.html`<div part="helper" class="${["textfield-support", isError ? "error" : "", belowText ? "" : "empty"].filter(Boolean).join(" ")}" id="${describedById}" aria-live="polite">${this.helperIcon && belowText ? import_lit3.html`<rel-icon part="helper-icon" name="${this.helperIcon}" aria-hidden="true"></rel-icon>` : ""}${belowText ?? ""}</div>`;
1212
1283
  if (this.variant === "legend") {
1213
- const fieldsetClass = ["field-fieldset", this.multiline ? "multiline" : ""].filter(Boolean).join(" ");
1284
+ const legendContainerClass = [
1285
+ "textfield-container",
1286
+ this.isDisabled ? "disabled" : "",
1287
+ this.multiline ? "multiline" : "",
1288
+ prefixClear ? "prefix-clear" : ""
1289
+ ].filter(Boolean).join(" ");
1214
1290
  return import_lit3.html`
1215
1291
  <div class="${wrapperClasses}">
1216
- <fieldset part="control" class="${fieldsetClass}" @mousedown="${this.handleControlMousedown}"><legend class="field-legend" id="${legendId}">${labelHidden ? "" : this.label ?? ""}${this.required && !labelHidden ? import_lit3.html`<span class="required-mark" aria-hidden="true">*</span>` : ""}</legend>
1217
- <div class="field-row">
1218
- ${prefixTemplate}
1219
- <div class="textfield-field">${inputTemplate}</div>
1220
- ${suffixTemplate}
1221
- </div>
1222
- </fieldset>
1292
+ <div part="control" class="${legendContainerClass}" @mousedown="${this.handleControlMousedown}">
1293
+ <!-- aria-hidden on the FIELDSET, and deliberately not countered on
1294
+ the legend: aria-hidden="false" cannot un-hide a descendant of
1295
+ a hidden element, so writing it would only mislead. Hiding is
1296
+ correct here — the fieldset now wraps nothing but the legend,
1297
+ so leaving it exposed would announce an empty group before the
1298
+ field. The name still works: accname step 2B includes hidden
1299
+ nodes when they are reached through aria-labelledby, which is
1300
+ how the input gets its name from this legend's id. -->
1301
+ <fieldset class="legend-outline" aria-hidden="true"><legend class="field-legend" id="${legendId}"><span class="legend-text">${labelHidden ? "" : this.label ?? ""}${this.required && !labelHidden ? import_lit3.html`<span class="required-mark" aria-hidden="true">*</span>` : ""}</span></legend></fieldset>
1302
+ ${prefixTemplate}
1303
+ <div class="textfield-field">${inputTemplate}</div>
1304
+ ${suffixTemplate}
1305
+ </div>
1223
1306
  ${belowTemplate}
1224
1307
  </div>
1225
1308
  `;
@@ -1332,7 +1415,23 @@ RelTextfield.styles = import_lit3.css`
1332
1415
 
1333
1416
  :host([floatinglabel]) .textfield-label {
1334
1417
  position: absolute;
1335
- left: var(--_rel-tf-pad-x, var(--rel-spacing-sm));
1418
+ /* ZERO, and the Figma numbers are why. Live spec, TextField Size=Medium
1419
+ Label Type=Floating: the "Inner Content" frame sits at dx=0 with
1420
+ padding-left 12, and the "Floating Label" frame sits at dx=12 with its
1421
+ own padding-left 4 — so the label BOX aligns with the content edge and
1422
+ its TEXT lands 4px in, that 4px being the notch padding rather than an
1423
+ indent.
1424
+ .textfield-field already begins at the control's border plus inline
1425
+ padding, i.e. at the content edge, so left: 0 puts the box exactly
1426
+ where Figma puts it. The old --_rel-tf-pad-x applied that inset a
1427
+ SECOND time and pushed the text to 29px from the control edge; briefly
1428
+ correcting it to -4px then over-shot the other way to 13px and left
1429
+ almost no border between the corner and the notch. Figma's own value is
1430
+ 16px from the control edge; we land on 17px, the extra 1px being our
1431
+ border, which Figma's inside-aligned stroke absorbs into its padding.
1432
+ With a prefix affix present .textfield-field starts after it, so the
1433
+ label follows the icon. */
1434
+ left: 0;
1336
1435
  top: 50%;
1337
1436
  transform: translateY(-50%);
1338
1437
  color: var(--rel-text-placeholder);
@@ -1363,19 +1462,59 @@ RelTextfield.styles = import_lit3.css`
1363
1462
  but a ring and a notch cannot coexist on one box — see the legend
1364
1463
  variant's own note below — and Recipe B requires the ring. The surface
1365
1464
  colour is therefore a documented seam: --rel-textfield-surface. */
1465
+ /* TWO OVERLAPPING strips, not one exact one. The reference: the label is
1466
+ top: 0 against .textfield-field, whose top edge is the container's
1467
+ CONTENT-box top, so in the label's own coordinates 50% is one
1468
+ border-width below the border-box top. Measured from there the ring
1469
+ occupies [50% - notch, 50% - border] and the border [50% - border, 50%].
1470
+ A single strip cut to exactly that band leaked at some zoom levels and
1471
+ not others (v1.4.9 "slash" report): the border and box-shadow are
1472
+ pixel-snapped by Blink, while a strip inside a translateY(-50%) label
1473
+ rasterises at a fractional offset, so at 105%, 115%, 135% zoom, and at
1474
+ Windows 125% scaling with NO zoom, their edges disagreed by under a
1475
+ device pixel and a hairline of ring or border showed through the text.
1476
+ No offset tuning can fix that; it is right at one scale factor only.
1477
+ So each strip overshoots into paint of ITS OWN colour, and every edge
1478
+ lands either on same-colour paint or inside the other strip's opaque
1479
+ interior:
1480
+ ::before surface colour, the whole band + 2px above + 2px below
1481
+ ::after the control's FILL, the border + 3px below, painted on top
1482
+ The strips must OVERLAP, not abut. Abutting them at the ring/border
1483
+ junction leaked a constant quarter of the ring colour: two anti-aliased
1484
+ edges on one device row cover a*(1-a) of what is underneath, 0.25 at
1485
+ a = 0.5. Measured as exactly 61/244 before the overlap, 0 after.
1486
+ The overshoot is 2px, not 1: the strip snaps to the grid in the label's
1487
+ own (fractionally offset) space and the ring in the container's, so
1488
+ the two can disagree by up to a full device pixel, and a 1px overshoot
1489
+ still left a 34/255 residue on the ring's outermost row at 105% zoom.
1490
+ Every term derives from --_rel-tf-notch-h / --_rel-tf-border-w, so a
1491
+ ring change carries through. The ::after fill is the container's own
1492
+ background, not the surface seam, so the overshoot into the interior
1493
+ is invisible. Gate: qa/paint-geometry.mjs SLASH pass, zoom x DSF. */
1366
1494
  :host([floatinglabel]) .textfield-wrapper.focused .textfield-label::before,
1367
1495
  :host([floatinglabel]) .textfield-wrapper.has-value .textfield-label::before {
1368
1496
  content: "";
1369
1497
  position: absolute;
1370
1498
  left: 0;
1371
1499
  right: 0;
1372
- top: 50%;
1373
- height: var(--_rel-tf-notch-h, 1px);
1374
- transform: translateY(-50%);
1500
+ top: calc(50% - var(--_rel-tf-notch-h, 1px) - 2px);
1501
+ height: calc(var(--_rel-tf-notch-h, 1px) + 4px);
1375
1502
  background: var(--rel-textfield-surface, var(--rel-bg-neutral));
1376
1503
  z-index: -1;
1377
1504
  }
1378
1505
 
1506
+ :host([floatinglabel]) .textfield-wrapper.focused .textfield-label::after,
1507
+ :host([floatinglabel]) .textfield-wrapper.has-value .textfield-label::after {
1508
+ content: "";
1509
+ position: absolute;
1510
+ left: 0;
1511
+ right: 0;
1512
+ top: calc(50% - var(--_rel-tf-border-w, 1px));
1513
+ height: calc(var(--_rel-tf-border-w, 1px) + 3px);
1514
+ background: var(--rel-bg-neutral);
1515
+ z-index: -1;
1516
+ }
1517
+
1379
1518
  /* Figma keeps the docked label on --rel-text-label and ramps it per size:
1380
1519
  body/md/regular (L), body/sm/regular (M), body/xs/regular (S). */
1381
1520
  :host([floatinglabel]) .textfield-wrapper.focused .textfield-label,
@@ -1449,6 +1588,9 @@ RelTextfield.styles = import_lit3.css`
1449
1588
  :host([floatinglabel]) .textfield-wrapper.disabled .textfield-label::before {
1450
1589
  background: var(--rel-textfield-surface, var(--rel-bg-neutral-elevated-2));
1451
1590
  }
1591
+ :host([floatinglabel]) .textfield-wrapper.disabled .textfield-label::after {
1592
+ background: var(--rel-bg-neutral-elevated-2);
1593
+ }
1452
1594
 
1453
1595
  /* Container */
1454
1596
  .textfield-container {
@@ -1861,11 +2003,17 @@ RelTextfield.styles = import_lit3.css`
1861
2003
  color: var(--rel-text-placeholder);
1862
2004
  }
1863
2005
 
1864
- :host([floatinglabel]) .textfield-input::placeholder {
2006
+ /* Hiding the placeholder only makes sense where a floating label is
2007
+ standing in its place. The legend variant has no floating label — its
2008
+ label lives in the notch at every state, and the legend branch renders
2009
+ no .textfield-label at all — so applying this there left an empty field
2010
+ at rest: no placeholder AND no label. Excluding legend restores the
2011
+ placeholder for the one variant that has nothing to replace it. */
2012
+ :host([floatinglabel]:not([variant="legend"])) .textfield-input::placeholder {
1865
2013
  opacity: 0;
1866
2014
  }
1867
2015
 
1868
- :host([floatinglabel]) .textfield-wrapper.focused .textfield-input::placeholder {
2016
+ :host([floatinglabel]:not([variant="legend"])) .textfield-wrapper.focused .textfield-input::placeholder {
1869
2017
  opacity: 1;
1870
2018
  }
1871
2019
 
@@ -1933,26 +2081,129 @@ RelTextfield.styles = import_lit3.css`
1933
2081
  /* ── Legend (notched-outline) variant ──
1934
2082
  Native <fieldset>/<legend>: the legend notches the top border and stays
1935
2083
  fixed in place across all states (no float/animation). */
1936
- :host([variant="legend"]) .field-fieldset {
1937
- position: relative;
2084
+ /* The container keeps a TRANSPARENT border rather than dropping it. The
2085
+ border is 1px of layout that the default variant also has, and the
2086
+ inline padding is measured from inside it — remove it and a legend
2087
+ field's text sits 1px left of its default-variant neighbour in the same
2088
+ form. The visible stroke is painted by .legend-outline on top, exactly
2089
+ over this transparent one, so the two never double up (including in
2090
+ forced-colors, where they coincide rather than stack). */
2091
+ :host([variant="legend"]) .textfield-container {
2092
+ border-color: transparent;
2093
+ background: transparent;
2094
+ /* The legend protrudes above the top edge; overflow: hidden from the
2095
+ base rule would slice its top half off. */
2096
+ overflow: visible;
2097
+ }
2098
+
2099
+ /* The decorative outline layer. inset: 0 makes its border box identical to
2100
+ the container's, so the painted outline IS --_rel-tf-h — 32 / 40 / 48,
2101
+ with no legend-half subtracted, which is the whole bug this replaced.
2102
+ pointer-events: none keeps clicks going to the control beneath, and the
2103
+ content is lifted above it rather than this being pushed to z-index: -1,
2104
+ because .textfield-container is position: relative with z-index: auto
2105
+ and therefore does NOT create a stacking context — a negative z-index
2106
+ here could slip behind an ancestor's background. */
2107
+ :host([variant="legend"]) .legend-outline {
2108
+ position: absolute;
2109
+ /* NOT inset: 0. An absolutely positioned box's insets resolve against
2110
+ its containing block's PADDING box, and the container keeps a 1px
2111
+ transparent border (see above), so inset: 0 put this layer's border
2112
+ box 1px inside the container's on every side — a painted outline 2px
2113
+ short of the ladder at every size, measured 30/38/46 against 32/40/48.
2114
+ Pulling out by the border width lands the two border boxes exactly on
2115
+ each other, which is what makes the painted height the ladder value. */
2116
+ inset: calc(-1 * var(--_rel-tf-border-w, 1px));
2117
+ /* The legend has a small height (see .field-legend), and Blink then
2118
+ paints the fieldset's top border (legend-height - border) / 2 lower
2119
+ than the layer's top edge. Pull the top edge up by that outset so the
2120
+ painted line stays exactly on the container's top edge and the
2121
+ painted height stays on the 32 / 40 / 48 ladder. Only the top moves;
2122
+ the bottom stays put. */
2123
+ --_rel-tf-legend-h: 3px;
2124
+ --_rel-tf-legend-outset: calc(
2125
+ (var(--_rel-tf-legend-h) - var(--_rel-tf-border-w, 1px)) / 2
2126
+ );
2127
+ top: calc(-1 * var(--_rel-tf-border-w, 1px) - var(--_rel-tf-legend-outset));
1938
2128
  margin: 0;
1939
2129
  min-inline-size: 0;
1940
- padding: 0;
2130
+ /* Aligns the legend TEXT with the input text. The legend carries its own
2131
+ --rel-spacing-2xs of side padding to cut the notch a little wider than
2132
+ the glyphs, so the layer insets by that much less than the control's
2133
+ own inline padding. Derived from --_rel-tf-pad-x rather than restated,
2134
+ which is what makes the small size line up: the old rule hard-coded
2135
+ --rel-spacing-sm and could not see the size ramp at all, because
2136
+ --_rel-tf-pad-x is declared on .textfield-container and the legend
2137
+ variant never rendered one. */
2138
+ padding-inline: calc(
2139
+ var(--_rel-tf-pad-x, var(--rel-spacing-sm)) - var(--rel-spacing-2xs)
2140
+ );
2141
+ padding-block: 0;
1941
2142
  border: 1px solid var(--rel-border-default);
1942
2143
  border-radius: var(--rel-border-radius-xs);
1943
2144
  background: var(--rel-bg-neutral);
2145
+ pointer-events: none;
1944
2146
  transition: border-color 0.2s;
1945
2147
  }
1946
2148
 
2149
+ /* Lift the real content above the decorative layer. */
2150
+ :host([variant="legend"]) .textfield-field,
2151
+ :host([variant="legend"]) .textfield-affix {
2152
+ position: relative;
2153
+ z-index: 1;
2154
+ }
2155
+
2156
+ /* The legend is taken OUT OF FLOW — height: 0 with visible overflow.
2157
+ Blink draws a fieldset's top border through the legend's VERTICAL
2158
+ MIDDLE, so an in-flow legend pushes the painted line down by half its
2159
+ own line-height: measured 23 / 30 / 37 against the 32 / 40 / 48 ladder,
2160
+ which is exactly half of the 18 / 20 / 22 legend line-heights. Collapsing
2161
+ the legend's height puts its middle on the layer's top edge, so the
2162
+ border is drawn there and the painted outline is the full ladder value.
2163
+ The notch still cuts, because the gap spans the legend's WIDTH and that
2164
+ is unaffected. The text is then lifted back onto the line by
2165
+ .legend-text, which needs its own element: translateY(-50%) on a
2166
+ zero-height box resolves to zero, so the transform has to live on a
2167
+ child that still has height. */
2168
+ /* NOT height: 0 any more, and the reason is Blink's cutout rule. The gap
2169
+ it cuts in the top border for the legend is max(legend height, border
2170
+ width) tall, measured from the layer's top edge. A zero-height legend
2171
+ therefore gets a cutout EXACTLY the border's height — and at any
2172
+ fractional scale factor (Windows 125%: DSF 1.25) the pixel-snapped
2173
+ border lands partly outside that rect, so it painted straight through
2174
+ the legend text at half strength (measured 122/244 on every legend cell
2175
+ at DSF 1.25, clean at DSF 1). A 3px legend makes the cutout 3px tall
2176
+ with a pixel of margin either side of the border. The price is Blink's
2177
+ other rule — the border is drawn through the legend's vertical middle,
2178
+ (3 - 1) / 2 = 1px down — which .legend-outline cancels by starting 1px
2179
+ higher, so the painted outline is still the ladder value. The text is
2180
+ lifted onto that line by .legend-text below. */
1947
2181
  :host([variant="legend"]) .field-legend {
1948
- margin-left: var(--rel-spacing-sm);
2182
+ display: block;
2183
+ height: var(--_rel-tf-legend-h, 3px);
2184
+ overflow: visible;
2185
+ margin: 0;
1949
2186
  padding: 0 var(--rel-spacing-2xs);
1950
2187
  color: var(--rel-text-label);
1951
- font-family: var(--rel-body-sm-medium-font-family);
1952
- font-weight: var(--rel-body-sm-medium-font-weight);
1953
- font-size: var(--rel-body-sm-medium-font-size);
1954
- line-height: var(--rel-body-sm-medium-line-height);
1955
- letter-spacing: var(--rel-body-sm-medium-letter-spacing);
2188
+ /* REGULAR, not medium. Figma's floated label is 400 at every size — the
2189
+ same finding already recorded at rel-select.ts. This rule used to be
2190
+ the only one in the ramp at 500, so the DEFAULT size rendered heavier
2191
+ than both small and large, which set their own regular weights below. */
2192
+ font-family: var(--rel-body-sm-regular-font-family);
2193
+ font-weight: var(--rel-body-sm-regular-font-weight);
2194
+ font-size: var(--rel-body-sm-regular-font-size);
2195
+ line-height: var(--rel-body-sm-regular-line-height);
2196
+ letter-spacing: var(--rel-body-sm-regular-letter-spacing);
2197
+ }
2198
+
2199
+ /* Carries the legend's text back onto the border line the zero-height
2200
+ legend now sits on. inline-block so it has a height of its own for the
2201
+ percentage to resolve against. */
2202
+ :host([variant="legend"]) .legend-text {
2203
+ display: inline-block;
2204
+ /* Centre the text on the painted line, which sits at the legend's
2205
+ vertical middle (half the legend's height below its top). */
2206
+ transform: translateY(calc(-50% + var(--_rel-tf-legend-h, 3px) / 2));
1956
2207
  }
1957
2208
 
1958
2209
  :host([variant="legend"]) .textfield-input {
@@ -1960,88 +2211,78 @@ RelTextfield.styles = import_lit3.css`
1960
2211
  box-sizing: border-box;
1961
2212
  }
1962
2213
 
1963
- /* Legend on the same 32/40/48 scale. The fieldset's border box excludes
1964
- the legend's protruding top half, so sizing the fieldset itself keeps
1965
- the visible outline at exactly --_rel-tf-h; the row fills the remaining
1966
- content area and centres the input. Multiline keeps auto height. */
1967
- :host([variant="legend"]) .field-fieldset:not(.multiline) {
1968
- box-sizing: border-box;
1969
- height: var(--_rel-tf-h, 40px);
1970
- /* Flex column so the row fills the content box below the legend (a
1971
- percentage height on the row doesn't resolve inside a fieldset). The
1972
- <legend> is never a flex item, so the notch is unaffected. */
1973
- display: flex;
1974
- flex-direction: column;
1975
- }
1976
- :host([variant="legend"]) .field-fieldset:not(.multiline) .field-row {
1977
- flex: 1;
1978
- min-height: 0;
1979
- }
1980
- :host([variant="legend"]) .field-fieldset:not(.multiline) .textfield-input {
1981
- padding-top: 0;
1982
- padding-bottom: 0;
1983
- /* Fill the row so the whole outline is clickable (see the default
1984
- variant's note above). */
1985
- align-self: stretch;
1986
- }
1987
-
1988
2214
  /* labelStyle="none" + legend: nothing to seat in the notch, so drop the
1989
2215
  legend entirely and let the top border run continuous. */
1990
2216
  :host([label-style="none"][variant="legend"]) .field-legend {
1991
2217
  display: none;
1992
2218
  }
1993
-
1994
- /* Legend: prefix | field | suffix sit in a flex row under the legend. The
1995
- fieldset's content box starts below the legend's midline, so a fill
1996
- prefix can't reach the top border here — square it off instead. */
1997
- :host([variant="legend"]) .field-row {
1998
- display: flex;
1999
- align-items: center;
2000
- }
2001
- :host([variant="legend"]) .textfield-affix.prefix.fill {
2002
- border-radius: 0;
2219
+ /* No legend rendered means no outset either, so the layer goes back to
2220
+ sitting exactly on the container's border box. */
2221
+ :host([label-style="none"][variant="legend"]) .legend-outline {
2222
+ top: calc(-1 * var(--_rel-tf-border-w, 1px));
2003
2223
  }
2004
2224
 
2005
- /* Hover */
2006
- :host([variant="legend"]) .field-fieldset:hover {
2007
- border-color: var(--rel-border-accent);
2225
+ /* A fill prefix used to be squared off because the fieldset's content box
2226
+ started below the legend's midline, so it could not reach the top
2227
+ border. The content now rides the ordinary container row, whose top IS
2228
+ the border, so the fill prefix can keep the control's own radius. */
2229
+
2230
+ /* Every state below paints on the OUTLINE LAYER, not the container: the
2231
+ container's own border is transparent by design. The container must also
2232
+ be told to drop the shared focus ring — see the note on focus. */
2233
+ :host([variant="legend"]) .textfield-wrapper:not(.focused):not(.error):not(.disabled)
2234
+ .textfield-container:hover .legend-outline {
2008
2235
  border-color: var(--rel-text-placeholder);
2009
2236
  }
2010
2237
 
2011
- /* Focus — border-color only. A box-shadow/outline ring hugs the full
2012
- rectangular border-box and would draw across the notch, appearing to
2013
- strike through the legend. The legend also recolors on focus (below). */
2014
- :host([variant="legend"]) .textfield-wrapper.focused .field-fieldset,
2015
- :host([variant="legend"]) .field-fieldset:focus-within {
2016
- border-color: var(--rel-border-strong);
2238
+ /* Focus — border-color only, no ring. A box-shadow or outline ring hugs
2239
+ the full rectangular border box and would draw straight across the
2240
+ notch, reading as a line struck through the legend. That is the same
2241
+ collision the floating label has on the other variants, except here
2242
+ there is no mask strip to hide it, so the ring is dropped outright and
2243
+ the legend recolours instead. */
2244
+ :host([variant="legend"]) .textfield-wrapper.focused .textfield-container,
2245
+ :host([variant="legend"]) .textfield-container:focus-within {
2017
2246
  box-shadow: none;
2247
+ border-color: transparent;
2248
+ }
2249
+ :host([variant="legend"]) .textfield-wrapper.focused .legend-outline,
2250
+ :host([variant="legend"]) .textfield-container:focus-within .legend-outline {
2251
+ border-color: var(--rel-border-strong);
2018
2252
  }
2019
2253
  :host([variant="legend"]) .textfield-wrapper.focused .field-legend,
2020
- :host([variant="legend"]) .field-fieldset:focus-within .field-legend {
2254
+ :host([variant="legend"]) .textfield-container:focus-within .field-legend {
2021
2255
  color: var(--rel-border-strong);
2022
2256
  }
2023
2257
 
2024
2258
  /* Error */
2025
- :host([variant="legend"]) .textfield-wrapper.error .field-fieldset {
2026
- border-color: var(--rel-border-danger);
2259
+ :host([variant="legend"]) .textfield-wrapper.error .textfield-container {
2027
2260
  box-shadow: none;
2261
+ border-color: transparent;
2262
+ }
2263
+ :host([variant="legend"]) .textfield-wrapper.error .legend-outline {
2264
+ border-color: var(--rel-border-danger);
2028
2265
  }
2029
2266
  :host([variant="legend"]) .textfield-wrapper.error .field-legend {
2030
2267
  color: var(--rel-text-danger);
2031
2268
  }
2032
2269
 
2033
- /* Disabled */
2034
- :host([variant="legend"]) .textfield-wrapper.disabled .field-fieldset {
2270
+ /* Disabled — the fill moves to the layer with the stroke, since the
2271
+ container is transparent and would otherwise paint the dulled surface
2272
+ underneath the layer's own background. */
2273
+ :host([variant="legend"]) .textfield-wrapper.disabled .textfield-container {
2274
+ background: transparent;
2275
+ }
2276
+ :host([variant="legend"]) .textfield-wrapper.disabled .legend-outline {
2035
2277
  background: var(--rel-bg-neutral-elevated-2);
2036
2278
  border-color: var(--rel-border-accent);
2037
- cursor: not-allowed;
2038
2279
  }
2039
2280
  :host([variant="legend"]) .textfield-wrapper.disabled .field-legend {
2040
2281
  color: var(--rel-text-disabled);
2041
2282
  }
2042
2283
 
2043
2284
  /* Readonly */
2044
- :host([variant="legend"]) .textfield-wrapper.readonly .field-fieldset {
2285
+ :host([variant="legend"]) .textfield-wrapper.readonly .textfield-container {
2045
2286
  cursor: default;
2046
2287
  }
2047
2288
 
@@ -2260,20 +2501,28 @@ var RelCheckbox = class extends import_lit4.LitElement {
2260
2501
  .checked=${this.status === "checked"}
2261
2502
  .indeterminate=${this.status === "indeterminate"}
2262
2503
  aria-checked="${this.status === "indeterminate" ? "mixed" : import_lit4.nothing}"
2504
+ aria-label="${this.fieldAriaLabel ?? import_lit4.nothing}"
2263
2505
  @click=${this.toggle}
2264
2506
  @blur=${() => this._onTouched()}
2265
2507
  ?disabled=${this.state === "disabled"}
2266
2508
  />
2267
2509
 
2510
+ <!-- Both marks are Figma's own paths, read from the live Published DS
2511
+ file (set 8418:1987) on 2026-09-25 and left in their authored
2512
+ 16-unit box. Figma knocks these out of the filled square; we paint
2513
+ them in the inverse tone over it instead, which is the same
2514
+ picture and keeps the existing per-state fill rules working.
2515
+ Do not re-scale the coordinates — the svg is sized to the drawn
2516
+ square so one unit here is one Figma unit. -->
2268
2517
  <span class="box">
2269
- <svg class="check-icon" viewBox="0 0 18 14" fill="var(--rel-text-inverse)">
2518
+ <svg class="check-icon" viewBox="0 0 16 16" fill="var(--rel-text-inverse)">
2270
2519
  <path
2271
- d="M6.44 14L0.5 7.36l1.49-1.66L6.44 10.68 16.01 0 17.5 1.66z"
2520
+ d="M6.22222 12.4444L1.77778 8L3.03111 6.74667L6.22222 9.92889L12.9689 3.18222L14.2222 4.44444L6.22222 12.4444Z"
2272
2521
  />
2273
2522
  </svg>
2274
2523
 
2275
- <svg class="indeterminate-icon" viewBox="0 0 16 2" fill="var(--rel-text-inverse)">
2276
- <rect width="16" height="2" />
2524
+ <svg class="indeterminate-icon" viewBox="0 0 16 16" fill="var(--rel-text-inverse)">
2525
+ <rect x="3.55556" y="7.11111" width="8.88889" height="1.77778" />
2277
2526
  </svg>
2278
2527
  </span>
2279
2528
 
@@ -2374,11 +2623,25 @@ RelCheckbox.styles = import_lit4.css`
2374
2623
  The inset is expressed as a ratio rather than four fractional pixel
2375
2624
  values, so it stays exact at any device pixel ratio and follows the
2376
2625
  frame if the ramp ever changes.
2377
- NOTE: the 2px border weight is UNVERIFIED against Figma. The control is
2378
- flattened artwork (a filled VECTOR, not a frame with a stroke — see
2379
- figma-bug E), so the spec gives the square's outer size but says nothing
2380
- about its stroke. Carried forward from the previous implementation pending the unflattening
2381
- request; one line to change if the designer publishes it. */
2626
+
2627
+ The control was UNFLATTENED in the 2026-09-23 Published DS revision, so
2628
+ the stroke and the marks are no longer guesses. Verified against the live
2629
+ file (set 8418:1987, node paths read 2026-09-25): the artwork box is 16
2630
+ units wide and EVERY value in it is 16/9 = 1.7778 units —
2631
+
2632
+ corner radius 1.7778 (outer rect: C0.8 0 0 0.8 0 1.77778)
2633
+ border thickness 1.7778 (outer 0…16 hollowed to 1.7778…14.2222)
2634
+ indeterminate dash height 1.7778 (y 7.11111 → 8.88889)
2635
+ indeterminate dash width 8.8889 (x 3.55556 → 12.4444, centred on 8)
2636
+
2637
+ Because the artwork box IS the drawn square (both are 0.8 × the frame),
2638
+ one sixteenth of the square is one unit, so a ninth of the square is the
2639
+ single number that governs radius, border and dash alike. That is why
2640
+ they are derived below rather than tabulated per size: four hand-written
2641
+ radii are what let large and extra-large silently share medium's 4px.
2642
+ Figma leaves the INNER corner square, which border-radius reproduces
2643
+ for free — an outer radius equal to the border width leaves no inner
2644
+ curve. */
2382
2645
  .box {
2383
2646
  width: var(--_rel-cb-size);
2384
2647
  height: var(--_rel-cb-size);
@@ -2389,6 +2652,12 @@ RelCheckbox.styles = import_lit4.css`
2389
2652
  flex-shrink: 0;
2390
2653
  }
2391
2654
 
2655
+ /* ONE box-shadow declaration, and every state contributes by setting
2656
+ --_rel-cb-border-color instead of restating the shadow. Restating it per
2657
+ state is the trap rel-stepper's connector hit and styles/focus-ring.ts
2658
+ documents at length: box-shadow is a single property, so two rules that
2659
+ both declare it do not compose — specificity picks a winner and the loser
2660
+ vanishes silently. */
2392
2661
  .box::before {
2393
2662
  content: "";
2394
2663
  position: absolute;
@@ -2396,11 +2665,30 @@ RelCheckbox.styles = import_lit4.css`
2396
2665
  inset: 10%;
2397
2666
  box-sizing: border-box;
2398
2667
  border-radius: var(--_rel-cb-radius);
2399
- border: 2px solid var(--rel-text-default);
2668
+ /* Resting wall colour; the state rules below override just this. */
2669
+ --_rel-cb-border-color: var(--rel-text-default);
2670
+ box-shadow: inset 0 0 0 var(--_rel-cb-border) var(--_rel-cb-border-color);
2400
2671
  background: transparent;
2401
2672
  transition: all 0.2s;
2402
2673
  }
2403
2674
 
2675
+ /* box-shadow is NOT painted in forced-colors mode (Windows High Contrast) —
2676
+ the same UA behaviour that styles/focus-ring.ts keeps its outer ring on an
2677
+ outline for. For a focus ring that costs a refinement; here it would erase
2678
+ the ENTIRE affordance, because the wall is all there is to see on an
2679
+ unchecked box. So forced-colors gets a real border back.
2680
+
2681
+ Safe to add rather than swap: the 10% inset pins all four edges, so the
2682
+ drawn square's outer edge is 80% of the frame whatever the wall is made
2683
+ of, and the shadow it sits beside is not painted here anyway. Bound to
2684
+ CanvasText because the forced palette, not the token, is what the user
2685
+ asked for. */
2686
+ @media (forced-colors: active) {
2687
+ .box::before {
2688
+ border: 1px solid CanvasText;
2689
+ }
2690
+ }
2691
+
2404
2692
  /* Figma puts the focus ring on the WHOLE component at 1.5px OUTSIDE, not
2405
2693
  around the control alone. Matches rel-radio, which already does this. */
2406
2694
  .wrapper:has(input:focus-visible) {
@@ -2412,9 +2700,16 @@ RelCheckbox.styles = import_lit4.css`
2412
2700
  }
2413
2701
 
2414
2702
  svg {
2415
- /* Proportional to the frame, so the mark tracks the drawn square. */
2416
- width: 55%;
2417
- height: 55%;
2703
+ /* Both marks are authored in Figma's 16-unit artwork box, and that box is
2704
+ the drawn square — 80% of the frame, the same as ::before's inset. So
2705
+ the svg covers the square exactly and every coordinate inside it is a
2706
+ Figma unit; position and size fall out of the path instead of being
2707
+ re-derived here. This was 55%/55% against an 8:1 viewBox, which (with
2708
+ preserveAspectRatio defaulting to meet) rendered the dash at 55%/8 =
2709
+ 6.875% of the frame — 1.6× thinner than Figma's 11.11%, and fractional
2710
+ at every size, which is what made it look blurred. */
2711
+ width: 80%;
2712
+ height: 80%;
2418
2713
  position: relative;
2419
2714
  display: none;
2420
2715
  }
@@ -2430,7 +2725,7 @@ RelCheckbox.styles = import_lit4.css`
2430
2725
  tokens for this same glyph. */
2431
2726
  .checked .box::before {
2432
2727
  background: var(--rel-text-default);
2433
- border-color: var(--rel-text-default);
2728
+ --_rel-cb-border-color: var(--rel-text-default);
2434
2729
  }
2435
2730
 
2436
2731
  .checked .check-icon {
@@ -2452,13 +2747,13 @@ RelCheckbox.styles = import_lit4.css`
2452
2747
  glyph with --rel-text-danger, a text token, never a border-* one; the two
2453
2748
  are the same value in light mode but diverge in dark (red-300 vs -400). */
2454
2749
  .error .box::before {
2455
- border-color: var(--rel-text-danger);
2750
+ --_rel-cb-border-color: var(--rel-text-danger);
2456
2751
  }
2457
2752
 
2458
2753
  /* Error checked — error fill, transparent checkmark */
2459
2754
  .error.checked .box::before {
2460
2755
  background: var(--rel-action-danger-fill);
2461
- border-color: var(--rel-action-danger-fill);
2756
+ --_rel-cb-border-color: var(--rel-action-danger-fill);
2462
2757
  }
2463
2758
 
2464
2759
  /* Figma State=Error tints the SUPPORTING text danger; the label stays
@@ -2481,7 +2776,7 @@ RelCheckbox.styles = import_lit4.css`
2481
2776
  (gray-500 vs gray-700) and identical to --rel-text-caption, so a hovered
2482
2777
  label was indistinguishable from supporting text. */
2483
2778
  .default .row:hover .box::before {
2484
- border-color: var(--rel-text-label);
2779
+ --_rel-cb-border-color: var(--rel-text-label);
2485
2780
  }
2486
2781
 
2487
2782
  .default .row:hover .label {
@@ -2490,7 +2785,7 @@ RelCheckbox.styles = import_lit4.css`
2490
2785
 
2491
2786
  .default.checked .row:hover .box::before {
2492
2787
  background: var(--rel-text-label);
2493
- border-color: var(--rel-text-label);
2788
+ --_rel-cb-border-color: var(--rel-text-label);
2494
2789
  }
2495
2790
 
2496
2791
  .default.indeterminate .row:hover .indeterminate-icon {
@@ -2546,19 +2841,80 @@ RelCheckbox.styles = import_lit4.css`
2546
2841
  role and compact indent token. The drawn square is 80% of the frame
2547
2842
  (see .box::before above), so these numbers are footprints, not squares:
2548
2843
  small 16 body-sm / body-xs indent xl (24)
2549
- medium 18 body-md / body-xs indent xl (24)
2550
- large 20 body-lg / body-sm indent 2xl (28)
2551
- extra-large 24 body-xl / body-md indent 3xl (32)
2552
- Medium is the base declared here; the other three override. */
2844
+ medium 18 body-md / body-sm indent xl (24)
2845
+ large 20 body-lg / body-md indent 2xl (28)
2846
+ extra-large 24 body-xl / body-lg indent 3xl (32)
2847
+ Verified against the live Published DS file 2026-09-25: .Check Control is
2848
+ 16/18/20/24 across all 15 variants per size. This ramp is correct — do
2849
+ not "fix" it because the three lower steps look alike; Figma's own steps
2850
+ are 2px apart and the difference it leans on is the type role.
2851
+ Supporting text (2026-09-26 Trello ticket, "visual adjustment"): ONE step
2852
+ below the label on the body scale at every size — 12/10, 14/12, 16/14,
2853
+ 20/16. It used to be two steps below at medium and above. The live file
2854
+ (lastModified 2026-09-23) carries the new pair at Small, Medium and
2855
+ Extra-Large; its Large variants still say body-sm (12) for supporting,
2856
+ which would give Medium and Large the same supporting size and breaks
2857
+ the ticket's own rule, so Large follows the ticket (body-md) and the
2858
+ Figma omission is flagged to the designer rather than copied.
2859
+
2860
+ Only the FRAME and the type roles vary per size. Radius and border are
2861
+ derived from the frame (a ninth of the drawn square, see .box::before),
2862
+ so they cannot fall out of step the way four literals did: large and
2863
+ extra-large previously declared no radius at all and inherited medium's
2864
+ 4px, leaving a 24px box and an 18px box with the same corner. */
2865
+ /* DO NOT ROUND THESE. The expression lands on fractional pixels at two of
2866
+ the four sizes and that is deliberate:
2867
+
2868
+ small 16px frame → 12.8px square → 1.4222px (2.84 device px @2x)
2869
+ medium 18px frame → 14.4px square → 1.6px (3.2)
2870
+ large 20px frame → 16px square → 1.7778px (3.56)
2871
+ extra-large 24px frame → 19.2px square → 2.1333px (4.27)
2872
+
2873
+ The fractions are downstream of the FRAME ramp, which is Figma's and
2874
+ already shipped, so rounding the derived value would break the fixed
2875
+ relationship between the radius and the square it sits on — and that
2876
+ relationship is the only thing making the four sizes read as one control
2877
+ rather than four drawings.
2878
+
2879
+ The blur this replaced was not caused by fractionality. It was a 1.6x
2880
+ ERROR: the dash rendered 1.10px where Figma wanted 1.78px, because an 8:1
2881
+ viewBox was being letterboxed into a square box. Fixing the ratio fixes
2882
+ the symptom; rounding on top would be a second, unrelated invention —
2883
+ and inventing a tidier-looking number than the derived one is exactly
2884
+ what produced the rel-radio Small regression that 1.4.9 reverts.
2885
+
2886
+ ── WHY THE WALL IS AN INSET SHADOW AND NOT A BORDER ──
2887
+ Because border-width is the one length here that the engine refuses to
2888
+ keep. Blink snaps it to whole CSS pixels at used-value time: declared as
2889
+ a border, the derived value computed to 1 / 1 / 1 / 2px for the four
2890
+ sizes, and read the same at device pixel ratios 1, 2 AND 3, while the
2891
+ radius on the very same element read 1.42222px throughout. One property
2892
+ keeping its fraction is the control group for the other losing it, so
2893
+ that is snapping and not a getComputedStyle artefact (measured
2894
+ 2026-09-25).
2895
+
2896
+ Snapped, the faithful value painted WORSE than the flat 2px guess it
2897
+ replaced — sum of absolute painted error 1.933px against 1.333px:
2898
+
2899
+ size Figma as border error flat 2px error
2900
+ s 1.4222 1px 0.422 2px 0.578
2901
+ m 1.6 1px 0.600 2px 0.400
2902
+ l 1.7778 1px 0.778 2px 0.222
2903
+ xl 2.1333 2px 0.133 2px 0.133
2904
+
2905
+ box-shadow spread is NOT snapped — it antialiases sub-pixel — so an inset
2906
+ shadow paints the derived value exactly at every size and the error goes
2907
+ to ~0, with no CSS round() and so no browser-floor question. */
2553
2908
  :host {
2554
2909
  --_rel-cb-size: 18px;
2555
- --_rel-cb-radius: 4px;
2910
+ /* drawn square = frame × 0.8; every Figma value in it = square ÷ 9 */
2911
+ --_rel-cb-radius: calc(var(--_rel-cb-size) * 0.8 / 9);
2912
+ --_rel-cb-border: calc(var(--_rel-cb-size) * 0.8 / 9);
2556
2913
  --_rel-cb-indent: var(--rel-spacing-xl);
2557
2914
  }
2558
2915
 
2559
2916
  :host([size="small"]) {
2560
2917
  --_rel-cb-size: 16px;
2561
- --_rel-cb-radius: 3px;
2562
2918
  --_rel-cb-indent: var(--rel-spacing-xl);
2563
2919
  }
2564
2920
 
@@ -2582,11 +2938,11 @@ RelCheckbox.styles = import_lit4.css`
2582
2938
  }
2583
2939
 
2584
2940
  .text {
2585
- font-family: var(--rel-body-xs-regular-font-family);
2586
- font-weight: var(--rel-body-xs-regular-font-weight);
2587
- font-size: var(--rel-body-xs-regular-font-size);
2588
- line-height: var(--rel-body-xs-regular-line-height);
2589
- letter-spacing: var(--rel-body-xs-regular-letter-spacing);
2941
+ font-family: var(--rel-body-sm-regular-font-family);
2942
+ font-weight: var(--rel-body-sm-regular-font-weight);
2943
+ font-size: var(--rel-body-sm-regular-font-size);
2944
+ line-height: var(--rel-body-sm-regular-line-height);
2945
+ letter-spacing: var(--rel-body-sm-regular-letter-spacing);
2590
2946
  }
2591
2947
 
2592
2948
  :host([size="small"]) .label {
@@ -2597,6 +2953,14 @@ RelCheckbox.styles = import_lit4.css`
2597
2953
  letter-spacing: var(--rel-body-sm-regular-letter-spacing);
2598
2954
  }
2599
2955
 
2956
+ :host([size="small"]) .text {
2957
+ font-family: var(--rel-body-xs-regular-font-family);
2958
+ font-weight: var(--rel-body-xs-regular-font-weight);
2959
+ font-size: var(--rel-body-xs-regular-font-size);
2960
+ line-height: var(--rel-body-xs-regular-line-height);
2961
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
2962
+ }
2963
+
2600
2964
  :host([size="large"]) .label {
2601
2965
  font-family: var(--rel-body-lg-regular-font-family);
2602
2966
  font-weight: var(--rel-body-lg-regular-font-weight);
@@ -2606,11 +2970,11 @@ RelCheckbox.styles = import_lit4.css`
2606
2970
  }
2607
2971
 
2608
2972
  :host([size="large"]) .text {
2609
- font-family: var(--rel-body-sm-regular-font-family);
2610
- font-weight: var(--rel-body-sm-regular-font-weight);
2611
- font-size: var(--rel-body-sm-regular-font-size);
2612
- line-height: var(--rel-body-sm-regular-line-height);
2613
- letter-spacing: var(--rel-body-sm-regular-letter-spacing);
2973
+ font-family: var(--rel-body-md-regular-font-family);
2974
+ font-weight: var(--rel-body-md-regular-font-weight);
2975
+ font-size: var(--rel-body-md-regular-font-size);
2976
+ line-height: var(--rel-body-md-regular-line-height);
2977
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
2614
2978
  }
2615
2979
 
2616
2980
  :host([size="extra-large"]) .label {
@@ -2622,11 +2986,11 @@ RelCheckbox.styles = import_lit4.css`
2622
2986
  }
2623
2987
 
2624
2988
  :host([size="extra-large"]) .text {
2625
- font-family: var(--rel-body-md-regular-font-family);
2626
- font-weight: var(--rel-body-md-regular-font-weight);
2627
- font-size: var(--rel-body-md-regular-font-size);
2628
- line-height: var(--rel-body-md-regular-line-height);
2629
- letter-spacing: var(--rel-body-md-regular-letter-spacing);
2989
+ font-family: var(--rel-body-lg-regular-font-family);
2990
+ font-weight: var(--rel-body-lg-regular-font-weight);
2991
+ font-size: var(--rel-body-lg-regular-font-size);
2992
+ line-height: var(--rel-body-lg-regular-line-height);
2993
+ letter-spacing: var(--rel-body-lg-regular-letter-spacing);
2630
2994
  }
2631
2995
  `;
2632
2996
  __decorateClass([
@@ -2656,6 +3020,9 @@ __decorateClass([
2656
3020
  __decorateClass([
2657
3021
  (0, import_decorators4.property)({ type: String, reflect: true })
2658
3022
  ], RelCheckbox.prototype, "alignment", 2);
3023
+ __decorateClass([
3024
+ (0, import_decorators4.property)({ type: String, attribute: "field-aria-label" })
3025
+ ], RelCheckbox.prototype, "fieldAriaLabel", 2);
2659
3026
  RelCheckbox = __decorateClass([
2660
3027
  (0, import_decorators4.customElement)("rel-checkbox")
2661
3028
  ], RelCheckbox);
@@ -3741,6 +4108,14 @@ var RelAutocomplete = class extends import_lit8.LitElement {
3741
4108
  get optionCheckboxSize() {
3742
4109
  return this.size;
3743
4110
  }
4111
+ /** Native tooltip carrying the row's full text. Both lines truncate to one
4112
+ * line with an ellipsis, so a long supporting line is otherwise unrecoverable
4113
+ * by sight; hovering the row reveals it. Only rows that carry a second line
4114
+ * get one, so plain lists do not sprout tooltips. */
4115
+ rowTitle(item) {
4116
+ return item.supportingText ? `${item.label}
4117
+ ${item.supportingText}` : import_lit8.nothing;
4118
+ }
3744
4119
  get selectAllStatus() {
3745
4120
  const filtered = this.filteredItems.filter((i) => !i.disabled);
3746
4121
  if (filtered.length === 0) return "unchecked";
@@ -4018,6 +4393,7 @@ var RelAutocomplete = class extends import_lit8.LitElement {
4018
4393
  role="option"
4019
4394
  aria-selected="${this.selectedValues.includes(item.value)}"
4020
4395
  aria-disabled="${item.disabled ? "true" : import_lit8.nothing}"
4396
+ title="${this.rowTitle(item)}"
4021
4397
  @mousedown="${(e) => e.preventDefault()}"
4022
4398
  @click="${() => this.toggleMultiOption(item)}"
4023
4399
  @pointerenter="${() => {
@@ -4041,6 +4417,7 @@ var RelAutocomplete = class extends import_lit8.LitElement {
4041
4417
  role="option"
4042
4418
  aria-selected="${item.value === this.value}"
4043
4419
  aria-disabled="${item.disabled ? "true" : import_lit8.nothing}"
4420
+ title="${this.rowTitle(item)}"
4044
4421
  @mousedown="${(e) => e.preventDefault()}"
4045
4422
  @click="${() => this.selectItem(item)}"
4046
4423
  @pointerenter="${() => {
@@ -4083,7 +4460,11 @@ RelAutocomplete.styles = import_lit8.css`
4083
4460
  surface with a 4px radius and shadow/md, and no border. ── */
4084
4461
  .ac-panel {
4085
4462
  position: fixed;
4086
- z-index: 1001;
4463
+ /* Only consulted on the no-Popover-API fallback path: once the panel is
4464
+ promoted to the top layer, z-index is inert and stacking is by
4465
+ promotion order. Shared with the pickers so the whole overlay family
4466
+ is overridable from one place. */
4467
+ z-index: var(--rel-overlay-fallback-z, 1001);
4087
4468
  display: none;
4088
4469
  /* Reset native [popover] UA styles (the panel is promoted to the
4089
4470
  top layer so transformed ancestors — e.g. a centered rel-modal
@@ -4169,13 +4550,32 @@ RelAutocomplete.styles = import_lit8.css`
4169
4550
  }
4170
4551
 
4171
4552
  /* Hover and the keyboard highlight share Figma's Hover wash; Active —
4172
- the committed selection — reuses it and bumps the label to medium. */
4173
- .ac-option:hover,
4553
+ the committed selection — reuses it and bumps the label to medium.
4554
+
4555
+ NOTE there is deliberately no :hover in this rule. The pointerenter
4556
+ handler already moves highlightedIndex onto the hovered row, so hover
4557
+ is expressed through .highlighted and a :hover selector here would be
4558
+ redundant — except in one case, where it was a real defect: arrowing
4559
+ the list moves .highlighted but CANNOT clear a CSS :hover, so the row
4560
+ the mouse happens to rest on stayed lit alongside the keyboard cursor
4561
+ and the committed selection. Three rows painted the same wash for
4562
+ three different meanings, and the user could not tell which one Enter
4563
+ would commit. Measured before the fix: rows 0/2/4 all rgb(243,243,243)
4564
+ with row 2 carrying :hover and .highlighted=false. */
4174
4565
  .ac-option.highlighted,
4175
4566
  .ac-option[aria-selected="true"] {
4176
4567
  background: var(--rel-autocomplete-option-hover, var(--rel-action-overlay-hover));
4177
4568
  }
4178
4569
 
4570
+ /* The Select-all row is the one row with no pointerenter handler (it is
4571
+ not in the options list, so it has no index to highlight), so it keeps
4572
+ a plain :hover. rel-select instead carries a selectAllHighlighted
4573
+ state for this; autocomplete has no such state and does not need one
4574
+ for a row that cannot be reached by the arrow keys. */
4575
+ .ac-select-all:hover {
4576
+ background: var(--rel-autocomplete-option-hover, var(--rel-action-overlay-hover));
4577
+ }
4578
+
4179
4579
  .ac-option:active {
4180
4580
  background: var(--rel-autocomplete-option-pressed, var(--rel-action-overlay-pressed));
4181
4581
  }
@@ -4273,6 +4673,28 @@ RelAutocomplete.styles = import_lit8.css`
4273
4673
  letter-spacing: var(--rel-body-sm-regular-letter-spacing);
4274
4674
  }
4275
4675
 
4676
+ /* Multi-select rows delegate to a nested rel-checkbox, which wraps its
4677
+ text by default, so the SAME item rendered as one truncated line in
4678
+ single mode and as three wrapped lines — or a sideways-overflowing
4679
+ identifier — under multiple (measured 2026-09-26: row 58px vs 120px,
4680
+ panel scrollWidth 326 in a 320 panel). The checkbox's label and helper
4681
+ are exposed as parts, so the row can reach them from here and apply the
4682
+ same one-line ellipsis; the host itself needs min-width: 0 or the flex
4683
+ row lets it grow past the panel instead of shrinking its text. */
4684
+ .ac-option > rel-checkbox {
4685
+ flex: 1;
4686
+ min-width: 0;
4687
+ }
4688
+
4689
+ .ac-option > rel-checkbox::part(label),
4690
+ .ac-option > rel-checkbox::part(helper) {
4691
+ display: block;
4692
+ min-width: 0;
4693
+ overflow: hidden;
4694
+ text-overflow: ellipsis;
4695
+ white-space: nowrap;
4696
+ }
4697
+
4276
4698
  :host([size="large"]) .ac-option-supporting {
4277
4699
  font-family: var(--rel-body-md-regular-font-family);
4278
4700
  font-weight: var(--rel-body-md-regular-font-weight);
@@ -6637,9 +7059,23 @@ RelCarousel.styles = import_lit14.css`
6637
7059
  height: 100%;
6638
7060
  }
6639
7061
 
7062
+ /* The height override stretches a content-height slide to fill the fixed
7063
+ viewport, so a caption that used to hug its text was left anchored to
7064
+ the top of a much taller box — measured 84px of content top-aligned in
7065
+ 180px, i.e. ~96px of dead space beneath it. Figma centres it: every
7066
+ vertical variant has the Content slot on justify=CENTER align=CENTER.
7067
+ Centring needs a formatting context the slide does not otherwise have,
7068
+ hence the flex display. It is behind a hook because forcing flex onto a
7069
+ consumer's slotted element can reflow that element's own children —
7070
+ set --rel-carousel-slide-display: block to opt out. Scoped to vertical
7071
+ only: horizontal slides are content-height, so there is nothing to
7072
+ centre and no reason to touch their box model. */
6640
7073
  :host([orientation="vertical"]) ::slotted(*) {
6641
7074
  width: 100% !important;
6642
7075
  height: var(--rel-carousel-slide-height, 100%) !important;
7076
+ display: var(--rel-carousel-slide-display, flex);
7077
+ flex-direction: column;
7078
+ justify-content: center;
6643
7079
  }
6644
7080
 
6645
7081
  .track.dragging {
@@ -6759,6 +7195,50 @@ RelCarousel.styles = import_lit14.css`
6759
7195
  transform: none;
6760
7196
  }
6761
7197
 
7198
+ /* Vertical orientation: the arrows ride the axis the carousel actually
7199
+ moves on. Without this they kept the horizontal placement — measured
7200
+ prev at x=32 y=98 and next at x=264 y=98, i.e. mid-left and mid-right,
7201
+ identical to the horizontal case — so they flanked the caption from
7202
+ both sides while the motion was up and down. Figma nests both arrow
7203
+ buttons in a layout=col Stack for the vertical variants. */
7204
+ :host([orientation="vertical"]) .nav-button {
7205
+ top: auto;
7206
+ left: 50%;
7207
+ right: auto;
7208
+ transform: translateX(-50%);
7209
+ }
7210
+
7211
+ :host([orientation="vertical"]) .nav-button.prev {
7212
+ top: var(--rel-spacing-xs);
7213
+ left: 50%;
7214
+ }
7215
+
7216
+ :host([orientation="vertical"]) .nav-button.next {
7217
+ bottom: var(--rel-spacing-xs);
7218
+ right: auto;
7219
+ }
7220
+
7221
+ /* chevron-left becomes up, chevron-right becomes down. Applied to the
7222
+ bottom-arrows row too: those buttons still page the carousel along the
7223
+ vertical axis, so a left/right glyph would misdescribe them wherever
7224
+ they sit. */
7225
+ :host([orientation="vertical"]) .nav-button svg {
7226
+ transform: rotate(90deg);
7227
+ }
7228
+
7229
+ /* Type=Bottom Arrows keeps its arrows inline in the indicators row even
7230
+ when the carousel is vertical. This rule has to restate the reset
7231
+ because the vertical block above outranks the plain .dots .nav-button
7232
+ selector (0,3,0 against 0,2,0). */
7233
+ :host([orientation="vertical"]) .dots .nav-button {
7234
+ position: static;
7235
+ transform: none;
7236
+ top: auto;
7237
+ bottom: auto;
7238
+ left: auto;
7239
+ right: auto;
7240
+ }
7241
+
6762
7242
  .nav-button svg {
6763
7243
  width: 16px;
6764
7244
  height: 16px;
@@ -6993,6 +7473,7 @@ RelCardMedia = __decorateClass([
6993
7473
  var import_lit16 = require("lit");
6994
7474
  var import_decorators15 = require("lit/decorators.js");
6995
7475
  var import_live = require("lit/directives/live.js");
7476
+ var import_rel_icon9 = require("@reliability-design/icons/rel-icon");
6996
7477
 
6997
7478
  // src/components/rel-date-picker/date-format.ts
6998
7479
  function formatDatePattern(date, pattern, locale = "en-US") {
@@ -7353,6 +7834,34 @@ var RelDatePicker = class extends import_lit16.LitElement {
7353
7834
  this.onReposition = () => {
7354
7835
  if (this.open) this.updatePopoverPosition();
7355
7836
  };
7837
+ /* Outside scroll CLOSES the panel; it used to reposition.
7838
+ *
7839
+ * Repositioning is what produced the reported "panel floats over the site
7840
+ * header" bug: the panel is in the top layer, so it paints above all normal
7841
+ * content by definition, and tracking a trigger that scrolls under a sticky
7842
+ * nav leaves a calendar anchored to something nobody can see. No z-index can
7843
+ * fix that — the top layer is not in the page's stacking order at all.
7844
+ *
7845
+ * rel-select reached the same conclusion for its own listbox and its comment
7846
+ * records the other half of the reason: re-deriving fixed coordinates on
7847
+ * every scroll event is fragile and was making the panel jump to the top of
7848
+ * the viewport. This is that pattern, so the family now behaves alike.
7849
+ *
7850
+ * Scrolling the panel's OWN content must not close it. The test is
7851
+ * CONTAINMENT, not equality with one known scroller: the panel is itself
7852
+ * `overflow-y: auto` today, but the same guard has to hold for any scroller
7853
+ * nested inside it (rel-time-picker's per-column lists are exactly that
7854
+ * case), and an equality test would silently close the panel the first time
7855
+ * one is added.
7856
+ *
7857
+ * resize still repositions — a resize keeps the trigger on screen, so there
7858
+ * is nothing to run away from. */
7859
+ this.onPageScroll = (e) => {
7860
+ if (!this.open) return;
7861
+ const panel = this.popoverElement;
7862
+ if (panel && e.composedPath().includes(panel)) return;
7863
+ this.closeCalendarPopover(false);
7864
+ };
7356
7865
  this.updatePopoverPosition = () => {
7357
7866
  const panel = this.popoverElement;
7358
7867
  const anchor = this.triggerButton;
@@ -7478,7 +7987,7 @@ var RelDatePicker = class extends import_lit16.LitElement {
7478
7987
  document.removeEventListener("click", this.handleClickOutside);
7479
7988
  document.removeEventListener("keydown", this.handleEscapeKey);
7480
7989
  window.removeEventListener("resize", this.onReposition);
7481
- window.removeEventListener("scroll", this.onReposition, true);
7990
+ window.removeEventListener("scroll", this.onPageScroll, true);
7482
7991
  this.hideTopLayer();
7483
7992
  }
7484
7993
  updated(changedProperties) {
@@ -7492,7 +8001,7 @@ var RelDatePicker = class extends import_lit16.LitElement {
7492
8001
  if (changedProperties.has("open")) {
7493
8002
  if (!this.open) {
7494
8003
  window.removeEventListener("resize", this.onReposition);
7495
- window.removeEventListener("scroll", this.onReposition, true);
8004
+ window.removeEventListener("scroll", this.onPageScroll, true);
7496
8005
  this.hideTopLayer();
7497
8006
  this.focusPanelOnOpen = true;
7498
8007
  this.panelView = "days";
@@ -7500,7 +8009,7 @@ var RelDatePicker = class extends import_lit16.LitElement {
7500
8009
  }
7501
8010
  this.showTopLayer();
7502
8011
  window.addEventListener("resize", this.onReposition);
7503
- window.addEventListener("scroll", this.onReposition, true);
8012
+ window.addEventListener("scroll", this.onPageScroll, true);
7504
8013
  const focusPanel = this.focusPanelOnOpen;
7505
8014
  this.focusPanelOnOpen = true;
7506
8015
  if (focusPanel) {
@@ -8012,6 +8521,28 @@ var RelDatePicker = class extends import_lit16.LitElement {
8012
8521
  * Index of the single grid cell carrying tabindex="0". The old expression
8013
8522
  * (`isSelected || (index === 0 && !value)`) left the grid with ZERO tab
8014
8523
  * stops once the user arrowed around without a selection.
8524
+ *
8525
+ * KNOWN GAP (deferred from 1.4.9, needs a ticket covering BOTH calendars):
8526
+ * `focusedDate` is read here as the first candidate but is never assigned
8527
+ * anywhere in this file, so it is permanently null and the tab stop tracks
8528
+ * the SELECTION rather than focus. Tab out of the grid and back and you
8529
+ * return to the selected day, not the day you left.
8530
+ *
8531
+ * Wiring it from each cell's `@focus` was tried and is NOT sufficient, which
8532
+ * is why this is still open. Measured 2026-09-25 by driving real keys: from
8533
+ * Sun 27 Sep, one ArrowDown lands focus on Sun 25 Oct instead of Sun 4 Oct,
8534
+ * because handleDayKeydown moves by grid INDEX and then lets the view flip
8535
+ * month, so index+7 addresses a different date in the new grid. lit reuses
8536
+ * the same button elements across that re-render, so the focused element
8537
+ * never changes and NO focus event fires — the handler cannot observe the
8538
+ * move it is supposed to record.
8539
+ *
8540
+ * The real fix is to navigate by DATE and set the rove explicitly before
8541
+ * focusing, the way rel-range-picker.moveGridFocusTo already does (verified
8542
+ * correct in the same run: Sep 27 → Oct 4 → Oct 11, tab stop and focus
8543
+ * agreeing at every step). That is a rewrite of this component's arrow-key
8544
+ * model, not a contained fix, and it must land on both calendars together or
8545
+ * they diverge again.
8015
8546
  */
8016
8547
  getRovingIndex(days) {
8017
8548
  const candidates = [
@@ -8226,11 +8757,11 @@ var RelDatePicker = class extends import_lit16.LitElement {
8226
8757
  ?disabled=${this.disabled}
8227
8758
  @click=${this.handleIconClick}
8228
8759
  >
8229
- <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
8230
- <path
8231
- d="M4 2h1V1h2v1h2V1h2v1h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm8 3H4v7h8V5z"
8232
- />
8233
- </svg>
8760
+ <!-- Figma pins this suffix to node 5137:1877, which resolves to
8761
+ COMPONENT_SET 5137:1876 "Calendar Month" — verified against
8762
+ the live Published DS file 2026-09-25. This was a hand-drawn
8763
+ inline path that matched no icon in the set. -->
8764
+ <rel-icon name="calendar-month"></rel-icon>
8234
8765
  </button>
8235
8766
  </span>
8236
8767
  </div>
@@ -8589,18 +9120,28 @@ RelDatePicker.styles = import_lit16.css`
8589
9120
  }
8590
9121
 
8591
9122
  /* Figma's suffix icon is 18px at medium; rel-textfield ramps 16 / 18 / 20
8592
- across the size scale, so the trigger follows the same ramp. */
8593
- .icon-button svg {
9123
+ across the size scale, so the trigger follows the same ramp.
9124
+ Both selectors are needed and neither is redundant: the CLEAR (×) button
9125
+ is still a raw inline <svg>, while the calendar suffix is now a
9126
+ <rel-icon> whose own svg lives in a shadow root that .icon-button svg
9127
+ cannot reach. Sizing the rel-icon ELEMENT works because a declaration
9128
+ from the outer tree beats the :host([size]) rules inside it — which is
9129
+ also why the element carries no size attribute: one source for the
9130
+ ramp, not two that have to agree. */
9131
+ .icon-button svg,
9132
+ .icon-button rel-icon {
8594
9133
  width: 18px;
8595
9134
  height: 18px;
8596
9135
  }
8597
9136
 
8598
- .date-picker-wrapper.small .icon-button svg {
9137
+ .date-picker-wrapper.small .icon-button svg,
9138
+ .date-picker-wrapper.small .icon-button rel-icon {
8599
9139
  width: 16px;
8600
9140
  height: 16px;
8601
9141
  }
8602
9142
 
8603
- .date-picker-wrapper.large .icon-button svg {
9143
+ .date-picker-wrapper.large .icon-button svg,
9144
+ .date-picker-wrapper.large .icon-button rel-icon {
8604
9145
  width: 20px;
8605
9146
  height: 20px;
8606
9147
  }
@@ -8624,7 +9165,14 @@ RelDatePicker.styles = import_lit16.css`
8624
9165
  position: absolute;
8625
9166
  top: calc(100% + 8px);
8626
9167
  left: 0;
8627
- z-index: 2147483647;
9168
+ /* Fallback stacking ONLY: while the panel is in the top layer this is
9169
+ inert, because top-layer elements are not in the page stacking order at
9170
+ all and order of promotion decides. It applies solely where the Popover
9171
+ API is unavailable. One overridable hook across every overlay, so a
9172
+ consumer with its own stacking scheme sets it once; the default matches
9173
+ rel-select / rel-autocomplete / rel-popover / rel-menubar. This was
9174
+ 2147483647, which cannot be layered against anything. */
9175
+ z-index: var(--rel-overlay-fallback-z, 1001);
8628
9176
  background: var(--rel-bg-neutral);
8629
9177
  border-radius: var(--rel-border-radius-sm);
8630
9178
  /* Figma ".Date Picker Calendar" is a shadowed card with no border.
@@ -9121,7 +9669,7 @@ RelDatePicker = __decorateClass([
9121
9669
  var import_lit17 = require("lit");
9122
9670
  var import_decorators16 = require("lit/decorators.js");
9123
9671
  var import_live2 = require("lit/directives/live.js");
9124
- var import_rel_icon9 = require("@reliability-design/icons/rel-icon");
9672
+ var import_rel_icon10 = require("@reliability-design/icons/rel-icon");
9125
9673
 
9126
9674
  // src/utils/shortcuts.ts
9127
9675
  function isApplePlatform() {
@@ -9288,6 +9836,21 @@ var RelRangePicker = class extends import_lit17.LitElement {
9288
9836
  this.onReposition = () => {
9289
9837
  if (this.open && this.mode === "popover") this.updatePopoverPosition();
9290
9838
  };
9839
+ /* Outside scroll CLOSES the panel; it used to reposition. Same change as
9840
+ * rel-date-picker and rel-time-picker — see the long note in
9841
+ * rel-date-picker.onPageScroll for why tracking the trigger is what put the
9842
+ * panel over the sticky site header, and why z-index cannot fix it.
9843
+ *
9844
+ * ONLY in popover mode. `popup` mode centres the shell in the viewport behind
9845
+ * a backdrop and never anchors to the trigger, so there is no trigger to
9846
+ * scroll away from and closing on scroll would just be a dialog that vanishes
9847
+ * when the page moves. Guarded exactly as onReposition above is. */
9848
+ this.onPageScroll = (e) => {
9849
+ if (!this.open || this.mode !== "popover") return;
9850
+ const panel = this.popoverEl;
9851
+ if (panel && e.composedPath().includes(panel)) return;
9852
+ this.cancelPopover();
9853
+ };
9291
9854
  this.updatePopoverPosition = () => {
9292
9855
  const panel = this.popoverEl;
9293
9856
  if (!panel || !this.open || this.mode !== "popover") return;
@@ -9500,7 +10063,7 @@ var RelRangePicker = class extends import_lit17.LitElement {
9500
10063
  document.removeEventListener("click", this.handleClickOutside);
9501
10064
  document.removeEventListener("keydown", this.handleGlobalKeydown);
9502
10065
  window.removeEventListener("resize", this.onReposition);
9503
- window.removeEventListener("scroll", this.onReposition, true);
10066
+ window.removeEventListener("scroll", this.onPageScroll, true);
9504
10067
  this.hideTopLayer();
9505
10068
  }
9506
10069
  updated(changed) {
@@ -9514,10 +10077,10 @@ var RelRangePicker = class extends import_lit17.LitElement {
9514
10077
  if (this.open) {
9515
10078
  this.showTopLayer();
9516
10079
  window.addEventListener("resize", this.onReposition);
9517
- window.addEventListener("scroll", this.onReposition, true);
10080
+ window.addEventListener("scroll", this.onPageScroll, true);
9518
10081
  } else {
9519
10082
  window.removeEventListener("resize", this.onReposition);
9520
- window.removeEventListener("scroll", this.onReposition, true);
10083
+ window.removeEventListener("scroll", this.onPageScroll, true);
9521
10084
  this.hideTopLayer();
9522
10085
  }
9523
10086
  }
@@ -10131,6 +10694,21 @@ var RelRangePicker = class extends import_lit17.LitElement {
10131
10694
  * The cell that carries tabindex="0". Prefers the last roved date, then the
10132
10695
  * draft start, then today, then the first selectable day — always exactly
10133
10696
  * one Tab stop, even after arrowing away with nothing selected.
10697
+ *
10698
+ * `focusedDate` is written by moveGridFocusTo, which sets it from the DATE
10699
+ * before moving focus rather than reading it back off a focus event. That
10700
+ * ordering is what makes the arrow keys correct across a month boundary —
10701
+ * verified 2026-09-25 by driving real keys: Sep 27 → Oct 4 → Oct 11 with the
10702
+ * tab stop and focus agreeing at every step, including the view change.
10703
+ * rel-date-picker moves by grid INDEX instead and lands on the wrong date
10704
+ * there; see the note on its getRovingIndex. Do not "simplify" this to a
10705
+ * focus listener — lit reuses the same cell elements across the re-render, so
10706
+ * the focused element does not change and no focus event fires.
10707
+ *
10708
+ * Still open for BOTH calendars: focus arriving from a click or from Tab does
10709
+ * not update the rove, only the arrow keys do. Deferred from 1.4.9 as a
10710
+ * shared ticket, since fixing one calendar alone reintroduces the divergence
10711
+ * the shared period-page module exists to prevent.
10134
10712
  */
10135
10713
  getRovingIso(days) {
10136
10714
  const inView = days.filter((d) => d.isCurrentMonth);
@@ -10186,7 +10764,14 @@ var RelRangePicker = class extends import_lit17.LitElement {
10186
10764
  aria-label=${triggerLabel || this.placeholder}
10187
10765
  @click=${() => this.toggleOpen()}
10188
10766
  >
10189
- <rel-icon name="calendar-range"></rel-icon>
10767
+ <!-- Deliberately the SAME icon as rel-date-picker, at the user's
10768
+ request, so the two triggers read as one family. Figma does
10769
+ NOT specify a trigger icon here: the range-picker spec lines
10770
+ only carry node 5137:2842, which resolves to "Plus" and
10771
+ appears in 440 places across the component specs — it is the
10772
+ un-overridden INSTANCE_SWAP default, not a choice. So this is
10773
+ ours to pick, and it should move with the date picker. -->
10774
+ <rel-icon name="calendar-month"></rel-icon>
10190
10775
  </rel-button>
10191
10776
  ` : import_lit17.html`
10192
10777
  <div
@@ -10241,9 +10826,7 @@ var RelRangePicker = class extends import_lit17.LitElement {
10241
10826
  ?disabled=${this.disabled}
10242
10827
  @click=${this.handleIconClick}
10243
10828
  >
10244
- <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
10245
- <path d="M4 2h1V1h2v1h2V1h2v1h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm8 3H4v7h8V5z"/>
10246
- </svg>
10829
+ <rel-icon name="calendar-month"></rel-icon>
10247
10830
  </button>
10248
10831
  </span>
10249
10832
  </div>
@@ -10614,9 +11197,18 @@ RelRangePicker.styles = import_lit17.css`
10614
11197
  color: var(--rel-text-default);
10615
11198
  }
10616
11199
 
10617
- .icon-button svg {
11200
+ /* Both selectors, deliberately. The calendar trigger is a <rel-icon>,
11201
+ whose own svg lives in a shadow root that a plain .icon-button svg
11202
+ selector cannot reach — so sizing only the raw svg would leave it at its
11203
+ component default. The clear (x) button is still a hand-drawn inline
11204
+ svg, so the raw selector has to stay as well. Sizing the rel-icon
11205
+ ELEMENT works because a declaration from the outer tree beats the
11206
+ :host([size]) rules inside it. Same fix as rel-date-picker. */
11207
+ .icon-button svg,
11208
+ .icon-button rel-icon {
10618
11209
  width: 16px;
10619
11210
  height: 16px;
11211
+ --rel-icon-size: 16px;
10620
11212
  }
10621
11213
 
10622
11214
  /* ── Popover shell ── */
@@ -10644,7 +11236,14 @@ RelRangePicker.styles = import_lit17.css`
10644
11236
  position: absolute;
10645
11237
  top: calc(100% + 8px);
10646
11238
  left: 0;
10647
- z-index: 2147483647;
11239
+ /* Fallback stacking ONLY: while the panel is in the top layer this is
11240
+ inert, because top-layer elements are not in the page stacking order at
11241
+ all and order of promotion decides. It applies solely where the Popover
11242
+ API is unavailable. One overridable hook across every overlay, so a
11243
+ consumer with its own stacking scheme sets it once; the default matches
11244
+ rel-select / rel-autocomplete / rel-popover / rel-menubar. This was
11245
+ 2147483647, which cannot be layered against anything. */
11246
+ z-index: var(--rel-overlay-fallback-z, 1001);
10648
11247
  border-radius: var(--rel-border-radius-sm, 8px);
10649
11248
  background: var(--rel-bg-neutral);
10650
11249
  /* Figma effectStyle=shadow/xl — the token now exists. */
@@ -10678,7 +11277,8 @@ RelRangePicker.styles = import_lit17.css`
10678
11277
  width: auto;
10679
11278
  height: auto;
10680
11279
  border: none;
10681
- z-index: 2147483646;
11280
+ /* One below the shell, so the backdrop never covers its own panel. */
11281
+ z-index: calc(var(--rel-overlay-fallback-z, 1001) - 1);
10682
11282
  background: rgba(0,0,0,0.4);
10683
11283
  }
10684
11284
 
@@ -11433,7 +12033,7 @@ RelDivider = __decorateClass([
11433
12033
  // src/components/rel-drawer/rel-drawer.ts
11434
12034
  var import_lit21 = require("lit");
11435
12035
  var import_decorators20 = require("lit/decorators.js");
11436
- var import_rel_icon10 = require("@reliability-design/icons/rel-icon");
12036
+ var import_rel_icon11 = require("@reliability-design/icons/rel-icon");
11437
12037
 
11438
12038
  // src/components/rel-drawer/rel-drawer-footer.ts
11439
12039
  var import_lit20 = require("lit");
@@ -12366,12 +12966,12 @@ RelLayout = __decorateClass([
12366
12966
  // src/components/rel-menu/rel-menu.ts
12367
12967
  var import_lit25 = require("lit");
12368
12968
  var import_decorators24 = require("lit/decorators.js");
12369
- var import_rel_icon12 = require("@reliability-design/icons/rel-icon");
12969
+ var import_rel_icon13 = require("@reliability-design/icons/rel-icon");
12370
12970
 
12371
12971
  // src/components/rel-radio/rel-radio.ts
12372
12972
  var import_lit24 = require("lit");
12373
12973
  var import_decorators23 = require("lit/decorators.js");
12374
- var import_rel_icon11 = require("@reliability-design/icons/rel-icon");
12974
+ var import_rel_icon12 = require("@reliability-design/icons/rel-icon");
12375
12975
  var SIZE_ALIASES5 = {
12376
12976
  sm: "small",
12377
12977
  md: "medium",
@@ -12398,18 +12998,27 @@ var RelRadio = class extends import_lit24.LitElement {
12398
12998
  };
12399
12999
  this._onTouched = () => {
12400
13000
  };
13001
+ /** Whether the author wrote `density` themselves, as opposed to inheriting
13002
+ * the default. Read once at connect: Lit reflects during update, which is
13003
+ * after this runs, so the attribute is present here only if authored. */
13004
+ this.densityExplicit = false;
12401
13005
  }
12402
13006
  willUpdate(changed) {
12403
13007
  if (changed.has("size")) {
12404
13008
  const canonical = SIZE_ALIASES5[this.size];
12405
13009
  if (canonical) this.size = canonical;
12406
13010
  }
12407
- if ((changed.has("alignment") || changed.has("density")) && this.alignment === "trailing" && this.density === "compact") {
13011
+ const densityChosen = this.densityExplicit || changed.get("density") !== void 0;
13012
+ if ((changed.has("alignment") || changed.has("density")) && this.alignment === "trailing" && this.density === "compact" && densityChosen) {
12408
13013
  console.warn(
12409
13014
  '[rel-radio] density="compact" has no trailing layout; rendering comfortable density.'
12410
13015
  );
12411
13016
  }
12412
13017
  }
13018
+ connectedCallback() {
13019
+ super.connectedCallback();
13020
+ this.densityExplicit = this.hasAttribute("density");
13021
+ }
12413
13022
  /** Trailing has no compact layout in the spec — trailing wins. */
12414
13023
  get effectiveDensity() {
12415
13024
  return this.alignment === "trailing" ? "comfortable" : this.density;
@@ -12677,13 +13286,22 @@ RelRadio.styles = import_lit24.css`
12677
13286
  letter-spacing: var(--rel-body-lg-regular-letter-spacing);
12678
13287
  }
12679
13288
 
13289
+ /* Supporting text sits ONE step below the label on the body scale at every
13290
+ size — 14/12 small, 16/14 medium, 20/16 large (2026-09-26 Trello ticket,
13291
+ "visual adjustment", confirmed by the designer as applying to every
13292
+ label + supporting pair, not just the checkbox). It used to be two steps
13293
+ below. The Figma ticket covered the Checkbox set only; extending the
13294
+ rule to Radio was proposed from the code side and approved by the
13295
+ designer on 2026-09-26, so the Radio Button set (lastModified
13296
+ 2026-09-23) still shows the old two-step pairs (body-xs / body-sm /
13297
+ body-md) and will be updated to match the code, not the other way. */
12680
13298
  .radio-supporting {
12681
13299
  color: var(--rel-text-caption);
12682
- font-family: var(--rel-body-sm-regular-font-family);
12683
- font-weight: var(--rel-body-sm-regular-font-weight);
12684
- font-size: var(--rel-body-sm-regular-font-size);
12685
- line-height: var(--rel-body-sm-regular-line-height);
12686
- letter-spacing: var(--rel-body-sm-regular-letter-spacing);
13300
+ font-family: var(--rel-body-md-regular-font-family);
13301
+ font-weight: var(--rel-body-md-regular-font-weight);
13302
+ font-size: var(--rel-body-md-regular-font-size);
13303
+ line-height: var(--rel-body-md-regular-line-height);
13304
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
12687
13305
  }
12688
13306
 
12689
13307
  /* Compact density — supporting text sits under the whole row, indented
@@ -12743,11 +13361,11 @@ RelRadio.styles = import_lit24.css`
12743
13361
  }
12744
13362
 
12745
13363
  :host([size="large"]) .radio-supporting {
12746
- font-family: var(--rel-body-md-regular-font-family);
12747
- font-weight: var(--rel-body-md-regular-font-weight);
12748
- font-size: var(--rel-body-md-regular-font-size);
12749
- line-height: var(--rel-body-md-regular-line-height);
12750
- letter-spacing: var(--rel-body-md-regular-letter-spacing);
13364
+ font-family: var(--rel-body-lg-regular-font-family);
13365
+ font-weight: var(--rel-body-lg-regular-font-weight);
13366
+ font-size: var(--rel-body-lg-regular-font-size);
13367
+ line-height: var(--rel-body-lg-regular-line-height);
13368
+ letter-spacing: var(--rel-body-lg-regular-letter-spacing);
12751
13369
  }
12752
13370
 
12753
13371
  :host([size="large"]) .radio-supporting.indent {
@@ -12764,39 +13382,29 @@ RelRadio.styles = import_lit24.css`
12764
13382
  letter-spacing: var(--rel-body-sm-semibold-letter-spacing);
12765
13383
  }
12766
13384
 
12767
- /* Small's own control (ticket 11b). The ramp is 16 / 20 / 24, and every
12768
- number falls out of the invariant the other two sizes already keep:
12769
- the RING is inset 2px from the BOX on each side (box - 4), and the DOT
12770
- is half the ring. M is 20/16/8, L is 24/20/10, so S is 16/12/6 — all
12771
- integers, so the 2px ring stays crisp at 1x and 2x. Deriving from the
12772
- stated ring/box = 0.8 instead would have given 12.8 and a 6.4 dot,
12773
- which is where a 2px border on a fractional circle goes soft. */
12774
- :host([size="small"]) .radio-custom {
12775
- width: 16px;
12776
- height: 16px;
12777
- }
13385
+ /* SMALL DELIBERATELY DECLARES NO CONTROL — it shares medium's 20px box,
13386
+ 16px ring and 8px dot, and only the TYPE below steps down.
12778
13387
 
12779
- :host([size="small"]) .radio-custom::before {
12780
- width: 12px;
12781
- height: 12px;
12782
- }
13388
+ v1.4.8 ticket 11b gave Small its own 16/12/6 control, derived from the
13389
+ premise "the ramp is 16 / 20 / 24". That premise was never read off
13390
+ Figma, and it is wrong. Verified against the LIVE Published DS file on
13391
+ 2026-09-25 (set "Radio Button" 8408:395, 45 variants): .Radio Control
13392
+ measures 20×20 in all 15 Small variants, 20×20 in all 15 Medium, and
13393
+ 24×24 in all 15 Large. Figma discriminates Small from Medium by type
13394
+ alone. memory/project_web_components.md recorded exactly this on
13395
+ 2026-08-21 — "S and M share the 20px control, only typography steps
13396
+ down" — before 1.4.8 overrode it.
12783
13397
 
12784
- :host([size="small"]) .radio-custom::after {
12785
- width: 6px;
12786
- height: 6px;
12787
- }
13398
+ So the correct Small is the ABSENCE of a rule. Adding one back means
13399
+ re-deriving it from Figma, not from the invariant the other two sizes
13400
+ happen to satisfy: that invariant (ring = box − 4, dot = ring ÷ 2) is a
13401
+ coincidence of rounding, since Figma's ring is proportional at 0.8 and
13402
+ our Large ring is a deliberate 19.2 → 20. */
12788
13403
 
12789
- /* The compact indent is box + the 8px row gap, bound to the token that
12790
- equals it — 2xl (28) for M, 3xl (32) for L, so xl (24) for a 16px S. */
13404
+ /* The compact indent is box + the 8px row gap, so Small takes medium's
13405
+ 2xl (28) along with medium's 20px box. */
12791
13406
  :host([size="small"]) .radio-supporting.indent {
12792
- padding-left: var(--rel-spacing-xl);
12793
- }
12794
-
12795
- /* The option avatar matches the control box at every size (20 at M, 24 at
12796
- L), so it follows Small down to 16. */
12797
- :host([size="small"]) .radio-avatar {
12798
- width: 16px;
12799
- height: 16px;
13407
+ padding-left: var(--rel-spacing-2xl);
12800
13408
  }
12801
13409
 
12802
13410
  :host([size="small"]) .radio-label {
@@ -12808,11 +13416,11 @@ RelRadio.styles = import_lit24.css`
12808
13416
  }
12809
13417
 
12810
13418
  :host([size="small"]) .radio-supporting {
12811
- font-family: var(--rel-body-xs-regular-font-family);
12812
- font-weight: var(--rel-body-xs-regular-font-weight);
12813
- font-size: var(--rel-body-xs-regular-font-size);
12814
- line-height: var(--rel-body-xs-regular-line-height);
12815
- letter-spacing: var(--rel-body-xs-regular-letter-spacing);
13419
+ font-family: var(--rel-body-sm-regular-font-family);
13420
+ font-weight: var(--rel-body-sm-regular-font-weight);
13421
+ font-size: var(--rel-body-sm-regular-font-size);
13422
+ line-height: var(--rel-body-sm-regular-line-height);
13423
+ letter-spacing: var(--rel-body-sm-regular-letter-spacing);
12816
13424
  }
12817
13425
 
12818
13426
  /* Error state (Figma State=Error) — ring, dot, and supporting text go
@@ -13427,12 +14035,23 @@ RelMenu._css = `
13427
14035
  flex: 1 1 auto;
13428
14036
  }
13429
14037
 
13430
- /* Submenu affordance \u2014 same rel-icon chevron the menubar uses. */
14038
+ /* Submenu affordance \u2014 same rel-icon chevron the menubar uses.
14039
+
14040
+ No opacity here: verified against the live Figma file, "Chevron Right"
14041
+ is opacity 1 on both the instance and its vector, on every State and
14042
+ Size, bound to the same colour variable as the row label beside it.
14043
+ The 0.6 that used to sit here dulled it to a mid grey.
14044
+
14045
+ The colour is declared rather than inherited on purpose. rel-menu
14046
+ renders to the LIGHT DOM (createRenderRoot returns this), so these
14047
+ selectors are unscoped globals and an inherited colour would travel
14048
+ through the consumer's page cascade \u2014 an app setting a colour on any
14049
+ ancestor would have been repainting this chevron. */
13431
14050
  .submenu-chevron {
13432
14051
  margin-left: auto;
13433
14052
  display: inline-flex;
13434
14053
  align-items: center;
13435
- opacity: 0.6;
14054
+ color: var(--rel-text-default);
13436
14055
  pointer-events: none;
13437
14056
  }
13438
14057
 
@@ -13518,7 +14137,7 @@ RelMenu = __decorateClass([
13518
14137
  // src/components/rel-menubar/rel-menubar.ts
13519
14138
  var import_lit26 = require("lit");
13520
14139
  var import_decorators25 = require("lit/decorators.js");
13521
- var import_rel_icon13 = require("@reliability-design/icons/rel-icon");
14140
+ var import_rel_icon14 = require("@reliability-design/icons/rel-icon");
13522
14141
  var RelMenubar = class extends import_lit26.LitElement {
13523
14142
  constructor() {
13524
14143
  super(...arguments);
@@ -14885,7 +15504,7 @@ RelOtp = __decorateClass([
14885
15504
  // src/components/rel-pagination/rel-pagination.ts
14886
15505
  var import_lit29 = require("lit");
14887
15506
  var import_decorators28 = require("lit/decorators.js");
14888
- var import_rel_icon14 = require("@reliability-design/icons/rel-icon");
15507
+ var import_rel_icon15 = require("@reliability-design/icons/rel-icon");
14889
15508
  var RelPagination = class extends import_lit29.LitElement {
14890
15509
  constructor() {
14891
15510
  super(...arguments);
@@ -16751,7 +17370,7 @@ RelResizableHandle = __decorateClass([
16751
17370
  // src/components/rel-segmented-button/rel-segmented-button.ts
16752
17371
  var import_lit36 = require("lit");
16753
17372
  var import_decorators35 = require("lit/decorators.js");
16754
- var import_rel_icon15 = require("@reliability-design/icons/rel-icon");
17373
+ var import_rel_icon16 = require("@reliability-design/icons/rel-icon");
16755
17374
  var RelSegmentedButton = class extends import_lit36.LitElement {
16756
17375
  constructor() {
16757
17376
  super(...arguments);
@@ -16783,12 +17402,12 @@ var RelSegmentedButton = class extends import_lit36.LitElement {
16783
17402
  setActive(option) {
16784
17403
  if (!option.disabled) {
16785
17404
  this.active = option.value;
17405
+ const detail = { value: option.value };
16786
17406
  this.dispatchEvent(
16787
- new CustomEvent("changed", {
16788
- detail: { value: option.value },
16789
- bubbles: true,
16790
- composed: true
16791
- })
17407
+ new CustomEvent("rel-change", { detail, bubbles: true, composed: true })
17408
+ );
17409
+ this.dispatchEvent(
17410
+ new CustomEvent("changed", { detail, bubbles: true, composed: true })
16792
17411
  );
16793
17412
  }
16794
17413
  }
@@ -17041,7 +17660,7 @@ RelSegmentedButton = __decorateClass([
17041
17660
  var import_lit37 = require("lit");
17042
17661
  var import_decorators36 = require("lit/decorators.js");
17043
17662
  var import_repeat2 = require("lit/directives/repeat.js");
17044
- var import_rel_icon16 = require("@reliability-design/icons/rel-icon");
17663
+ var import_rel_icon17 = require("@reliability-design/icons/rel-icon");
17045
17664
  function normalizeOption(o) {
17046
17665
  if (typeof o === "string") return { value: o, label: o, supportingText: "", icon: "" };
17047
17666
  return {
@@ -17239,6 +17858,14 @@ var RelSelect = class extends import_lit37.LitElement {
17239
17858
  get optionCheckboxSize() {
17240
17859
  return this.size;
17241
17860
  }
17861
+ /** Native tooltip carrying the row's full text. Both lines truncate to one
17862
+ * line with an ellipsis, so a long supporting line is otherwise unrecoverable
17863
+ * by sight; hovering the row reveals it. Only rows that carry a second line
17864
+ * get one, so plain lists do not sprout tooltips. */
17865
+ rowTitle(option) {
17866
+ return option.supportingText ? `${option.label}
17867
+ ${option.supportingText}` : import_lit37.nothing;
17868
+ }
17242
17869
  get selectAllStatus() {
17243
17870
  const filtered = this.filteredOptions;
17244
17871
  if (filtered.length === 0) return "unchecked";
@@ -17257,13 +17884,27 @@ var RelSelect = class extends import_lit37.LitElement {
17257
17884
  window.removeEventListener("resize", this.updatePosition);
17258
17885
  window.removeEventListener("scroll", this.onPageScroll, true);
17259
17886
  }
17887
+ /**
17888
+ * BOTH events fire, deliberately — do not delete either.
17889
+ *
17890
+ * This repo is split down the middle on the convention, not inconsistent in
17891
+ * one place: rel-select and rel-segmented-button emit `changed`, while
17892
+ * rel-switch, rel-checkbox and rel-radio emit `rel-change`. Renaming
17893
+ * `changed` would break every existing rel-select consumer, so 1.4.9 adds
17894
+ * the prefixed name alongside it rather than moving anyone. New code should
17895
+ * listen for `rel-change`; `changed` stays for as long as 1.x does.
17896
+ *
17897
+ * One `detail` object is shared by both dispatches so the two events cannot
17898
+ * drift apart — a listener on one sees exactly what a listener on the other
17899
+ * sees.
17900
+ */
17260
17901
  emitChange() {
17902
+ const detail = { value: this.multiple ? this.selectedValues : this.selected };
17261
17903
  this.dispatchEvent(
17262
- new CustomEvent("changed", {
17263
- detail: { value: this.multiple ? this.selectedValues : this.selected },
17264
- bubbles: true,
17265
- composed: true
17266
- })
17904
+ new CustomEvent("changed", { detail, bubbles: true, composed: true })
17905
+ );
17906
+ this.dispatchEvent(
17907
+ new CustomEvent("rel-change", { detail, bubbles: true, composed: true })
17267
17908
  );
17268
17909
  }
17269
17910
  openListbox() {
@@ -17707,6 +18348,7 @@ var RelSelect = class extends import_lit37.LitElement {
17707
18348
  role="option"
17708
18349
  aria-selected="${this.selectedValues.includes(option.value)}"
17709
18350
  aria-label="${option.label}"
18351
+ title="${this.rowTitle(option)}"
17710
18352
  @pointerenter="${() => {
17711
18353
  this.highlightedIndex = index;
17712
18354
  this.selectAllHighlighted = false;
@@ -17738,6 +18380,7 @@ var RelSelect = class extends import_lit37.LitElement {
17738
18380
  part="option" class="select-option ${index === this.highlightedIndex ? "highlighted" : ""}"
17739
18381
  role="option"
17740
18382
  aria-selected="${option.value === this.selected}"
18383
+ title="${this.rowTitle(option)}"
17741
18384
  @mousedown="${(e) => e.preventDefault()}"
17742
18385
  @click="${() => this.selectSingleOption(option)}"
17743
18386
  @pointerenter="${() => this.highlightedIndex = index}"
@@ -18058,12 +18701,40 @@ RelSelect.styles = import_lit37.css`
18058
18701
  --_rel-sel-icon: 16px;
18059
18702
  }
18060
18703
 
18704
+ /* Border geometry, in one place, so the mask strip below is DERIVED from
18705
+ the paint it has to cover rather than restating it. Declared on the
18706
+ wrapper and not on .select-trigger because .floating-label is the
18707
+ trigger's SIBLING, not its child, and would not inherit from it. */
18708
+ .select-wrapper {
18709
+ --_rel-sel-border-w: 1px;
18710
+ --_rel-sel-ring-w: 0px;
18711
+ --_rel-sel-notch-h: calc(var(--_rel-sel-border-w) + var(--_rel-sel-ring-w));
18712
+ }
18713
+
18714
+ .select-wrapper.focused {
18715
+ --_rel-sel-ring-w: 2px;
18716
+ }
18717
+
18061
18718
  .floating-label {
18062
18719
  position: absolute;
18720
+ /* Matches Figma directly and needs no arithmetic: the live spec puts the
18721
+ "Floating Label" frame at dx=12 from the Input frame's edge, and this
18722
+ element's containing block (.select-container) IS the trigger's outer
18723
+ edge, so --_rel-sel-pad lands it exactly there. rel-textfield needs a
18724
+ different value (0) for the same result, because ITS containing block
18725
+ already starts inside the border and padding. Do not copy one into the
18726
+ other — I briefly "corrected" this one to subtract the notch padding
18727
+ and moved a label that was already right. */
18063
18728
  left: var(--_rel-sel-pad, var(--rel-spacing-sm));
18064
18729
  top: 50%;
18065
18730
  transform: translateY(-50%);
18066
- background: var(--rel-select-bg, var(--rel-bg-neutral));
18731
+ /* NO background here. It used to paint --rel-select-bg across the
18732
+ label's whole ~20px line box to hide a 1px border, which erased about
18733
+ 10px of the trigger's interior and 10px of whatever sat above it.
18734
+ Figma does not do that: the "Floating Label" frame is a strip whose
18735
+ HEIGHT EQUALS THE BORDER IT MASKS — size=45x1 at rest and 45x2 when
18736
+ focused (rel-select.md). rel-textfield already moved off the chip for
18737
+ exactly this reason; the strip is now on ::before below. */
18067
18738
  /* Figma pads the floating-label chip by --rel-spacing-2xs; this was a
18068
18739
  raw 4px that happened to match but was not on the scale. */
18069
18740
  padding: 0 var(--rel-spacing-2xs);
@@ -18109,6 +18780,8 @@ RelSelect.styles = import_lit37.css`
18109
18780
  .select-wrapper.has-value .floating-label,
18110
18781
  .select-wrapper.open .floating-label {
18111
18782
  top: 0;
18783
+ /* Same inset as the resting rule above, so the label does not shift
18784
+ horizontally as it floats. */
18112
18785
  left: var(--_rel-sel-pad, var(--rel-spacing-sm));
18113
18786
  /* Figma's floated label is REGULAR 400, not medium 500. */
18114
18787
  font-family: var(--rel-body-sm-regular-font-family);
@@ -18137,10 +18810,66 @@ RelSelect.styles = import_lit37.css`
18137
18810
  letter-spacing: var(--rel-body-xs-regular-letter-spacing);
18138
18811
  }
18139
18812
 
18140
- /* Focus thickens the border to 2px; the chip rides up with it so it stays
18141
- optically centred on the stroke rather than sinking into the field. */
18142
- .select-wrapper.focused .floating-label {
18143
- top: -2px;
18813
+ /* The mask: TWO OVERLAPPING strips, same idea as rel-textfield, different
18814
+ reference — and the reference is the whole reason this cannot be copied
18815
+ across. In rel-textfield the label is positioned inside the control, so
18816
+ its 50% is one border-width BELOW the border-box top. Here
18817
+ .floating-label is a SIBLING of .select-trigger inside
18818
+ .select-container, so its 50% IS the trigger's border-box top. Measured
18819
+ from there the focus outline occupies [50% - ringW, 50%] (outlines draw
18820
+ outside the border box) and the border [50%, 50% + borderW].
18821
+ A single strip cut to exactly that band leaked at some zoom levels and
18822
+ not others (v1.4.9 "slash" report): border and outline are
18823
+ pixel-snapped by Blink, while a strip inside a translateY(-50%) label
18824
+ rasterises at a fractional offset, so their edges disagreed by under a
18825
+ device pixel and a hairline showed through the text. So each strip
18826
+ overshoots into paint of ITS OWN colour, and the two overlap so no two
18827
+ anti-aliased edges share a row (abutting edges leak a*(1-a) of the band
18828
+ — measured as exactly a quarter of the ring colour on rel-textfield):
18829
+ ::before surface colour, ring + border + 2px above + 2px below
18830
+ ::after the trigger's FILL, border + 3px below, painted on top
18831
+ 2px of overshoot, not 1: the strip and the band snap to the device grid
18832
+ in different coordinate spaces and can disagree by a full pixel. */
18833
+ .select-wrapper.focused .floating-label::before,
18834
+ .select-wrapper.has-value .floating-label::before,
18835
+ .select-wrapper.open .floating-label::before {
18836
+ content: "";
18837
+ position: absolute;
18838
+ left: 0;
18839
+ right: 0;
18840
+ top: calc(50% - var(--_rel-sel-ring-w, 0px) - 2px);
18841
+ height: calc(var(--_rel-sel-notch-h, 1px) + 4px);
18842
+ background: var(--rel-select-surface, var(--rel-select-bg, var(--rel-bg-neutral)));
18843
+ z-index: -1;
18844
+ }
18845
+
18846
+ .select-wrapper.focused .floating-label::after,
18847
+ .select-wrapper.has-value .floating-label::after,
18848
+ .select-wrapper.open .floating-label::after {
18849
+ content: "";
18850
+ position: absolute;
18851
+ left: 0;
18852
+ right: 0;
18853
+ top: 50%;
18854
+ height: calc(var(--_rel-sel-border-w, 1px) + 3px);
18855
+ background: var(--rel-select-bg, var(--rel-bg-neutral));
18856
+ z-index: -1;
18857
+ }
18858
+
18859
+ /* Figma swaps the strip's fill on the disabled variant
18860
+ (--rel-bg-neutral-elevated-2), matching the disabled trigger fill at
18861
+ .select-wrapper.disabled .select-trigger. rel-select had no equivalent of
18862
+ rel-textfield's disabled swap at all, so the old chip painted
18863
+ --rel-bg-neutral over a trigger filled --rel-bg-neutral-elevated-2: a
18864
+ visibly lighter block sitting on the field. */
18865
+ .select-wrapper.disabled .floating-label::before {
18866
+ background: var(
18867
+ --rel-select-surface-disabled,
18868
+ var(--rel-select-bg-disabled, var(--rel-bg-neutral-elevated-2))
18869
+ );
18870
+ }
18871
+ .select-wrapper.disabled .floating-label::after {
18872
+ background: var(--rel-select-bg-disabled, var(--rel-bg-neutral-elevated-2));
18144
18873
  }
18145
18874
 
18146
18875
  /* The real placeholder hides while the float-label rests in its spot,
@@ -18196,7 +18925,12 @@ RelSelect.styles = import_lit37.css`
18196
18925
  /* ── Dropdown listbox ── */
18197
18926
  .select-listbox {
18198
18927
  position: fixed;
18199
- z-index: 1001;
18928
+ /* Only consulted on the no-Popover-API fallback path: once the listbox
18929
+ is promoted to the top layer, z-index is inert and stacking is by
18930
+ promotion order. Shared with the pickers so the whole overlay family
18931
+ is overridable from one place instead of disagreeing (they were on
18932
+ 2147483647, which beat consumer app chrome unconditionally). */
18933
+ z-index: var(--rel-overlay-fallback-z, 1001);
18200
18934
  display: none;
18201
18935
  /* Reset native [popover] UA styles (the listbox is promoted to the
18202
18936
  top layer so transformed ancestors — e.g. a centered rel-modal
@@ -18421,6 +19155,26 @@ RelSelect.styles = import_lit37.css`
18421
19155
  white-space: nowrap;
18422
19156
  }
18423
19157
 
19158
+ /* Multi-select rows delegate to a nested rel-checkbox, which wraps its
19159
+ text by default, so the same option truncated to one line in single
19160
+ mode and wrapped to three (or overflowed the panel sideways on an
19161
+ unbroken identifier) under multiple. The checkbox's label and helper
19162
+ are parts, so the row reaches them from here and applies the same
19163
+ one-line ellipsis. Same fix as rel-autocomplete, same date. */
19164
+ .checkbox-option > rel-checkbox {
19165
+ flex: 1;
19166
+ min-width: 0;
19167
+ }
19168
+
19169
+ .checkbox-option > rel-checkbox::part(label),
19170
+ .checkbox-option > rel-checkbox::part(helper) {
19171
+ display: block;
19172
+ min-width: 0;
19173
+ overflow: hidden;
19174
+ text-overflow: ellipsis;
19175
+ white-space: nowrap;
19176
+ }
19177
+
18424
19178
  /* One line, truncated like the label. supportingText used to render only in
18425
19179
  multi-select rows (through rel-checkbox); single-select rows drop it on
18426
19180
  the floor, which made the same option object render differently
@@ -18858,7 +19612,7 @@ RelSkeleton = __decorateClass([
18858
19612
  // src/components/rel-sheet/rel-sheet.ts
18859
19613
  var import_lit39 = require("lit");
18860
19614
  var import_decorators38 = require("lit/decorators.js");
18861
- var import_rel_icon17 = require("@reliability-design/icons/rel-icon");
19615
+ var import_rel_icon18 = require("@reliability-design/icons/rel-icon");
18862
19616
  var SIZE_ALIASES7 = {
18863
19617
  sm: "small",
18864
19618
  md: "medium",
@@ -20272,7 +21026,7 @@ RelSlider = __decorateClass([
20272
21026
  // src/components/rel-snackbar/rel-snackbar.ts
20273
21027
  var import_lit41 = require("lit");
20274
21028
  var import_decorators40 = require("lit/decorators.js");
20275
- var import_rel_icon18 = require("@reliability-design/icons/rel-icon");
21029
+ var import_rel_icon19 = require("@reliability-design/icons/rel-icon");
20276
21030
  var DESTRUCTIVE_ALIASES2 = /* @__PURE__ */ new Set(["error", "danger", "critical"]);
20277
21031
  var RelSnackbar = class extends import_lit41.LitElement {
20278
21032
  constructor() {
@@ -20663,6 +21417,7 @@ var RelStepper = class extends import_lit42.LitElement {
20663
21417
  this.orientation = "horizontal";
20664
21418
  this.size = "large";
20665
21419
  this.type = "default";
21420
+ this.connector = "solid";
20666
21421
  }
20667
21422
  willUpdate(changed) {
20668
21423
  if (changed.has("size") && this.size === "small") {
@@ -20673,6 +21428,14 @@ var RelStepper = class extends import_lit42.LitElement {
20673
21428
  "Figma has no Size axis on the Stepper: the connector geometry is fixed, so size no longer changes anything. Retune the connector with the CSS hook instead."
20674
21429
  );
20675
21430
  }
21431
+ if (changed.has("textAlign") && this.textAlign !== "below" && !this.labelPlacement) {
21432
+ warnDeprecated(
21433
+ "rel-stepper",
21434
+ "textAlign",
21435
+ "label-placement",
21436
+ 'Use label-placement="inverted" for textalign="below" and label-placement="centered" for textalign="right".'
21437
+ );
21438
+ }
20676
21439
  }
20677
21440
  isDone(step) {
20678
21441
  return step.status === "done";
@@ -21055,9 +21818,15 @@ RelStepper.styles = import_lit42.css`
21055
21818
  /********************************************
21056
21819
  * Connector Lines
21057
21820
  ********************************************/
21821
+ /* The line colour is held in a custom property rather than written straight
21822
+ to background, because the dashed variant below needs the SAME colour
21823
+ inside a gradient. With the colour in background the state rules and the
21824
+ dash pattern would each have to win the same declaration, and whichever
21825
+ came last would erase the other. */
21058
21826
  .connector {
21059
21827
  position: absolute;
21060
- background: var(--rel-bg-neutral-elevated-2);
21828
+ --_rel-stepper-line-color: var(--rel-bg-neutral-elevated-2);
21829
+ background: var(--_rel-stepper-line-color);
21061
21830
  z-index: 1;
21062
21831
  }
21063
21832
 
@@ -21069,7 +21838,6 @@ RelStepper.styles = import_lit42.css`
21069
21838
  transform: translateY(-50%);
21070
21839
  height: 1px;
21071
21840
  position: absolute;
21072
- background: var(--rel-bg-neutral-elevated-2);
21073
21841
  }
21074
21842
 
21075
21843
  .rel-stepper.horizontal .rel-step:last-child .connector {
@@ -21104,6 +21872,37 @@ RelStepper.styles = import_lit42.css`
21104
21872
  display: none;
21105
21873
  }
21106
21874
 
21875
+ /* ── connector="dot" (Figma ".Line Types" Type=Dot, set 6437:428) ──
21876
+ Verified against the LIVE Published DS file 2026-09-25: both Dot variants
21877
+ carry RECTANGLE "Line" 1×20 with strokeWeight=1 and strokeDashes=[2,2],
21878
+ i.e. a 1px line, 2px on, 2px off. (The Default variants carry no dashes;
21879
+ the COMPONENT_SET node's own [10,5] is the set frame's border, not a
21880
+ variant value.)
21881
+
21882
+ A repeating gradient rather than a dashed border-style, because the dashed
21883
+ border style is engine-defined — no browser lets you state a 2/2 period,
21884
+ and each one picks its own dash length from the border width. The
21885
+ gradient is exact and stays exact at any device pixel ratio.
21886
+
21887
+ The line thickness still comes from the height/width declared above, so
21888
+ this only replaces the paint. Colour comes through the custom property so
21889
+ done / progress steps dash in their own tone. */
21890
+ :host([connector="dot"]) .rel-stepper.horizontal .connector {
21891
+ background: repeating-linear-gradient(
21892
+ to right,
21893
+ var(--_rel-stepper-line-color) 0 2px,
21894
+ transparent 2px 4px
21895
+ );
21896
+ }
21897
+
21898
+ :host([connector="dot"]) .rel-stepper.vertical .connector {
21899
+ background: repeating-linear-gradient(
21900
+ to bottom,
21901
+ var(--_rel-stepper-line-color) 0 2px,
21902
+ transparent 2px 4px
21903
+ );
21904
+ }
21905
+
21107
21906
  /********************************************
21108
21907
  * Step States
21109
21908
  ********************************************/
@@ -21129,11 +21928,11 @@ RelStepper.styles = import_lit42.css`
21129
21928
  }
21130
21929
 
21131
21930
  .rel-step.done .connector {
21132
- background: var(--rel-border-strong);
21931
+ --_rel-stepper-line-color: var(--rel-border-strong);
21133
21932
  }
21134
21933
 
21135
21934
  .rel-step.progress .connector {
21136
- background: var(--rel-border-strong);
21935
+ --_rel-stepper-line-color: var(--rel-border-strong);
21137
21936
  }
21138
21937
 
21139
21938
  /* Figma "Content": body/md/medium, text default; Inactive at 40% */
@@ -21219,6 +22018,9 @@ __decorateClass([
21219
22018
  __decorateClass([
21220
22019
  (0, import_decorators41.property)({ type: String, reflect: true })
21221
22020
  ], RelStepper.prototype, "type", 2);
22021
+ __decorateClass([
22022
+ (0, import_decorators41.property)({ type: String, reflect: true })
22023
+ ], RelStepper.prototype, "connector", 2);
21222
22024
  __decorateClass([
21223
22025
  (0, import_decorators41.property)({ type: String, reflect: true, attribute: "label-placement" })
21224
22026
  ], RelStepper.prototype, "labelPlacement", 2);
@@ -21701,7 +22503,7 @@ RelSwitch = __decorateClass([
21701
22503
  // src/components/rel-tab/rel-tab.ts
21702
22504
  var import_lit44 = require("lit");
21703
22505
  var import_decorators43 = require("lit/decorators.js");
21704
- var import_rel_icon19 = require("@reliability-design/icons/rel-icon");
22506
+ var import_rel_icon20 = require("@reliability-design/icons/rel-icon");
21705
22507
  var RelTabItem = class extends import_lit44.LitElement {
21706
22508
  constructor() {
21707
22509
  super(...arguments);
@@ -21773,6 +22575,13 @@ var RelTab = class extends import_lit44.LitElement {
21773
22575
  composed: true
21774
22576
  })
21775
22577
  );
22578
+ this.dispatchEvent(
22579
+ new CustomEvent("rel-change", {
22580
+ detail: index,
22581
+ bubbles: true,
22582
+ composed: true
22583
+ })
22584
+ );
21776
22585
  }
21777
22586
  onKeyDown(event, index) {
21778
22587
  if (event.key === "Enter" || event.key === " ") {
@@ -22049,7 +22858,7 @@ RelTab = __decorateClass([
22049
22858
  var import_lit45 = require("lit");
22050
22859
  var import_decorators44 = require("lit/decorators.js");
22051
22860
  var import_live3 = require("lit/directives/live.js");
22052
- var import_rel_icon20 = require("@reliability-design/icons/rel-icon");
22861
+ var import_rel_icon21 = require("@reliability-design/icons/rel-icon");
22053
22862
 
22054
22863
  // src/components/rel-time-picker/time-parse.ts
22055
22864
  function parseTimeInput(text) {
@@ -22154,6 +22963,23 @@ var RelTimePicker = class extends import_lit45.LitElement {
22154
22963
  this.onReposition = () => {
22155
22964
  if (this.open) this.updatePopoverPosition();
22156
22965
  };
22966
+ /* Outside scroll CLOSES the panel; it used to reposition. Same change as
22967
+ * rel-date-picker and rel-range-picker, and the pattern rel-select has used
22968
+ * all along — see the long note in rel-date-picker.onPageScroll for why
22969
+ * repositioning is what produced the "panel floats over the sticky header"
22970
+ * report, and why no z-index can fix it.
22971
+ *
22972
+ * This component is the reason the exemption tests CONTAINMENT rather than
22973
+ * equality with the panel: the hour / minute / period columns are
22974
+ * `overflow-y: auto` in their own right (`.column-list`), so a scroll that
22975
+ * must NOT close the panel very often has one of them as its target, not the
22976
+ * panel. Discards the draft, like every other outside dismissal. */
22977
+ this.onPageScroll = (e) => {
22978
+ if (!this.open) return;
22979
+ const panel = this.popoverElement;
22980
+ if (panel && e.composedPath().includes(panel)) return;
22981
+ this.cancelPopover(false);
22982
+ };
22157
22983
  this.updatePopoverPosition = () => {
22158
22984
  const panel = this.popoverElement;
22159
22985
  const anchor = this.triggerButton;
@@ -22213,7 +23039,7 @@ var RelTimePicker = class extends import_lit45.LitElement {
22213
23039
  document.removeEventListener("click", this.handleClickOutside);
22214
23040
  document.removeEventListener("keydown", this.handleEscapeKey);
22215
23041
  window.removeEventListener("resize", this.onReposition);
22216
- window.removeEventListener("scroll", this.onReposition, true);
23042
+ window.removeEventListener("scroll", this.onPageScroll, true);
22217
23043
  this.hideTopLayer();
22218
23044
  }
22219
23045
  // `open` is a public reflected property (consumers may set it directly,
@@ -22223,7 +23049,7 @@ var RelTimePicker = class extends import_lit45.LitElement {
22223
23049
  if (changedProperties.has("open")) {
22224
23050
  if (!this.open) {
22225
23051
  window.removeEventListener("resize", this.onReposition);
22226
- window.removeEventListener("scroll", this.onReposition, true);
23052
+ window.removeEventListener("scroll", this.onPageScroll, true);
22227
23053
  this.hideTopLayer();
22228
23054
  this.focusPanelOnOpen = true;
22229
23055
  return;
@@ -22231,7 +23057,7 @@ var RelTimePicker = class extends import_lit45.LitElement {
22231
23057
  this.resetDraftFromValue();
22232
23058
  this.showTopLayer();
22233
23059
  window.addEventListener("resize", this.onReposition);
22234
- window.addEventListener("scroll", this.onReposition, true);
23060
+ window.addEventListener("scroll", this.onPageScroll, true);
22235
23061
  const focusPanel = this.focusPanelOnOpen;
22236
23062
  this.focusPanelOnOpen = true;
22237
23063
  setTimeout(() => {
@@ -23208,7 +24034,14 @@ RelTimePicker.styles = import_lit45.css`
23208
24034
  position: absolute;
23209
24035
  top: calc(100% + 8px);
23210
24036
  left: 0;
23211
- z-index: 2147483647;
24037
+ /* Fallback stacking ONLY: while the panel is in the top layer this is
24038
+ inert, because top-layer elements are not in the page stacking order at
24039
+ all and order of promotion decides. It applies solely where the Popover
24040
+ API is unavailable. One overridable hook across every overlay, so a
24041
+ consumer with its own stacking scheme sets it once; the default matches
24042
+ rel-select / rel-autocomplete / rel-popover / rel-menubar. This was
24043
+ 2147483647, which cannot be layered against anything. */
24044
+ z-index: var(--rel-overlay-fallback-z, 1001);
23212
24045
  background: var(--rel-bg-surface);
23213
24046
  border: 1px solid var(--rel-border-subtle);
23214
24047
  border-radius: var(--rel-border-radius-sm);
@@ -23557,7 +24390,7 @@ RelTimePicker = __decorateClass([
23557
24390
  // src/components/rel-toast/rel-toast.ts
23558
24391
  var import_lit46 = require("lit");
23559
24392
  var import_decorators45 = require("lit/decorators.js");
23560
- var import_rel_icon21 = require("@reliability-design/icons/rel-icon");
24393
+ var import_rel_icon22 = require("@reliability-design/icons/rel-icon");
23561
24394
  var SIZE_ALIASES10 = {
23562
24395
  sm: "small",
23563
24396
  md: "medium",
@@ -24350,7 +25183,7 @@ RelTooltip = __decorateClass([
24350
25183
  // src/components/rel-upload/rel-upload.ts
24351
25184
  var import_lit48 = require("lit");
24352
25185
  var import_decorators47 = require("lit/decorators.js");
24353
- var import_rel_icon22 = require("@reliability-design/icons/rel-icon");
25186
+ var import_rel_icon23 = require("@reliability-design/icons/rel-icon");
24354
25187
  var uid = 0;
24355
25188
  var RelUpload = class extends import_lit48.LitElement {
24356
25189
  constructor() {
@@ -24763,7 +25596,11 @@ var RelUpload = class extends import_lit48.LitElement {
24763
25596
  }
24764
25597
  renderRow(f) {
24765
25598
  return import_lit48.html`
24766
- <div class="row ${f.status}">
25599
+ <!-- role="listitem" is required, not decorative: the container at
25600
+ .rows carries role="list", and a list whose children are plain divs
25601
+ fails axe's aria-required-children. It was failing on the uploading,
25602
+ failure, disabled and delayed stories alike. -->
25603
+ <div class="row ${f.status}" role="listitem">
24767
25604
  <rel-icon name="document" size="md"></rel-icon>
24768
25605
 
24769
25606
  <div class="row-content">
@@ -24868,8 +25705,19 @@ RelUpload.styles = import_lit48.css`
24868
25705
  font-family: var(--rel-fonts-font-family);
24869
25706
  }
24870
25707
 
25708
+ /* not-allowed must live on the HOST, which keeps pointer-events: auto.
25709
+ It used to sit on .dropzone, where it was unreachable: .dropzone is
25710
+ inside .body, .body is pointer-events: none when disabled, and an
25711
+ element that is not a hit target never has its cursor consulted —
25712
+ the cursor resolves from the nearest ancestor that IS one. Measured on
25713
+ a disabled upload before the fix: .dropzone computed not-allowed
25714
+ while the user saw the default arrow. getComputedStyle agreed with the
25715
+ CSS and disagreed with the screen, so anyone re-verifying this must
25716
+ HOVER it rather than read the computed value.
25717
+ rel-button hit the same trap and documents the same resolution. */
24871
25718
  :host([disabled]) {
24872
25719
  opacity: 0.6;
25720
+ cursor: not-allowed;
24873
25721
  }
24874
25722
 
24875
25723
  :host([disabled]) .body {
@@ -24882,7 +25730,6 @@ RelUpload.styles = import_lit48.css`
24882
25730
  instead of a faded live one. */
24883
25731
  :host([disabled]) .dropzone {
24884
25732
  background: var(--rel-bg-neutral-elevated-2);
24885
- cursor: not-allowed;
24886
25733
  }
24887
25734
 
24888
25735
  .body {
@@ -25160,7 +26007,7 @@ RelUpload = __decorateClass([
25160
26007
  // src/components/rel-upload/rel-upload-actions.ts
25161
26008
  var import_lit49 = require("lit");
25162
26009
  var import_decorators48 = require("lit/decorators.js");
25163
- var import_rel_icon23 = require("@reliability-design/icons/rel-icon");
26010
+ var import_rel_icon24 = require("@reliability-design/icons/rel-icon");
25164
26011
  var EMPTY_STATE = {
25165
26012
  phase: "empty",
25166
26013
  total: 0,