@cahyo-dimas/freeday 2.1.0 → 3.0.0

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 (53) hide show
  1. package/CHANGELOG.md +244 -0
  2. package/COMPONENTS.md +191 -18
  3. package/README.id.md +1 -1
  4. package/README.md +1 -1
  5. package/USAGE.md +1 -1
  6. package/adapters/blazor/FdyAppShell.razor +1 -1
  7. package/adapters/blazor/FdyAppShell.razor.cs +35 -0
  8. package/adapters/blazor/FdyAutocomplete.razor +4 -1
  9. package/adapters/blazor/FdyAutocomplete.razor.cs +26 -0
  10. package/adapters/blazor/FdyCascade.razor +6 -1
  11. package/adapters/blazor/FdyCascade.razor.cs +24 -0
  12. package/adapters/blazor/FdyCombo.razor +1 -0
  13. package/adapters/blazor/FdyCombo.razor.cs +13 -0
  14. package/adapters/blazor/FdyDatepicker.razor +15 -1
  15. package/adapters/blazor/FdyDatepicker.razor.cs +46 -0
  16. package/adapters/blazor/FdyTable.razor +44 -4
  17. package/adapters/blazor/FdyTable.razor.cs +110 -0
  18. package/adapters/blazor/freeday-blazor.js +8 -0
  19. package/adapters/react/components/FdyAppShell.tsx +52 -11
  20. package/adapters/react/components/FdyDrawer.tsx +3 -1
  21. package/adapters/react/components/FdyModal.tsx +3 -1
  22. package/adapters/react/components/FdyTable.tsx +123 -2
  23. package/adapters/vue/components/FdyAppShell.vue +41 -11
  24. package/adapters/vue/components/FdyDrawer.vue +4 -1
  25. package/adapters/vue/components/FdyModal.vue +4 -1
  26. package/adapters/vue/components/FdyTable.vue +125 -4
  27. package/dist/freeday-app-shell.js +23 -5
  28. package/dist/freeday-autocomplete.js +17 -1
  29. package/dist/freeday-busy.js +168 -0
  30. package/dist/freeday-cascade.js +53 -3
  31. package/dist/freeday-chart.js +33 -3
  32. package/dist/freeday-datepicker.js +109 -17
  33. package/dist/freeday-select.js +18 -1
  34. package/dist/freeday-stepper.js +54 -4
  35. package/dist/freeday-table.js +4 -2
  36. package/dist/freeday-timepicker.js +19 -1
  37. package/dist/freeday.bundle.css +616 -39
  38. package/dist/freeday.css +93 -11
  39. package/dist/freeday.js +499 -37
  40. package/dist/freeday.tokens.css +523 -28
  41. package/docs/agent-onboarding.md +4 -0
  42. package/docs/getting-started.md +1 -1
  43. package/package.json +4 -3
  44. package/src/components/app-shell.css +29 -5
  45. package/src/components/appbar.css +2 -2
  46. package/src/components/busy.css +33 -0
  47. package/src/components/card.css +1 -1
  48. package/src/components/drawer.css +1 -1
  49. package/src/components/menu.css +1 -1
  50. package/src/components/modal.css +1 -1
  51. package/src/components/stepper.css +11 -0
  52. package/src/components/table.css +13 -0
  53. package/tokens/tokens.json +54 -10
package/dist/freeday.css CHANGED
@@ -85,7 +85,7 @@ a { color: var(--color-primary); }
85
85
  * > (.fdy-app__topbar, .fdy-app__main)). The __content wrapper gives the sticky
86
86
  * topbar a tall containing block so it can travel, a bare grid cell can't. */
87
87
  .fdy-app{display:flex;align-items:stretch;min-height:100vh;background:var(--color-surface-2);}
88
- .fdy-app__sidebar{position:sticky;top:0;align-self:flex-start;height:100vh;overflow:hidden;flex:none;width:15.5rem;box-sizing:border-box;background:var(--color-surface);border-right:var(--bw) solid var(--color-border);display:flex;flex-direction:column;transition:width var(--dur-slow) var(--ease-standard),border-right-width var(--dur-slow) var(--ease-standard);}
88
+ .fdy-app__sidebar{position:sticky;top:0;align-self:flex-start;height:100vh;overflow:hidden;flex:none;width:15.5rem;box-sizing:border-box;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border-right:var(--bw) solid var(--color-border);display:flex;flex-direction:column;transition:width var(--dur-slow) var(--ease-standard),border-right-width var(--dur-slow) var(--ease-standard);}
89
89
  .fdy-app__content{flex:1;min-width:0;display:flex;flex-direction:column;}
90
90
  /* Brand is a fixed header, same height as the topbar so they align, and only
91
91
  * the nav below it scrolls. Bare text (no children) renders as one bold
@@ -158,7 +158,23 @@ a { color: var(--color-primary); }
158
158
  /* Desktop collapse: animate width -> 0 (content clips via overflow:hidden; the content area
159
159
  * flexes in to fill). Not display:none, so the collapse/expand slides instead of snapping. */
160
160
  @media (min-width:721px){.fdy-app--nav-collapsed .fdy-app__sidebar{width:0;border-right-width:0;}}
161
- .fdy-app__topbar{position:sticky;top:0;z-index:20;min-height:var(--space-16);display:flex;align-items:center;gap:var(--space-3);padding:0 var(--space-8);background:var(--color-surface);border-bottom:var(--bw) solid var(--color-border);}
161
+ /* Desktop OVERLAY: the nav floats over the content instead of displacing it, the arrangement a
162
+ * "menu mode" preference offers on a wide screen. The declarations below are the mobile off-canvas
163
+ * ones repeated, and that repetition is not an oversight: the two conditions are
164
+ * `(max-width:720px)` and `(min-width:721px) and .fdy-app--nav-overlay`, which no single selector
165
+ * can express, and the mobile path has to keep working with no JS and no class at all. What the
166
+ * modifier changes is the LAYOUT; every behaviour (backdrop, Escape, close-on-follow, focus return,
167
+ * inert, the Tab trap) is the same code path the drawer already used. */
168
+ @media (min-width:721px){
169
+ .fdy-app--nav-overlay .fdy-app__sidebar{position:fixed;top:0;left:0;bottom:0;height:100dvh;width:16rem;z-index:60;transform:translateX(-100%);box-shadow:var(--shadow-4);transition:transform var(--dur-base) var(--ease-standard);}
170
+ .fdy-app--nav-overlay.fdy-app--nav-open .fdy-app__sidebar{transform:translateX(0);}
171
+ .fdy-app--nav-overlay .fdy-app__backdrop{display:block;position:fixed;inset:0;z-index:55;background:var(--color-scrim);opacity:0;visibility:hidden;pointer-events:none;transition:opacity var(--dur-base) var(--ease-standard),visibility var(--dur-base) var(--ease-standard);}
172
+ .fdy-app--nav-overlay.fdy-app--nav-open .fdy-app__backdrop{opacity:1;visibility:visible;pointer-events:auto;}
173
+ /* In overlay mode the sidebar is out of flow, so the collapse-to-zero-width rule above must not
174
+ also apply — it would fight the transform for the same panel. */
175
+ .fdy-app--nav-overlay.fdy-app--nav-collapsed .fdy-app__sidebar{width:16rem;border-right-width:var(--bw);}
176
+ }
177
+ .fdy-app__topbar{position:sticky;top:0;z-index:20;min-height:var(--space-16);display:flex;align-items:center;gap:var(--space-3);padding:0 var(--space-8);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border-bottom:var(--bw) solid var(--color-border);}
162
178
  .fdy-app__title{font-family:var(--font-display);font-size:var(--text-xl);font-weight:var(--weight-semibold);color:var(--color-text);margin:0 auto 0 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
163
179
  .fdy-app__main{flex:1;padding:var(--space-8);}
164
180
  .fdy-skip{position:absolute;left:var(--space-2);top:var(--space-2);background:var(--color-primary);color:var(--color-on-primary);padding:var(--space-2) var(--space-4);border-radius:var(--radius-md);z-index:100;transform:translateY(-250%);transition:transform var(--dur-fast) var(--ease-standard);}
@@ -167,13 +183,21 @@ a { color: var(--color-primary); }
167
183
  * drop sticky so nothing escapes the box; sidebar stretches to fill it. */
168
184
  .fdy-app--static .fdy-app__sidebar{position:static;height:auto;align-self:stretch;}
169
185
  .fdy-app--static .fdy-app__topbar{position:static;}
170
- /* Mobile: the sidebar becomes an off-canvas drawer opened by the topbar toggle. */
186
+ /* `pointer-events` on the backdrop is load-bearing, not a belt-and-braces extra. `visibility` is a
187
+ * TRANSITIONED property here, so a backdrop on its way out stays `visible` for the whole duration
188
+ * and keeps swallowing clicks aimed at the topbar underneath it — and switching INTO overlay mode is
189
+ * worse, because the element goes display:none -> display:block with its visibility animating from
190
+ * the initial `visible`, so a mode switch alone hands the page ~200ms of dead clicks. Measured: with
191
+ * only `visibility:hidden`, elementFromPoint over the nav toggle returned the backdrop.
192
+ * Mobile: the sidebar becomes an off-canvas drawer opened by the topbar toggle. */
171
193
  .fdy-app__backdrop{display:none;}
172
194
  @media (max-width:720px){
173
195
  .fdy-app__sidebar{position:fixed;top:0;left:0;bottom:0;height:100dvh;width:16rem;z-index:60;transform:translateX(-100%);transition:transform var(--dur-base) var(--ease-standard);box-shadow:var(--shadow-4);}
174
196
  .fdy-app--nav-open .fdy-app__sidebar{transform:translateX(0);}
175
- .fdy-app__backdrop{display:block;position:fixed;inset:0;z-index:55;background:rgba(8,10,20,.45);opacity:0;visibility:hidden;transition:opacity var(--dur-base) var(--ease-standard),visibility var(--dur-base) var(--ease-standard);}
176
- .fdy-app--nav-open .fdy-app__backdrop{opacity:1;visibility:visible;}
197
+ /* --color-scrim, not a literal: this was the last raw colour left in a component stylesheet, and
198
+ it also means the drawer behind the nav dims exactly like every other overlay in the kit. */
199
+ .fdy-app__backdrop{display:block;position:fixed;inset:0;z-index:55;background:var(--color-scrim);opacity:0;visibility:hidden;pointer-events:none;transition:opacity var(--dur-base) var(--ease-standard),visibility var(--dur-base) var(--ease-standard);}
200
+ .fdy-app--nav-open .fdy-app__backdrop{opacity:1;visibility:visible;pointer-events:auto;}
177
201
  .fdy-app__main{padding:var(--space-5);}
178
202
  .fdy-app__topbar{padding:0 var(--space-4);}
179
203
  }
@@ -182,9 +206,9 @@ a { color: var(--color-primary); }
182
206
  /* Freeday. App bar (standalone top bar: brand · actions · spacer).
183
207
  * Distinct from .fdy-app__topbar (which is bound to the shell grid). Use anywhere;
184
208
  * sticky / elevated / dense / coloured via modifiers. Pure CSS. */
185
- .fdy-appbar{display:flex;align-items:center;gap:var(--space-3);height:var(--space-16);padding:0 var(--space-4);background:var(--color-surface);color:var(--color-text);border-bottom:var(--bw) solid var(--color-border);}
209
+ .fdy-appbar{display:flex;align-items:center;gap:var(--space-3);height:var(--space-16);padding:0 var(--space-4);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);border-bottom:var(--bw) solid var(--color-border);}
186
210
  .fdy-appbar--sticky{position:sticky;top:0;z-index:90;}
187
- .fdy-appbar--elevated{border-bottom:0;box-shadow:var(--shadow-2);}
211
+ .fdy-appbar--elevated{border-bottom:0;box-shadow:var(--shadow-2),var(--surface-inset);}
188
212
  .fdy-appbar--dense{height:var(--space-12);}
189
213
  .fdy-appbar--primary{background:var(--color-primary);color:var(--color-on-primary);border-bottom-color:transparent;}
190
214
  .fdy-appbar__brand{display:inline-flex;align-items:center;gap:var(--space-2);font-family:var(--font-display);font-weight:var(--weight-bold);font-size:var(--text-lg);color:inherit;white-space:nowrap;}
@@ -317,6 +341,40 @@ a { color: var(--color-primary); }
317
341
  @media (min-width:960px){.fdy-hide-above-md{display:none !important;}}
318
342
  @media (min-width:1280px){.fdy-hide-above-lg{display:none !important;}}
319
343
 
344
+ /* Freeday. Busy overlay */
345
+ /* The blocking "the app is working" state, for an operation the reader must not interrupt or
346
+ * double-submit. There is deliberately NO markup contract to hand-write: it is created by
347
+ * `Freeday.busy()` and removed by `Freeday.idle()`, because two of these on screen at once — with
348
+ * two different captions, from two components that each thought they owned it — is the exact bug
349
+ * this component exists to prevent. One owner, enforced by there being only one way in.
350
+ *
351
+ * It is not a dialog. Nothing here asks a question and nothing can be dismissed; interaction is
352
+ * removed with `inert` on everything else, not with a focus trap. */
353
+
354
+ /* `display` is gated on `.is-open` rather than on `:popover-open`, and that is load-bearing. The UA
355
+ * sheet hides a popover with `[popover]:not(:popover-open){display:none}`, but AUTHOR rules beat UA
356
+ * rules whatever the specificity, so a bare `.fdy-busy{display:flex}` would paint the overlay
357
+ * permanently the moment the element existed. Gating on a class instead also means the overlay
358
+ * still works where the Popover API does not: it falls back to a plain fixed layer at this
359
+ * z-index, which cannot clear an open <dialog> but does everything else. */
360
+ .fdy-busy{display:none;position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:0;border:0;overflow:hidden;background:var(--color-scrim);z-index:190;}
361
+ .fdy-busy.is-open{display:flex;align-items:center;justify-content:center;}
362
+ /* The popover's own backdrop would double the dim; this element paints its own. */
363
+ .fdy-busy::backdrop{background:transparent;}
364
+
365
+ /* The caption sits on a solid surface, not on the scrim. A scrim is semi-transparent over unknown
366
+ * content, so text on it has no computable contrast ratio and no gate could ever cover it. On
367
+ * --color-surface it is the same pair the contrast suite already checks. */
368
+ .fdy-busy__panel{display:flex;flex-direction:column;align-items:center;gap:var(--space-4);padding:var(--space-6) var(--space-8);border-radius:var(--radius-lg);background:var(--color-surface);box-shadow:var(--shadow-lift);max-width:min(20rem,calc(100vw - var(--space-8)));}
369
+ .fdy-busy__mark{display:flex;align-items:center;justify-content:center;min-height:2.5rem;}
370
+ .fdy-busy__caption{margin:0;font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);text-align:center;}
371
+
372
+ /* Nothing travels under reduced motion. The spinner keeps turning (slowed by spinner.css): a
373
+ * loading indicator that stops moving reads as "finished", which is the opposite of the message. */
374
+ @media (prefers-reduced-motion:reduce){
375
+ .fdy-busy{transition:none;}
376
+ }
377
+
320
378
  /* Freeday. Button */
321
379
  .fdy-btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-2);
322
380
  height:var(--control-h);padding:0 var(--space-4);
@@ -420,7 +478,7 @@ a { color: var(--color-primary); }
420
478
  /* Freeday. Card */
421
479
  /* position:relative, containing block for out-of-flow card content (see base.css); it also gives
422
480
  consumers the anchor a corner ribbon/badge needs. */
423
- .fdy-card{position:relative;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-lift);transition:box-shadow var(--dur-base) var(--ease-standard),transform var(--dur-base) var(--ease-standard);}
481
+ .fdy-card{position:relative;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-lift),var(--surface-inset);transition:box-shadow var(--dur-base) var(--ease-standard),transform var(--dur-base) var(--ease-standard);}
424
482
  .fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
425
483
  .fdy-card--interactive{cursor:pointer;}
426
484
  .fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
@@ -865,7 +923,7 @@ a { color: var(--color-primary); }
865
923
  /* display is gated behind [open] so the native <dialog> stays hidden until opened.
866
924
  * An unconditional display here would beat the UA `dialog:not([open]){display:none}`
867
925
  * rule (author origin wins over user-agent) and leave the drawer visible on load. */
868
- .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface);color:var(--color-text);box-shadow:var(--shadow-4);overflow:hidden;transform:translateX(-100%);transition:transform var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
926
+ .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition:transform var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
869
927
  .fdy-drawer[open]{display:flex;transform:none;}
870
928
  .fdy-drawer--right{inset:0 0 0 auto;transform:translateX(100%);}
871
929
  .fdy-drawer::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition:opacity var(--dur-slow) var(--ease-standard),overlay var(--dur-slow) allow-discrete,display var(--dur-slow) allow-discrete;}
@@ -1108,7 +1166,7 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1108
1166
 
1109
1167
  /* Freeday. Menu (action popup) + Split button. Enhanced by freeday-menu.js. */
1110
1168
  .fdy-menu-wrap{position:relative;display:inline-flex;}
1111
- .fdy-menu{position:absolute;top:calc(100% + var(--space-1));left:0;z-index:130;min-width:11rem;margin:0;padding:var(--space-1);list-style:none;background:var(--color-surface);color:var(--color-text);border:var(--bw) solid var(--color-border-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-3);}
1169
+ .fdy-menu{position:absolute;top:calc(100% + var(--space-1));left:0;z-index:130;min-width:11rem;margin:0;padding:var(--space-1);list-style:none;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);border:var(--bw) solid var(--color-border-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-3),var(--surface-inset);}
1112
1170
  .fdy-menu[hidden]{display:none;}
1113
1171
  .fdy-menu--end{left:auto;right:0;}
1114
1172
  .fdy-menu__item{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:var(--space-2) var(--space-3);border:0;background:transparent;border-radius:var(--radius-sm);font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);text-align:left;cursor:pointer;white-space:nowrap;}
@@ -1139,7 +1197,7 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1139
1197
  .fdy-btn-split__toggle svg{display:block;width:1rem;height:1rem;}
1140
1198
 
1141
1199
  /* Freeday. Modal (native <dialog>) */
1142
- .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface);color:var(--color-text);box-shadow:var(--shadow-lift-hover);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);transition:opacity var(--dur-base) var(--ease-spring),transform var(--dur-base) var(--ease-spring),overlay var(--dur-base) allow-discrete,display var(--dur-base) allow-discrete;}
1200
+ .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);transition:opacity var(--dur-base) var(--ease-spring),transform var(--dur-base) var(--ease-spring),overlay var(--dur-base) allow-discrete,display var(--dur-base) allow-discrete;}
1143
1201
  .fdy-modal--sm{width:min(24rem,calc(100vw - var(--space-8)));}
1144
1202
  .fdy-modal--md{width:min(32rem,calc(100vw - var(--space-8)));}
1145
1203
  .fdy-modal--lg{width:min(48rem,calc(100vw - var(--space-8)));}
@@ -1289,7 +1347,18 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1289
1347
  .fdy-step.is-active .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary-strong);}
1290
1348
  .fdy-step.is-complete .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary);color:var(--color-on-primary);}
1291
1349
  .fdy-step.is-error .fdy-step__marker{border-color:var(--color-danger);background:var(--color-danger-btn);color:var(--color-on-danger);}
1350
+ /* The error badge. Colour alone cannot carry "this step has a problem" — the marker already turns
1351
+ red and that is exactly the signal a colour-blind reader loses — so the badge holds a COUNT and
1352
+ the step names it in text. Positioned against `.fdy-step` rather than `__btn`, because the
1353
+ indicator-only variant has no `__btn` and the badge has to land in the same place in both.
1354
+ It is a SIBLING of the marker, never a child: the enhancer rewrites `__marker.innerHTML` on
1355
+ every render, so a badge inside it would be destroyed the first time the reader moved a step. */
1356
+ .fdy-step__badge{position:absolute;top:0;left:50%;z-index:2;transform:translate(.45rem,-.3rem);min-width:1.05rem;height:1.05rem;padding:0 .25rem;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-full);background:var(--color-danger-btn);color:var(--color-on-danger);font-size:var(--text-xs);font-weight:var(--weight-semibold);font-variant-numeric:tabular-nums;line-height:1;}
1292
1357
  .fdy-step__label{font-size:var(--text-sm);color:var(--color-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;}
1358
+ .fdy-step.is-error .fdy-step__label{color:var(--color-danger-strong);}
1359
+ /* While a guard is deciding, the whole control is spoken for: the nav buttons are disabled by the
1360
+ enhancer, and this dims the header so a second click has nothing to aim at. */
1361
+ .fdy-stepper[aria-busy="true"]{opacity:.72;}
1293
1362
  .fdy-step.is-active .fdy-step__label{color:var(--color-text);font-weight:var(--weight-semibold);}
1294
1363
  .fdy-step__btn:focus-visible{outline:none;}
1295
1364
  .fdy-step__btn:focus-visible .fdy-step__marker{box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 30%,transparent);}
@@ -1308,6 +1377,13 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1308
1377
  .fdy-table td{padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);color:var(--color-text);}
1309
1378
  .fdy-table tbody tr:last-child td{border-bottom:0;}
1310
1379
  .fdy-table tbody tr{transition:background-color var(--dur-fast) var(--ease-standard);}
1380
+ /* Zebra rows. Sits BEFORE :hover and [aria-selected] on purpose: all three land on the same
1381
+ specificity (0,2,2), so source order is what decides, and a stripe that won would make hover
1382
+ invisible on every second row. The tint is HALF a surface step rather than --color-surface-2
1383
+ itself, for the same reason: hover paints the full step, so the two must not be the same colour.
1384
+ Exposed as a Tier-3 token through the var() FALLBACK, never as a declaration — a custom property
1385
+ set here would outrank a host's own :root override. */
1386
+ .fdy-table--striped tbody tr:nth-child(even){background:var(--fdy-table-stripe,color-mix(in srgb,var(--color-surface-2) 55%,var(--color-surface)));}
1311
1387
  .fdy-table tbody tr:hover{background:var(--color-surface-2);}
1312
1388
  /* Monospace data, ids, codes, IPs, timestamps. Alignment-neutral so it works in any cell
1313
1389
  or inline (`<span class="fdy-mono">`), not just numeric columns. */
@@ -1372,6 +1448,12 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1372
1448
  a hovered row should band unbroken across the freeze line, says so itself. */
1373
1449
  .fdy-table--sticky tbody .fdy-table__freeze{z-index:1;}
1374
1450
  .fdy-table--sticky tbody td.fdy-table__freeze{background:var(--color-surface);}
1451
+ /* A frozen cell has to paint an OPAQUE background of its own (it scrolls over its neighbours), so
1452
+ on a striped table it would cut a plain column straight through the zebra. Repaint it with the
1453
+ stripe — and deliberately WITHOUT `.fdy-table--sticky` in the selector, which would push this to
1454
+ (0,4,3) and beat the hover rule below. At (0,3,3) it ties, and the later rule wins, so a hovered
1455
+ row still bands unbroken across the freeze line. */
1456
+ .fdy-table--striped tbody tr:nth-child(even) td.fdy-table__freeze{background:var(--fdy-table-stripe,color-mix(in srgb,var(--color-surface-2) 55%,var(--color-surface)));}
1375
1457
  .fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--color-surface-2);}
1376
1458
  /* A frozen header cell is stuck on both axes wherever the head is frozen too, so
1377
1459
  it has to outrank both. */