@vectoriox/iox-ui 5.85.0 → 5.86.1

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.
Files changed (31) hide show
  1. package/actions-core/index.d.ts +30 -2
  2. package/design-core/index.d.ts +1488 -224
  3. package/fesm2022/vectoriox-iox-ui-actions-core.mjs +136 -70
  4. package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
  5. package/fesm2022/vectoriox-iox-ui-design-core.mjs +1751 -279
  6. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  7. package/fesm2022/vectoriox-iox-ui-forms-core.mjs +27 -4
  8. package/fesm2022/vectoriox-iox-ui-forms-core.mjs.map +1 -1
  9. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +223 -12
  10. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  11. package/fesm2022/vectoriox-iox-ui-scene-core.mjs +3 -4
  12. package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
  13. package/fesm2022/vectoriox-iox-ui.mjs +1272 -69
  14. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  15. package/forms-core/index.d.ts +23 -4
  16. package/generation-core/index.d.ts +5 -1
  17. package/index.d.ts +426 -22
  18. package/package.json +1 -1
  19. package/src/actions-core/action.model.ts +1 -1
  20. package/src/actions-core/dialog-opener.ts +9 -3
  21. package/src/actions-core/element-actions.ts +25 -1
  22. package/src/actions-core/popover-opener.ts +50 -0
  23. package/src/actions-core/public-api.ts +1 -0
  24. package/src/design-core/flavor-spec.generated.ts +1750 -277
  25. package/src/forms-core/submit-lifecycle.ts +30 -3
  26. package/src/generation-core/baseline-roles.ts +1 -0
  27. package/src/generation-core/node-baselines.generated.ts +214 -8
  28. package/src/generation-core/pictures.ts +8 -4
  29. package/src/scene-core/scroll-driver.ts +2 -1
  30. package/styles/iox-design.css +216 -14
  31. package/styles/iox-forms.css +24 -2
@@ -24,6 +24,27 @@ export enum FormRenderState {
24
24
  /** The attribute every renderer writes a form's state on (the canvas switcher, both live paths). */
25
25
  export const FORM_STATE_ATTRIBUTE = 'data-iox-state';
26
26
 
27
+ /**
28
+ * What a form shows once it has sent (premium M5, N3) — the Form's "After sending" choice:
29
+ * - `message`: the success in place (M3): the fields leave, the Success block shows, Send reads "Sent" and stays so;
30
+ * - `toast`: the fields STAY, cleared for the next answer, and a toast says the form's Sent words — a newsletter in a
31
+ * footer, a form someone may send again.
32
+ */
33
+ export enum FormAfterSend {
34
+ Message = 'message',
35
+ Toast = 'toast',
36
+ }
37
+
38
+ /** The Form trait holding the choice. */
39
+ export const FORM_AFTER_SEND_TRAIT = 'afterSend';
40
+ /** The attribute every renderer writes it on (only when it is `toast` — `message` is what a form always did). */
41
+ export const FORM_AFTER_SEND_ATTRIBUTE = 'data-iox-after-send';
42
+
43
+ /** The choice a stored value names (`message` unless it says `toast`). */
44
+ export function formAfterSend(value: unknown): FormAfterSend {
45
+ return value === FormAfterSend.Toast ? FormAfterSend.Toast : FormAfterSend.Message;
46
+ }
47
+
27
48
  /** What the form does once the server has answered. */
28
49
  export interface SubmitOutcome {
29
50
  state: FormRenderState;
@@ -32,13 +53,19 @@ export interface SubmitOutcome {
32
53
  /** A success clears the answers, so they cannot be sent twice. */
33
54
  reset: boolean;
34
55
  redirect?: string;
56
+ /** Say it in a toast (the form keeps its fields and goes back to Idle). */
57
+ toast?: boolean;
35
58
  }
36
59
 
37
60
  /**
38
- * The server's answer → what the visitor sees. Only a confirmed success is Success; per-field verdicts
39
- * land on the fields; anything else (a dropped network included) is the designed Error block.
61
+ * The server's answer → what the visitor sees. Only a confirmed success is Success (or, for a form that keeps its
62
+ * fields, Idle again with a toast); per-field verdicts land on the fields; anything else (a dropped network included)
63
+ * is the designed Error block.
40
64
  */
41
- export function submitOutcome(response: SubmitResponse | null | undefined): SubmitOutcome {
65
+ export function submitOutcome(response: SubmitResponse | null | undefined, afterSend: FormAfterSend | string | null = FormAfterSend.Message): SubmitOutcome {
66
+ if (response?.ok && formAfterSend(afterSend) === FormAfterSend.Toast) {
67
+ return { state: FormRenderState.Idle, reset: true, toast: true, ...(response.redirect ? { redirect: response.redirect } : {}) };
68
+ }
42
69
  if (response?.ok) return { state: FormRenderState.Success, reset: true, ...(response.redirect ? { redirect: response.redirect } : {}) };
43
70
  if (response?.errors && Object.keys(response.errors).length) return { state: FormRenderState.Idle, errors: response.errors, reset: false };
44
71
  return { state: FormRenderState.Error, reset: false };
@@ -61,6 +61,7 @@ export const BASELINE_ROLES: Readonly<Record<string, Readonly<Record<string, Bas
61
61
  FormSuccess: { width: Behaviour, color: Look, padding: Look, borderRadius: Look, backgroundColor: Look },
62
62
  FormError: { width: Behaviour, color: Look, padding: Look, borderRadius: Look, backgroundColor: Look },
63
63
  Dialog: { width: Behaviour, padding: Look, borderRadius: Look, backgroundColor: Look, boxShadow: Look },
64
+ Popover: { width: Behaviour, padding: Look, borderRadius: Look, backgroundColor: Look, boxShadow: Look },
64
65
  // CC3: a Video fills its column at 16 / 9 — the shape the poster and the player take.
65
66
  Video: { width: Behaviour, aspectRatio: Behaviour },
66
67
  };
@@ -206,7 +206,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
206
206
  "pageRoute": "",
207
207
  "target": "_self",
208
208
  "type": "button",
209
- "ariaLabel": ""
209
+ "ariaLabel": "",
210
+ "tooltip": "",
211
+ "tooltipPlacement": "above"
210
212
  },
211
213
  "styleProps": {
212
214
  "position": "static",
@@ -302,7 +304,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
302
304
  "target",
303
305
  "type",
304
306
  "__accessibilityLabel",
305
- "ariaLabel"
307
+ "ariaLabel",
308
+ "tooltip",
309
+ "tooltipPlacement"
306
310
  ],
307
311
  "styleTraits": [
308
312
  "position",
@@ -1383,7 +1387,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
1383
1387
  "url": "https://",
1384
1388
  "pageRoute": "",
1385
1389
  "target": "_self",
1386
- "ariaLabel": ""
1390
+ "ariaLabel": "",
1391
+ "tooltip": "",
1392
+ "tooltipPlacement": "above"
1387
1393
  },
1388
1394
  "styleProps": {
1389
1395
  "position": "static",
@@ -1478,7 +1484,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
1478
1484
  "pageRoute",
1479
1485
  "target",
1480
1486
  "__accessibilityLabel",
1481
- "ariaLabel"
1487
+ "ariaLabel",
1488
+ "tooltip",
1489
+ "tooltipPlacement"
1482
1490
  ],
1483
1491
  "styleTraits": [
1484
1492
  "position",
@@ -1583,7 +1591,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
1583
1591
  "pageRoute": "",
1584
1592
  "target": "_self",
1585
1593
  "colorScheme": "default",
1586
- "ariaLabel": ""
1594
+ "ariaLabel": "",
1595
+ "tooltip": "",
1596
+ "tooltipPlacement": "above"
1587
1597
  },
1588
1598
  "styleProps": {
1589
1599
  "position": "static",
@@ -1678,7 +1688,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
1678
1688
  "target",
1679
1689
  "colorScheme",
1680
1690
  "__accessibilityLabel",
1681
- "ariaLabel"
1691
+ "ariaLabel",
1692
+ "tooltip",
1693
+ "tooltipPlacement"
1682
1694
  ],
1683
1695
  "styleTraits": [
1684
1696
  "position",
@@ -3649,7 +3661,7 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3649
3661
  "name": "Contact form",
3650
3662
  "formId": "",
3651
3663
  "feedback": "smart",
3652
- "resetAfterSubmit": false,
3664
+ "afterSend": "message",
3653
3665
  "focusFirstProblem": true,
3654
3666
  "announceFailure": true,
3655
3667
  "honeypot": true,
@@ -3745,7 +3757,7 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3745
3757
  "name",
3746
3758
  "formId",
3747
3759
  "feedback",
3748
- "resetAfterSubmit",
3760
+ "afterSend",
3749
3761
  "focusFirstProblem",
3750
3762
  "announceFailure",
3751
3763
  "__spamLabel",
@@ -5035,6 +5047,200 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
5035
5047
  ],
5036
5048
  "container": true
5037
5049
  },
5050
+ "Popover": {
5051
+ "props": {
5052
+ "placement": "below",
5053
+ "ariaLabel": "",
5054
+ "colorScheme": "default"
5055
+ },
5056
+ "styleProps": {
5057
+ "position": "static",
5058
+ "top": "auto",
5059
+ "right": "auto",
5060
+ "bottom": "auto",
5061
+ "left": "auto",
5062
+ "zIndex": "0",
5063
+ "display": "block",
5064
+ "flexDirection": "row",
5065
+ "flexWrap": "nowrap",
5066
+ "justifyContent": "flex-start",
5067
+ "alignItems": "stretch",
5068
+ "gap": "0px",
5069
+ "gridTemplateColumns": "1fr",
5070
+ "gridTemplateRows": "auto",
5071
+ "justifyItems": "stretch",
5072
+ "alignContent": "stretch",
5073
+ "alignSelf": "auto",
5074
+ "order": "0",
5075
+ "flexGrow": "0",
5076
+ "flexShrink": "1",
5077
+ "flexBasis": "auto",
5078
+ "justifySelf": "auto",
5079
+ "gridColumn": "auto",
5080
+ "gridRow": "auto",
5081
+ "width": "320px",
5082
+ "height": "auto",
5083
+ "minWidth": "auto",
5084
+ "minHeight": "auto",
5085
+ "maxWidth": "auto",
5086
+ "maxHeight": "auto",
5087
+ "aspectRatio": "auto",
5088
+ "objectFit": "cover",
5089
+ "overflow": "visible",
5090
+ "fontFamily": "Poppins, sans-serif",
5091
+ "fontSize": "16px",
5092
+ "fontWeight": "400",
5093
+ "color": "#000000",
5094
+ "lineHeight": "1.5",
5095
+ "letterSpacing": "0px",
5096
+ "textTransform": "none",
5097
+ "textAlign": "start",
5098
+ "fontStyle": "normal",
5099
+ "textDecoration": "none",
5100
+ "fontVariantNumeric": "normal",
5101
+ "WebkitTextStrokeWidth": "0px",
5102
+ "WebkitTextStrokeColor": "transparent",
5103
+ "margin": "0px 0px 0px 0px",
5104
+ "padding": "16px",
5105
+ "borderWidth": "0px",
5106
+ "borderStyle": "solid",
5107
+ "borderColor": "#CCCCCC",
5108
+ "borderRadius": "12px",
5109
+ "backgroundColor": "#ffffff",
5110
+ "opacity": "1",
5111
+ "mixBlendMode": "normal",
5112
+ "cursor": "auto",
5113
+ "boxShadow": "0 12px 32px rgba(0, 0, 0, 0.18)",
5114
+ "textShadow": "",
5115
+ "translateX": "0px",
5116
+ "translateY": "0px",
5117
+ "scaleX": "1",
5118
+ "scaleY": "1",
5119
+ "rotate": "0deg",
5120
+ "skewX": "0deg",
5121
+ "skewY": "0deg",
5122
+ "transformOrigin": "center",
5123
+ "filterBlur": "0px",
5124
+ "filterHueRotate": "0deg",
5125
+ "filterBrightness": "1",
5126
+ "filterContrast": "1",
5127
+ "filterSaturate": "1",
5128
+ "filterGrayscale": "0",
5129
+ "filterSepia": "0",
5130
+ "filterInvert": "0",
5131
+ "backdropFilter": "none",
5132
+ "backdropBlur": "0px",
5133
+ "backdropBrightness": "1",
5134
+ "backdropContrast": "1",
5135
+ "backdropSaturate": "1",
5136
+ "clipPath": "none",
5137
+ "transitionProperty": "",
5138
+ "transitionDuration": "0ms",
5139
+ "transitionDelay": "0ms",
5140
+ "transitionTimingFunction": "ease"
5141
+ },
5142
+ "traits": [
5143
+ "placement",
5144
+ "__accessibilityLabel",
5145
+ "ariaLabel",
5146
+ "colorScheme"
5147
+ ],
5148
+ "styleTraits": [
5149
+ "position",
5150
+ "top",
5151
+ "right",
5152
+ "bottom",
5153
+ "left",
5154
+ "zIndex",
5155
+ "display",
5156
+ "flexDirection",
5157
+ "flexWrap",
5158
+ "justifyContent",
5159
+ "alignItems",
5160
+ "gap",
5161
+ "gridTemplateColumns",
5162
+ "gridTemplateRows",
5163
+ "justifyItems",
5164
+ "alignContent",
5165
+ "textDirection",
5166
+ "alignSelf",
5167
+ "order",
5168
+ "flexGrow",
5169
+ "flexShrink",
5170
+ "flexBasis",
5171
+ "justifySelf",
5172
+ "gridColumn",
5173
+ "gridRow",
5174
+ "width",
5175
+ "height",
5176
+ "minWidth",
5177
+ "minHeight",
5178
+ "maxWidth",
5179
+ "maxHeight",
5180
+ "aspectRatio",
5181
+ "objectFit",
5182
+ "overflow",
5183
+ "fontFamily",
5184
+ "fontSize",
5185
+ "fontWeight",
5186
+ "color",
5187
+ "lineHeight",
5188
+ "letterSpacing",
5189
+ "textTransform",
5190
+ "textAlign",
5191
+ "fontStyle",
5192
+ "textDecoration",
5193
+ "fontVariantNumeric",
5194
+ "_sep_stroke",
5195
+ "WebkitTextStrokeWidth",
5196
+ "WebkitTextStrokeColor",
5197
+ "margin",
5198
+ "padding",
5199
+ "borderWidth",
5200
+ "borderStyle",
5201
+ "borderColor",
5202
+ "borderRadius",
5203
+ "backgroundColor",
5204
+ "opacity",
5205
+ "mixBlendMode",
5206
+ "cursor",
5207
+ "_sep_shadow",
5208
+ "boxShadow",
5209
+ "textShadow",
5210
+ "_sep_transform",
5211
+ "translateX",
5212
+ "translateY",
5213
+ "scaleX",
5214
+ "scaleY",
5215
+ "rotate",
5216
+ "skewX",
5217
+ "skewY",
5218
+ "transformOrigin",
5219
+ "_sep_filter",
5220
+ "filterBlur",
5221
+ "filterHueRotate",
5222
+ "filterBrightness",
5223
+ "filterContrast",
5224
+ "filterSaturate",
5225
+ "filterGrayscale",
5226
+ "filterSepia",
5227
+ "filterInvert",
5228
+ "_sep_backdrop",
5229
+ "backdropFilter",
5230
+ "backdropBlur",
5231
+ "backdropBrightness",
5232
+ "backdropContrast",
5233
+ "backdropSaturate",
5234
+ "_sep_clip",
5235
+ "clipPath",
5236
+ "_sep_transition",
5237
+ "transitionProperty",
5238
+ "transitionDuration",
5239
+ "transitionDelay",
5240
+ "transitionTimingFunction"
5241
+ ],
5242
+ "container": true
5243
+ },
5038
5244
  "Video": {
5039
5245
  "props": {
5040
5246
  "videoSource": "youtube",
@@ -351,14 +351,18 @@ export const SCRIM_LEVELS: ReadonlyArray<ScrimLevel> = [ScrimLevel.Soft, ScrimLe
351
351
  /** What each level writes on the scrim node (`backgroundColor`, token references only) — `null`: the style's own. */
352
352
  export const SCRIM_BACKGROUND: Readonly<Record<ScrimLevel, string | null>> = {
353
353
  [ScrimLevel.Soft]: null,
354
- [ScrimLevel.Firm]: 'linear-gradient(180deg, transparent 0%, var(--iox-media-scrim) 18%)',
354
+ [ScrimLevel.Firm]: 'linear-gradient(180deg, color-mix(in srgb, var(--iox-media-scrim) 75%, transparent) 0%, var(--iox-media-scrim) 35%)',
355
355
  [ScrimLevel.Full]: 'var(--iox-media-scrim)',
356
356
  };
357
357
 
358
- /** The least share of the scrim's own strength under the first line of words, at each level (the gradient's ramp). */
358
+ /**
359
+ * The least share of the scrim's own strength under the FIRST line of words, at each level — read from where the words
360
+ * start in the box (its top padding) against the gradient's ramp, on a phone (the tallest box): the element style's own
361
+ * ramp (`transparent 0%` → full at 45%) has only about a quarter of its strength there; Firm starts at three quarters.
362
+ */
359
363
  export const SCRIM_COVER: Readonly<Record<ScrimLevel, number>> = {
360
- [ScrimLevel.Soft]: 0.5,
361
- [ScrimLevel.Firm]: 0.85,
364
+ [ScrimLevel.Soft]: 0.25,
365
+ [ScrimLevel.Firm]: 0.75,
362
366
  [ScrimLevel.Full]: 1,
363
367
  };
364
368
 
@@ -131,8 +131,9 @@ export class ScrollDriver {
131
131
  private lastTime = 0;
132
132
  private readonly wakeUp = (): void => this.wake();
133
133
  private readonly onWakeEvent = (e: Event): void => {
134
+ // What arrived may be the stage itself (a played slider marks its stage when it boots): read the root again.
134
135
  const scene = [...this.scenes.values()].find(s => s.root.contains(e.target as Node));
135
- if (scene) { scene.fresh = true; this.wake(); }
136
+ if (scene) this.readScene(scene);
136
137
  };
137
138
 
138
139
  constructor(private readonly options: ScrollDriverOptions = {}) {
@@ -142,7 +142,7 @@ iox-gallery.iox-es-media[data-iox-empty] > .iox-gallery {
142
142
  --iox-image-hover-scale: 1;
143
143
  }
144
144
  .iox-es-card .iox-es-media {
145
- transition: transform var(--iox-dur-ui) var(--iox-ease-standard), scale var(--iox-dur-ui) var(--iox-ease-standard);
145
+ transition: transform var(--iox-dur-ui, 200ms) var(--iox-ease-standard), scale var(--iox-dur-ui, 200ms) var(--iox-ease-standard);
146
146
  }
147
147
  @media (hover: hover) and (pointer: fine) {
148
148
  :is(a.iox-es-card, .iox-es-card:has(> a.linked-container-root), .iox-es-card:has(.iox-card-link)):hover .iox-es-media {
@@ -157,7 +157,7 @@ iox-gallery.iox-es-media[data-iox-empty] > .iox-gallery {
157
157
  forces its own), so the pseudo sits under the card's content and its shadow outside the card's edge. */
158
158
  :is(a.iox-es-card, .iox-es-card:has(> a.linked-container-root), .iox-es-card:has(.iox-card-link)) {
159
159
  --iox-card-shadow-hover: var(--iox-card-shadow);
160
- --iox-dur-base: var(--iox-dur-ui);
160
+ --iox-dur-base: var(--iox-dur-ui, 200ms);
161
161
  translate: 0px;
162
162
  }
163
163
  :is(a.iox-es-card, .iox-es-card:has(> a.linked-container-root), .iox-es-card:has(.iox-card-link))::before {
@@ -165,11 +165,11 @@ iox-gallery.iox-es-media[data-iox-empty] > .iox-gallery {
165
165
  position: absolute;
166
166
  inset: calc(var(--iox-card-border-width, 0px) * -1);
167
167
  border-radius: inherit;
168
- box-shadow: var(--iox-card-shadow-raise);
168
+ box-shadow: var(--iox-card-shadow-raise, var(--iox-shadow-md, none));
169
169
  opacity: 0;
170
170
  pointer-events: none;
171
171
  z-index: -1;
172
- transition: opacity var(--iox-dur-ui) var(--iox-ease-standard);
172
+ transition: opacity var(--iox-dur-ui, 200ms) var(--iox-ease-standard);
173
173
  }
174
174
  @media (hover: hover) and (pointer: fine) {
175
175
  :is(a.iox-es-card, .iox-es-card:has(> a.linked-container-root), .iox-es-card:has(.iox-card-link)):hover::before {
@@ -495,12 +495,15 @@ span.iox-video__play { pointer-events: none; }
495
495
  display: inline-grid; place-items: center; width: 24px; height: 24px; margin: 0; padding: 0; border: 0; border-radius: var(--iox-radius-pill, 999px);
496
496
  background: none; opacity: 1; cursor: pointer; color: inherit;
497
497
  }
498
+ /* The dot grows into the pill by its CLIP (premium M4 — Emil 41: never an animated width): the pill is always 26px wide,
499
+ clipped to an 8px dot at rest and opened whole when current — a compositor property, the dot's round ends kept. */
498
500
  .iox-role-slider-pagination .swiper-pagination-bullet::after {
499
- content: ''; display: block; width: 8px; height: 8px; border-radius: var(--iox-radius-pill, 999px); background: var(--iox-line-strong);
500
- transition: width var(--iox-dur-fast) var(--iox-ease-standard);
501
+ content: ''; display: block; width: 26px; height: 8px; border-radius: var(--iox-radius-pill, 999px); background: var(--iox-line-strong);
502
+ clip-path: inset(0 9px round var(--iox-radius-pill, 999px));
503
+ transition: clip-path var(--iox-dur-fast) var(--iox-ease-standard), background-color var(--iox-dur-fast) var(--iox-ease-standard);
501
504
  }
502
505
  .iox-role-slider-pagination .swiper-pagination-bullet-active { width: 34px; }
503
- .iox-role-slider-pagination .swiper-pagination-bullet-active::after { width: 26px; background: currentColor; }
506
+ .iox-role-slider-pagination .swiper-pagination-bullet-active::after { clip-path: inset(0 0 round var(--iox-radius-pill, 999px)); background: currentColor; }
504
507
  .iox-role-slider-pagination .swiper-pagination-bullet:focus-visible { outline: none; box-shadow: var(--iox-focus-ring); }
505
508
 
506
509
  /* ── The date picker's sheet (premium components N8) ────────────────────────────────────────────────
@@ -545,7 +548,7 @@ span.iox-video__play { pointer-events: none; }
545
548
  block-size: 16px;
546
549
  pointer-events: none;
547
550
  opacity: 0;
548
- transition: opacity var(--iox-dur-ui) var(--iox-ease-standard);
551
+ transition: opacity var(--iox-dur-ui, 200ms) var(--iox-ease-standard);
549
552
  }
550
553
  [data-iox-header][data-iox-header-edge='fade']::after {
551
554
  background: linear-gradient(to bottom,
@@ -595,8 +598,8 @@ span.iox-video__play { pointer-events: none; }
595
598
  .iox-es-link {
596
599
  --iox-link-decoration: none;
597
600
  --iox-link-current-decoration: none;
598
- --iox-dur-fast: var(--iox-dur-hover);
599
- --iox-ease-standard: var(--iox-ease-hover);
601
+ --iox-dur-fast: var(--iox-dur-hover, 150ms);
602
+ --iox-ease-standard: var(--iox-ease-hover, ease);
600
603
  background-image: linear-gradient(currentColor, currentColor);
601
604
  background-repeat: no-repeat;
602
605
  background-position: 0% calc(100% - var(--iox-link-underline-offset, 0px));
@@ -627,14 +630,14 @@ span.iox-video__play { pointer-events: none; }
627
630
  }
628
631
  /* A chip's colour moves on the hover timing too (Emil 92 — M3's review). */
629
632
  .iox-es-chip {
630
- --iox-dur-fast: var(--iox-dur-hover);
631
- --iox-ease-standard: var(--iox-ease-hover);
633
+ --iox-dur-fast: var(--iox-dur-hover, 150ms);
634
+ --iox-ease-standard: var(--iox-ease-hover, ease);
632
635
  }
633
636
 
634
637
  /* ── Premium components M4 (Emil 55/85): a press lands faster than its release ──────────────────────────────────────
635
638
  While pressed, every pressable's transition runs on dur-press; it releases on its own (slower) dur-fast. */
636
639
  :is(.iox-es-button-primary, .iox-es-button-secondary, .iox-es-button-ghost, .iox-es-bottom-bar-item):active {
637
- transition-duration: var(--iox-dur-press);
640
+ transition-duration: var(--iox-dur-press, 100ms);
638
641
  }
639
642
 
640
643
  /* ── Premium components M4 (T3): the Slider's feel, its bar and the slider the scroll plays ─────────────────────────
@@ -664,7 +667,7 @@ span.iox-video__play { pointer-events: none; }
664
667
  }
665
668
  /* The caption follows the slide in view: it changes behind a short fade (the runtime holds it at 0 for a frame). */
666
669
  .iox-role-slider-caption {
667
- transition: opacity var(--iox-dur-hover) var(--iox-ease-hover);
670
+ transition: opacity var(--iox-dur-hover, 150ms) var(--iox-ease-hover, ease);
668
671
  }
669
672
  .iox-role-slider-caption[data-iox-swapping] {
670
673
  opacity: 0;
@@ -705,3 +708,202 @@ span.iox-video__play { pointer-events: none; }
705
708
  }
706
709
  }
707
710
 
711
+ /* ── The floating family (premium components M5 — N3 toast · N4 tooltip and popover) ──────────────────────────────
712
+ ONE motion grammar for all three: they come FROM where they belong — a tooltip and a popover grow from their trigger
713
+ (the transform-origin placeFloating writes, Emil 47), a toast slides from its edge and back to it (Emil 129–130) —
714
+ and leave faster than they came (Emil 30). A neighbour tooltip, a keyboard open and Esc have no motion at all
715
+ (`data-iox-instant`, Emil 2/125). On a page that wears the design system the look's tokens time them (a look whose
716
+ motion is Off: 0ms); under reduced motion they keep the fade and drop the movement (Emil 192). The element styles'
717
+ colours and type are the contract's floating baseline, verbatim (`.iox-es-tooltip` / `.iox-es-toast`, pinned by the
718
+ token-names spec); a legacy page keeps the neutral literals of the unmarked rules. */
719
+
720
+ /* The layer they are placed in: the viewport on the site (a top-layer popover, so a fixed header, a slider's clip or a
721
+ nav's backdrop-filter cannot hold them), the page frame on the canvas (fixed inside the zoomed frame). */
722
+ .iox-float-layer {
723
+ position: fixed;
724
+ inset: 0;
725
+ width: auto;
726
+ height: auto;
727
+ max-width: none;
728
+ max-height: none;
729
+ margin: 0;
730
+ padding: 0;
731
+ border: 0;
732
+ background: transparent;
733
+ color: inherit;
734
+ overflow: visible;
735
+ pointer-events: none;
736
+ z-index: 60;
737
+ }
738
+ .iox-float-layer[popover]:not(:popover-open) { display: none; }
739
+
740
+ /* The tooltip. */
741
+ .iox-tooltip {
742
+ --tip-scale: 0.97;
743
+ position: absolute;
744
+ top: 0;
745
+ left: 0;
746
+ box-sizing: border-box;
747
+ max-width: min(280px, calc(100% - 24px));
748
+ padding: 6px 10px;
749
+ font: 500 13px/1.35 system-ui, -apple-system, 'Segoe UI', sans-serif;
750
+ background-color: #1f2328;
751
+ color: #ffffff;
752
+ border-radius: 6px;
753
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
754
+ overflow-wrap: anywhere;
755
+ text-align: start;
756
+ pointer-events: auto;
757
+ transition: opacity 125ms cubic-bezier(0.23, 1, 0.32, 1), scale 125ms cubic-bezier(0.23, 1, 0.32, 1);
758
+ }
759
+ .iox-tooltip[hidden] { display: none; }
760
+ @starting-style { .iox-tooltip:not([hidden]) { opacity: 0; scale: var(--tip-scale); } }
761
+ .iox-tooltip[data-iox-closing] { opacity: 0; scale: var(--tip-scale); transition-duration: 80ms; }
762
+ .iox-es-tooltip {
763
+ background-color: var(--iox-tooltip-bg);
764
+ color: var(--iox-tooltip-ink);
765
+ border-radius: var(--iox-tooltip-radius);
766
+ box-shadow: var(--iox-tooltip-shadow);
767
+ font-family: var(--iox-font-body);
768
+ font-size: var(--iox-tooltip-size);
769
+ font-weight: var(--iox-weight-strong);
770
+ }
771
+ .iox-tooltip.iox-es-tooltip {
772
+ --tip-scale: var(--iox-tooltip-scale);
773
+ transition: opacity var(--iox-tooltip-dur-in) var(--iox-tooltip-ease), scale var(--iox-tooltip-dur-in) var(--iox-tooltip-ease);
774
+ }
775
+ .iox-tooltip.iox-es-tooltip[data-iox-closing] { transition-duration: var(--iox-tooltip-dur-out); }
776
+ .iox-tooltip[data-iox-instant],
777
+ .iox-tooltip.iox-es-tooltip[data-iox-instant] { transition: none; }
778
+
779
+ /* The toast stack, at its edge — above the home indicator (M7) and above a bottom tab bar (CC1's reserve). */
780
+ .iox-toasts {
781
+ position: absolute;
782
+ inset-inline: 0;
783
+ display: flex;
784
+ flex-direction: column;
785
+ align-items: center;
786
+ gap: 8px;
787
+ padding-inline: 16px;
788
+ box-sizing: border-box;
789
+ pointer-events: none;
790
+ }
791
+ .iox-toasts[data-iox-edge='bottom'] { bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
792
+ .iox-toasts[data-iox-edge='top'] { top: calc(16px + env(safe-area-inset-top, 0px)); flex-direction: column-reverse; }
793
+ html:has(.iox-bar-reserve) .iox-toasts[data-iox-edge='bottom'] { bottom: calc(16px + var(--iox-tabbar-height, 64px) + env(safe-area-inset-bottom, 0px)); }
794
+ .iox-toast {
795
+ --toast-from: translateY(100%);
796
+ pointer-events: auto;
797
+ box-sizing: border-box;
798
+ display: flex;
799
+ align-items: center;
800
+ gap: 8px;
801
+ max-width: min(440px, 100%);
802
+ padding-block: 10px;
803
+ padding-inline: 16px 6px;
804
+ font: 500 15px/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif;
805
+ background-color: #1f2328;
806
+ color: #ffffff;
807
+ border-radius: 10px;
808
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
809
+ transition: transform 400ms ease, opacity 400ms ease;
810
+ }
811
+ .iox-toasts[data-iox-edge='top'] .iox-toast { --toast-from: translateY(-100%); }
812
+ @starting-style { .iox-toast { opacity: 0; transform: var(--toast-from); } }
813
+ .iox-toast[data-iox-closing] { opacity: 0; transform: var(--toast-from); transition-duration: 220ms; }
814
+ .iox-es-toast {
815
+ background-color: var(--iox-toast-bg);
816
+ color: var(--iox-toast-ink);
817
+ border-radius: var(--iox-toast-radius);
818
+ box-shadow: var(--iox-toast-shadow);
819
+ font-family: var(--iox-font-body);
820
+ font-size: var(--iox-size-body);
821
+ font-weight: var(--iox-weight-strong);
822
+ }
823
+ .iox-toast.iox-es-toast { transition: transform var(--iox-toast-dur-in) var(--iox-toast-ease), opacity var(--iox-toast-dur-in) var(--iox-toast-ease); }
824
+ .iox-toast.iox-es-toast[data-iox-closing] { transition-duration: var(--iox-toast-dur-out); }
825
+ .iox-toast__text { margin: 0; flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
826
+ /* The close control: a 44px target (M2) drawn in the toast's own ink, whatever the look — its ring too (currentColor
827
+ on the toast's fill is the pair the toast already clears). */
828
+ .iox-toast__close {
829
+ flex: none;
830
+ display: inline-grid;
831
+ place-items: center;
832
+ inline-size: 44px;
833
+ block-size: 44px;
834
+ margin-block: -8px;
835
+ padding: 0;
836
+ border: 0;
837
+ border-radius: 999px;
838
+ background: transparent;
839
+ color: inherit;
840
+ cursor: pointer;
841
+ opacity: 0.8;
842
+ touch-action: manipulation;
843
+ transition: opacity 150ms ease-out, background-color 150ms ease-out;
844
+ }
845
+ .iox-toast__close:focus-visible { outline: 2px solid currentColor; outline-offset: -4px; opacity: 1; }
846
+ @media (hover: hover) and (pointer: fine) {
847
+ .iox-toast__close:hover { opacity: 1; background-color: color-mix(in srgb, currentColor 14%, transparent); }
848
+ }
849
+
850
+ /* The popover — the node itself, in the top layer beside its opener (placeFloating writes top / left / the origin).
851
+ Closed it is the browser's hidden popover; the panel takes focus on open and draws no ring (its controls do). */
852
+ [data-iox-popover] { box-sizing: border-box; }
853
+ [data-iox-popover][popover] {
854
+ --pop-scale: 0.95;
855
+ position: fixed;
856
+ inset: auto;
857
+ margin: 0;
858
+ max-width: calc(100vw - 24px);
859
+ max-height: calc(100dvh - 24px);
860
+ overflow: auto;
861
+ overscroll-behavior: contain;
862
+ outline: none;
863
+ transition: opacity 200ms cubic-bezier(0.23, 1, 0.32, 1), scale 200ms cubic-bezier(0.23, 1, 0.32, 1), overlay 200ms allow-discrete, display 200ms allow-discrete;
864
+ }
865
+ @starting-style { [data-iox-popover][popover]:popover-open { opacity: 0; scale: var(--pop-scale); } }
866
+ [data-iox-popover][popover][data-iox-closing] { opacity: 0; scale: var(--pop-scale); transition-duration: 140ms; }
867
+ .iox-es-popover[data-iox-popover][popover] {
868
+ --pop-scale: var(--iox-popover-scale);
869
+ transition: opacity var(--iox-popover-dur-in) var(--iox-popover-ease), scale var(--iox-popover-dur-in) var(--iox-popover-ease),
870
+ overlay var(--iox-popover-dur-in) allow-discrete, display var(--iox-popover-dur-in) allow-discrete;
871
+ }
872
+ .iox-es-popover[data-iox-popover][popover][data-iox-closing] { transition-duration: var(--iox-popover-dur-out); }
873
+ /* On a phone: a bottom sheet over the scrim, the safe area kept (the date picker's decision, FLOATING_SHEET_QUERY). */
874
+ [data-iox-popover][popover][data-iox-sheet] {
875
+ inset: auto 0 0 0;
876
+ width: 100%;
877
+ max-width: none;
878
+ max-height: 85dvh;
879
+ border-end-start-radius: 0;
880
+ border-end-end-radius: 0;
881
+ padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
882
+ transition: translate 500ms cubic-bezier(0.32, 0.72, 0, 1), overlay 500ms allow-discrete, display 500ms allow-discrete;
883
+ }
884
+ @starting-style { [data-iox-popover][popover][data-iox-sheet]:popover-open { opacity: 1; scale: 1; translate: 0 100%; } }
885
+ [data-iox-popover][popover][data-iox-sheet][data-iox-closing] { opacity: 1; scale: 1; translate: 0 100%; transition-duration: 280ms; }
886
+ [data-iox-popover][popover][data-iox-sheet]::backdrop { background: rgba(0, 0, 0, 0.45); transition: opacity 500ms cubic-bezier(0.32, 0.72, 0, 1); }
887
+ @starting-style { [data-iox-popover][popover][data-iox-sheet]:popover-open::backdrop { opacity: 0; } }
888
+ [data-iox-popover][popover][data-iox-sheet][data-iox-closing]::backdrop { opacity: 0; transition-duration: 280ms; }
889
+ .iox-es-popover[data-iox-popover][popover][data-iox-sheet] {
890
+ padding-bottom: calc(var(--iox-popover-pad) + env(safe-area-inset-bottom, 0px));
891
+ transition: translate var(--iox-sheet-dur-in) var(--iox-sheet-ease), overlay var(--iox-sheet-dur-in) allow-discrete, display var(--iox-sheet-dur-in) allow-discrete;
892
+ }
893
+ .iox-es-popover[data-iox-popover][popover][data-iox-sheet][data-iox-closing] { transition-duration: var(--iox-sheet-dur-out); }
894
+ .iox-es-popover[data-iox-popover][popover][data-iox-sheet]::backdrop { background: var(--iox-scrim); transition: opacity var(--iox-sheet-dur-in) var(--iox-sheet-ease); }
895
+ .iox-es-popover[data-iox-popover][popover][data-iox-sheet][data-iox-closing]::backdrop { transition-duration: var(--iox-sheet-dur-out); }
896
+ .iox-es-popover[data-iox-popover][popover][data-iox-instant],
897
+ .iox-es-popover[data-iox-popover][popover][data-iox-instant]::backdrop,
898
+ [data-iox-popover][popover][data-iox-instant],
899
+ [data-iox-popover][popover][data-iox-instant]::backdrop { transition: none; }
900
+
901
+ /* Reduced motion: each of them fades — gentler, not zero; nothing scales or slides. */
902
+ @media (prefers-reduced-motion: reduce) {
903
+ .iox-tooltip.iox-tooltip,
904
+ [data-iox-popover][popover][popover] { --tip-scale: 1; --pop-scale: 1; }
905
+ .iox-toasts[data-iox-edge] .iox-toast.iox-toast { --toast-from: none; }
906
+ [data-iox-popover][popover][data-iox-sheet][data-iox-sheet] { transition-property: opacity, overlay, display; }
907
+ @starting-style { [data-iox-popover][popover][data-iox-sheet][data-iox-sheet]:popover-open { opacity: 0; translate: none; } }
908
+ [data-iox-popover][popover][data-iox-sheet][data-iox-sheet][data-iox-closing] { opacity: 0; translate: none; }
909
+ }