@birdycom/omniaui 0.12.5 → 0.13.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 (102) hide show
  1. package/README.md +1 -1
  2. package/bin/lib/copy-js.mjs +12 -1
  3. package/bin/omniaui.mjs +2 -2
  4. package/dist/barcode.d.ts +15 -0
  5. package/dist/barcode.js +164 -0
  6. package/dist/components-extra.css +15 -1
  7. package/dist/components.css +2237 -256
  8. package/dist/components.min.css +1 -1
  9. package/dist/contact-form.d.ts +5 -0
  10. package/dist/contact-form.js +138 -0
  11. package/dist/data-table.js +135 -98
  12. package/dist/fly.d.ts +4 -0
  13. package/dist/fly.js +80 -0
  14. package/dist/form-extras.d.ts +11 -0
  15. package/dist/form-extras.js +214 -0
  16. package/dist/lazy.d.ts +5 -0
  17. package/dist/lazy.js +37 -0
  18. package/dist/oc-theme.css +3 -1
  19. package/dist/parts/app-data-table-footer.css +38 -0
  20. package/dist/parts/app-data-table-rich.css +161 -0
  21. package/dist/parts/app-data-table.css +44 -74
  22. package/dist/parts/app-name-card.css +65 -0
  23. package/dist/parts/app-result.css +39 -0
  24. package/dist/parts/app-titlebar.css +16 -8
  25. package/dist/parts/data-meter-row.css +40 -0
  26. package/dist/parts/data-price.css +20 -0
  27. package/dist/parts/data-ribbon.css +76 -0
  28. package/dist/parts/data-spec-list.css +36 -0
  29. package/dist/parts/data-stock.css +41 -0
  30. package/dist/parts/forms-affix-input.css +1 -1
  31. package/dist/parts/forms-avatar-picker.css +43 -0
  32. package/dist/parts/forms-date-field.css +53 -0
  33. package/dist/parts/forms-density.css +13 -0
  34. package/dist/parts/forms-form-section.css +64 -0
  35. package/dist/parts/forms-form.css +76 -0
  36. package/dist/parts/forms-quantity.css +55 -0
  37. package/dist/parts/forms-rating.css +81 -0
  38. package/dist/parts/media-attachment.css +1 -1
  39. package/dist/parts/media-barcode.css +23 -0
  40. package/dist/parts/media-chat-composer.css +3 -0
  41. package/dist/parts/media-emoji-picker.css +3 -1
  42. package/dist/parts/media-gallery.css +55 -0
  43. package/dist/parts/media-glass-liquid.css +99 -0
  44. package/dist/parts/media-glass-options.css +50 -0
  45. package/dist/parts/media-lazy.css +46 -0
  46. package/dist/parts/media-lightbox.css +4 -0
  47. package/dist/parts/media-message-thread.css +1 -1
  48. package/dist/parts/media-message.css +1 -1
  49. package/dist/parts/nav-topbar-variants.css +96 -0
  50. package/dist/parts/site-base.css +33 -0
  51. package/dist/parts/site-brand-mark.css +56 -0
  52. package/dist/parts/site-cards.css +183 -0
  53. package/dist/parts/site-fly.css +40 -0
  54. package/dist/parts/site-header-variants.css +70 -0
  55. package/dist/parts/site-layout.css +69 -0
  56. package/dist/parts/site-panel.css +29 -0
  57. package/dist/parts/site-prose.css +13 -1
  58. package/dist/parts/utility-hover.css +44 -0
  59. package/dist/showcase.css +114 -22
  60. package/manifest.json +193 -9
  61. package/package.json +7 -2
  62. package/snippets/accordion-accent.html +1 -1
  63. package/snippets/attachment.html +4 -4
  64. package/snippets/avatar-picker.html +40 -0
  65. package/snippets/barcode.html +20 -0
  66. package/snippets/brand-mark.html +41 -0
  67. package/snippets/chat-composer.html +3 -3
  68. package/snippets/data-table.html +51 -76
  69. package/snippets/date-field.html +44 -0
  70. package/snippets/doc-viewer.html +1 -1
  71. package/snippets/emoji-picker.html +1 -1
  72. package/snippets/feature-card.html +52 -0
  73. package/snippets/fly-in.html +30 -0
  74. package/snippets/form-density.html +25 -0
  75. package/snippets/form-section.html +47 -0
  76. package/snippets/gallery.html +25 -0
  77. package/snippets/hover-effects.html +19 -0
  78. package/snippets/icon-list.html +28 -0
  79. package/snippets/lazy.html +39 -0
  80. package/snippets/message-thread.html +12 -12
  81. package/snippets/message.html +10 -10
  82. package/snippets/meter-row.html +29 -0
  83. package/snippets/name-card.html +72 -0
  84. package/snippets/page-header.html +1 -1
  85. package/snippets/page-sections.html +50 -0
  86. package/snippets/panel.html +18 -0
  87. package/snippets/price.html +25 -0
  88. package/snippets/prose.html +3 -3
  89. package/snippets/quantity.html +12 -0
  90. package/snippets/rating.html +26 -0
  91. package/snippets/result.html +31 -0
  92. package/snippets/ribbon.html +48 -0
  93. package/snippets/shot-grid.html +27 -0
  94. package/snippets/sidebar.html +3 -3
  95. package/snippets/spec-list.html +34 -0
  96. package/snippets/stock.html +20 -0
  97. package/snippets/table-actions.html +5 -5
  98. package/snippets/table.html +2 -2
  99. package/snippets/titlebar.html +1 -1
  100. package/snippets/user.html +3 -3
  101. package/dist/parts/app-column-menu.css +0 -85
  102. package/snippets/column-menu.html +0 -38
@@ -0,0 +1,53 @@
1
+ /* File: packages/ui/src/parts/forms-date-field.css | Map: field frame -> icon zone -> native date input -> ISO week label -> sizes and states */
2
+ /* Date field: calendar icon | line | date, with the ISO week at the end when wanted.
3
+ The date is a native <input type="date"> (keyboard, picker and locale from the browser). The week label is filled
4
+ by packages/ui/src/date-field.js (or by your own code). */
5
+
6
+ .oc-date-field {
7
+ display: inline-flex;
8
+ align-items: stretch;
9
+ block-size: 2.5rem;
10
+ background: var(--oc-background);
11
+ border: 1px solid var(--oc-input, var(--oc-border));
12
+ border-radius: var(--oc-radius);
13
+ overflow: hidden;
14
+ }
15
+ .oc-date-field:focus-within { border-color: var(--oc-ring); box-shadow: 0 0 0 3px oklch(from var(--oc-ring) l c h / 0.18); }
16
+ .oc-date-field__icon {
17
+ display: inline-flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ inline-size: 2.5rem;
21
+ font-size: 1.125rem;
22
+ color: var(--oc-muted-foreground);
23
+ border-inline-end: 1px solid var(--oc-border);
24
+ }
25
+ .oc-date-field__input {
26
+ min-inline-size: 0;
27
+ padding: 0 0.75rem;
28
+ font: inherit;
29
+ font-size: 0.875rem;
30
+ color: var(--oc-foreground);
31
+ background: transparent;
32
+ border: 0;
33
+ outline: none;
34
+ }
35
+ .oc-date-field__input::-webkit-calendar-picker-indicator { opacity: 0.55; cursor: pointer; }
36
+ /* ISO week: a quiet label at the end ("Week 45"); hidden while there is no date. */
37
+ .oc-date-field__week {
38
+ display: inline-flex;
39
+ align-items: center;
40
+ padding-inline: 0.75rem;
41
+ font-size: 0.75rem;
42
+ font-weight: 600;
43
+ font-variant-numeric: tabular-nums;
44
+ color: var(--oc-primary);
45
+ background: oklch(from var(--oc-primary) l c h / 0.08);
46
+ border-inline-start: 1px solid var(--oc-border);
47
+ white-space: nowrap;
48
+ }
49
+ .oc-date-field__week:empty { display: none; }
50
+ .oc-date-field--sm { block-size: 2rem; }
51
+ .oc-date-field--sm .oc-date-field__icon { inline-size: 2rem; font-size: 1rem; }
52
+ .oc-date-field:has(.oc-date-field__input:disabled) { opacity: 0.55; }
53
+ .oc-date-field:has(.oc-date-field__input[aria-invalid="true"]) { border-color: var(--oc-destructive); }
@@ -0,0 +1,13 @@
1
+ /* File: packages/ui/src/parts/forms-density.css | Map: compact scope -> inputs, selects, date fields, textareas -> buttons -> field gaps */
2
+ /* Form density: .oc-density-compact on a form or any wrapper makes its controls
3
+ 2rem high instead of 2.5rem, for dense editors (invoice templates, inline tables). The default needs no class. */
4
+
5
+ .oc-density-compact :is(.oc-input, .oc-select, .oc-date-field) { block-size: 2rem; font-size: 0.8125rem; }
6
+ .oc-density-compact :is(.oc-input, .oc-select) { padding-block: 0; }
7
+ .oc-density-compact .oc-textarea { padding-block: 0.375rem; font-size: 0.8125rem; }
8
+ .oc-density-compact .oc-date-field__icon { inline-size: 2rem; font-size: 1rem; }
9
+ .oc-density-compact .oc-date-field__input { font-size: 0.8125rem; }
10
+ .oc-density-compact .oc-btn:not(.oc-btn--icon, .oc-btn--plain, .oc-btn--inline) { padding-block: 0.3125rem; font-size: 0.8125rem; }
11
+ .oc-density-compact .oc-btn--icon { --oc-btn-square: 2rem; }
12
+ .oc-density-compact .oc-field { gap: 0.25rem; }
13
+ .oc-density-compact .oc-field-label { font-size: 0.75rem; }
@@ -0,0 +1,64 @@
1
+ /* File: packages/ui/src/parts/forms-form-section.css | Map: section (details) -> summary (icon, title, hint, status, chevron) -> body grid -> invalid state */
2
+ /* Collapsible form section: a long form in named parts that open and close.
3
+ Native <details>; sections with the same name="" keep one open at a time. */
4
+
5
+ .oc-form-section {
6
+ background: var(--oc-card);
7
+ border: 1px solid var(--oc-border);
8
+ border-radius: var(--oc-radius);
9
+ }
10
+ .oc-form-section + .oc-form-section { margin-block-start: 0.75rem; }
11
+ .oc-form-section__summary {
12
+ display: grid;
13
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
14
+ align-items: center;
15
+ gap: 0.25rem 0.75rem;
16
+ padding: 0.875rem 1rem;
17
+ list-style: none;
18
+ cursor: pointer;
19
+ }
20
+ .oc-form-section__summary::-webkit-details-marker { display: none; }
21
+ .oc-form-section__summary:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: -2px; border-radius: var(--oc-radius); }
22
+ .oc-form-section__icon {
23
+ display: inline-flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ inline-size: 2.25rem;
27
+ block-size: 2.25rem;
28
+ font-size: 1.25rem;
29
+ color: var(--oc-primary);
30
+ background: oklch(from var(--oc-primary) l c h / 0.1);
31
+ border-radius: calc(var(--oc-radius) - 2px);
32
+ }
33
+ .oc-form-section__heading { display: grid; gap: 0.125rem; min-inline-size: 0; }
34
+ .oc-form-section__title { margin: 0; font-size: 0.9375rem; font-weight: 600; }
35
+ .oc-form-section__hint { margin: 0; font-size: 0.8125rem; color: var(--oc-muted-foreground); }
36
+ .oc-form-section__status { font-size: 0.75rem; white-space: nowrap; }
37
+ .oc-form-section__summary::after {
38
+ content: "";
39
+ inline-size: 1.125rem;
40
+ block-size: 1.125rem;
41
+ background: var(--oc-muted-foreground);
42
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.41 8.58L12 13.17l4.59-4.59L18 10l-6 6l-6-6z'/%3E%3C/svg%3E") center / contain no-repeat;
43
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.41 8.58L12 13.17l4.59-4.59L18 10l-6 6l-6-6z'/%3E%3C/svg%3E") center / contain no-repeat;
44
+ transition: rotate var(--oc-transition-fast);
45
+ }
46
+ .oc-form-section[open] > .oc-form-section__summary::after { rotate: 180deg; }
47
+ .oc-form-section__body {
48
+ display: grid;
49
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
50
+ gap: 1rem;
51
+ padding: 1rem;
52
+ border-block-start: 1px solid var(--oc-border);
53
+ }
54
+ .oc-form-section__body > .oc-form-section__wide { grid-column: 1 / -1; }
55
+ /* A section with an invalid field gets a red edge, also while closed, so an error is never hidden. */
56
+ .oc-form-section:has(:user-invalid, [aria-invalid="true"]) { border-color: var(--oc-destructive); }
57
+ .oc-form-section:has(:user-invalid, [aria-invalid="true"]) .oc-form-section__icon {
58
+ color: var(--oc-destructive);
59
+ background: oklch(from var(--oc-destructive) l c h / 0.1);
60
+ }
61
+
62
+ @media (prefers-reduced-motion: reduce) {
63
+ .oc-form-section__summary::after { transition: none; }
64
+ }
@@ -0,0 +1,76 @@
1
+ /* File: packages/ui/src/parts/forms-form.css */
2
+ /* Map: form grid -> label hint -> actions + note -> busy -> filled variant */
3
+ /* A form as one grid of fields with one gap, an "(optional)" hint in a label, a row with the submit button and a short
4
+ note, the busy state while it sends (aria-busy on the form), and .oc-form--filled: fields with a fill and a stronger
5
+ border, for a form on a card, a photo or a dark band. All through the field tokens of .oc-input. */
6
+
7
+ .oc-form {
8
+ display: grid;
9
+ gap: var(--oc-form-gap, 1.1rem);
10
+ }
11
+ .oc-form .oc-field-group { gap: var(--oc-form-gap, 1.1rem); }
12
+ .oc-form .oc-field {
13
+ gap: 0.4rem;
14
+ margin-bottom: 0;
15
+ }
16
+
17
+ /* <label class="oc-field-label">Phone <span class="oc-field-label__hint">(optional)</span></label> */
18
+ .oc-field-label__hint {
19
+ color: var(--oc-muted-foreground);
20
+ font-weight: 400;
21
+ }
22
+
23
+ /* Submit button with a short note next to it (wraps under it on a narrow form). */
24
+ .oc-form-actions {
25
+ display: flex;
26
+ flex-wrap: wrap;
27
+ align-items: center;
28
+ gap: 1rem 1.5rem;
29
+ margin-block-start: 0.4rem;
30
+ }
31
+ .oc-form-note {
32
+ flex: 1 1 16rem;
33
+ margin: 0;
34
+ color: var(--oc-muted-foreground);
35
+ font-size: 0.85rem;
36
+ }
37
+ .oc-form-note a {
38
+ color: var(--oc-primary);
39
+ text-decoration: underline;
40
+ text-underline-offset: 3px;
41
+ }
42
+ .oc-form-note a:hover { color: var(--oc-foreground); }
43
+
44
+ /* While the form sends (form aria-busy="true"): the submit button dims and shows a progress cursor. */
45
+ .oc-form[aria-busy="true"] :is([type="submit"], button:not([type])) {
46
+ opacity: 0.7;
47
+ cursor: progress;
48
+ }
49
+
50
+ /* Filled fields: a fill (dark in dark mode), the strong border, more room and a wider focus ring. */
51
+ .oc-form--filled {
52
+ --oc-input-bg: light-dark(oklch(from var(--oc-foreground) l c h / 0.03), oklch(0 0 0 / 0.25));
53
+ --oc-input-border: var(--oc-border-strong);
54
+ --oc-input-padding: 0.75rem 0.9rem;
55
+ --oc-input-radius: var(--oc-radius-md, 0.75rem);
56
+ --oc-input-size: 1rem;
57
+ --oc-ring-width: 3px;
58
+ --oc-textarea-min: 9rem;
59
+ }
60
+ .oc-form--filled .oc-field-label {
61
+ font-size: 0.9rem;
62
+ font-weight: 600;
63
+ line-height: 1.4;
64
+ }
65
+ .oc-form--filled .oc-field-error { font-size: 0.85rem; }
66
+ .oc-form--filled .oc-select option { background: var(--oc-card); }
67
+
68
+ /* Honeypot: a field for bots, off screen for people. Not display:none (some bots skip hidden fields). Give the wrapper
69
+ aria-hidden="true" and the input tabindex="-1" autocomplete="off"; the server ignores a form where it is filled. */
70
+ .oc-honeypot {
71
+ position: absolute;
72
+ inset-inline-start: -10000px;
73
+ inline-size: 1px;
74
+ block-size: 1px;
75
+ overflow: hidden;
76
+ }
@@ -0,0 +1,55 @@
1
+ /* File: packages/ui/src/parts/forms-quantity.css | Map: frame -> step buttons -> number input (no spinners) -> focus and disabled -> sizes */
2
+ /* Quantity field: minus, a number input and plus in one frame. Without a script it is a normal number input
3
+ (the browser's arrows and keyboard); form-extras.js makes the buttons step within min / max / step. */
4
+
5
+ .oc-quantity {
6
+ display: inline-flex;
7
+ inline-size: fit-content;
8
+ justify-self: start;
9
+ align-items: stretch;
10
+ block-size: 2.25rem;
11
+ background: var(--oc-background);
12
+ border: 1px solid var(--oc-input, var(--oc-border));
13
+ border-radius: var(--oc-radius);
14
+ overflow: hidden;
15
+ }
16
+ .oc-quantity__step {
17
+ display: inline-grid;
18
+ place-items: center;
19
+ flex-shrink: 0;
20
+ inline-size: 2.25rem;
21
+ padding: 0;
22
+ font: inherit;
23
+ color: var(--oc-muted-foreground);
24
+ background: transparent;
25
+ border: 0;
26
+ cursor: pointer;
27
+ }
28
+ .oc-quantity__step > :is(svg, iconify-icon) { inline-size: 1rem; block-size: 1rem; font-size: 1rem; }
29
+ .oc-quantity__step:hover { color: var(--oc-foreground); background: var(--oc-accent); }
30
+ .oc-quantity__step:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: -2px; }
31
+ .oc-quantity__step:disabled { opacity: 0.35; cursor: not-allowed; background: transparent; }
32
+ .oc-quantity__input {
33
+ inline-size: 3.25rem;
34
+ min-inline-size: 0;
35
+ padding: 0 0.25rem;
36
+ font: inherit;
37
+ font-size: 0.875rem;
38
+ font-weight: 600;
39
+ font-variant-numeric: tabular-nums;
40
+ text-align: center;
41
+ color: var(--oc-foreground);
42
+ background: transparent;
43
+ border: 0;
44
+ border-inline: 1px solid var(--oc-border);
45
+ -moz-appearance: textfield;
46
+ appearance: textfield;
47
+ }
48
+ .oc-quantity__input::-webkit-outer-spin-button,
49
+ .oc-quantity__input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
50
+ .oc-quantity__input:focus { outline: none; }
51
+ .oc-quantity:has(.oc-quantity__input:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
52
+ .oc-quantity:has(.oc-quantity__input:disabled) { opacity: 0.5; }
53
+ .oc-quantity--sm { block-size: 2rem; }
54
+ .oc-quantity--sm .oc-quantity__step { inline-size: 2rem; }
55
+ .oc-quantity--sm .oc-quantity__input { inline-size: 2.75rem; font-size: 0.8125rem; }
@@ -0,0 +1,81 @@
1
+ /* File: packages/ui/src/parts/forms-rating.css | Map: star mask -> rating (display) -> sizes -> summary line -> rating input (radios, hover preview) -> focus */
2
+ /* Star rating without JavaScript. .oc-rating shows a score (--oc-rating, halves and decimals work); .oc-rating-input
3
+ is five radio buttons drawn as stars: :has() fills up to the checked or hovered star. */
4
+
5
+ .oc-rating,
6
+ .oc-rating-input {
7
+ --oc-rating-size: 1.125rem;
8
+ --oc-rating-color: var(--oc-warning);
9
+ --oc-rating-empty: oklch(from var(--oc-muted-foreground) l c h / 0.28);
10
+ --oc-rating-star: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2L9.19 8.62L2 9.24l5.45 4.73L5.82 21z'/%3E%3C/svg%3E");
11
+ }
12
+
13
+ /* Display: one bar of five stars; the fill is the score out of --oc-rating-max (default 5). */
14
+ .oc-rating {
15
+ --oc-rating: 0;
16
+ --oc-rating-max: 5;
17
+ display: inline-block;
18
+ flex-shrink: 0;
19
+ inline-size: calc(var(--oc-rating-size) * var(--oc-rating-max));
20
+ block-size: var(--oc-rating-size);
21
+ vertical-align: middle;
22
+ background: linear-gradient(90deg, var(--oc-rating-color) calc(var(--oc-rating) / var(--oc-rating-max) * 100%), var(--oc-rating-empty) 0);
23
+ -webkit-mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
24
+ mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
25
+ }
26
+ .oc-rating--sm, .oc-rating-input.oc-rating-input--sm { --oc-rating-size: 0.875rem; }
27
+ .oc-rating--lg, .oc-rating-input.oc-rating-input--lg { --oc-rating-size: 1.5rem; }
28
+
29
+ /* Summary: stars, the score and the number of reviews on one line. */
30
+ .oc-rating-summary { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.5rem; font-size: 0.875rem; }
31
+ .oc-rating-summary__score { font-weight: 700; font-variant-numeric: tabular-nums; }
32
+ .oc-rating-summary__count { color: var(--oc-muted-foreground); }
33
+ a.oc-rating-summary__count { text-decoration: underline; text-underline-offset: 0.2em; }
34
+ a.oc-rating-summary__count:hover { color: var(--oc-foreground); }
35
+
36
+ /* Input: a fieldset with five labels (radio inside). The stars are one masked bar behind the labels. */
37
+ .oc-rating-input {
38
+ position: relative;
39
+ display: inline-flex;
40
+ inline-size: calc(var(--oc-rating-size) * 5 + 0.5rem);
41
+ block-size: calc(var(--oc-rating-size) + 0.5rem);
42
+ margin: 0;
43
+ padding: 0.25rem;
44
+ border: 0;
45
+ border-radius: calc(var(--oc-radius) - 2px);
46
+ }
47
+ .oc-rating-input::before {
48
+ content: "";
49
+ position: absolute;
50
+ inset: 0.25rem;
51
+ background: linear-gradient(90deg, var(--oc-rating-color) calc(var(--oc-rating-hover, var(--oc-rating-pick, 0)) * var(--oc-rating-size)), var(--oc-rating-empty) 0);
52
+ -webkit-mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
53
+ mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
54
+ pointer-events: none;
55
+ }
56
+ .oc-rating-input > legend { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
57
+ .oc-rating-input > label { position: relative; z-index: 1; flex: 0 0 var(--oc-rating-size); cursor: pointer; }
58
+ .oc-rating-input > label > input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
59
+ .oc-rating-input > label > span { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
60
+
61
+ .oc-rating-input:has(label:nth-of-type(1) input:checked) { --oc-rating-pick: 1; }
62
+ .oc-rating-input:has(label:nth-of-type(2) input:checked) { --oc-rating-pick: 2; }
63
+ .oc-rating-input:has(label:nth-of-type(3) input:checked) { --oc-rating-pick: 3; }
64
+ .oc-rating-input:has(label:nth-of-type(4) input:checked) { --oc-rating-pick: 4; }
65
+ .oc-rating-input:has(label:nth-of-type(5) input:checked) { --oc-rating-pick: 5; }
66
+ /* Hover preview (pointer devices only, so a tap does not leave a preview behind). */
67
+ @media (hover: hover) {
68
+ .oc-rating-input:has(label:nth-of-type(1):hover) { --oc-rating-hover: 1; }
69
+ .oc-rating-input:has(label:nth-of-type(2):hover) { --oc-rating-hover: 2; }
70
+ .oc-rating-input:has(label:nth-of-type(3):hover) { --oc-rating-hover: 3; }
71
+ .oc-rating-input:has(label:nth-of-type(4):hover) { --oc-rating-hover: 4; }
72
+ .oc-rating-input:has(label:nth-of-type(5):hover) { --oc-rating-hover: 5; }
73
+ }
74
+ .oc-rating-input:has(input:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
75
+ .oc-rating-input:has(input:disabled) { opacity: 0.5; }
76
+ .oc-rating-input:has(input:disabled) > label,
77
+ .oc-rating-input:has(input:disabled) > label > input { cursor: not-allowed; }
78
+
79
+ @media (forced-colors: active) {
80
+ .oc-rating, .oc-rating-input::before { --oc-rating-color: CanvasText; --oc-rating-empty: GrayText; forced-color-adjust: none; }
81
+ }
@@ -1,7 +1,7 @@
1
1
  /* File: packages/ui/src/parts/media-attachment.css | Map: list -> file chip (icon by type, name, size, open/remove) -> type tones
2
2
  -> uploading progress + error -> image preview (in a message) -> thumbnail chip (in a composer) -> reduced motion
3
3
  Attachment: a file chip or an image preview inside a chat message or a composer. The icon tile shows the extension and is
4
- tinted per type (--pdf, --doc, --sheet, --zip, --image). Based on the attachment picker of the Korrekt question chat. */
4
+ tinted per type (--pdf, --doc, --sheet, --zip, --image). */
5
5
 
6
6
  .oc-attachment-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; padding: 0; list-style: none; }
7
7
  .oc-attachment-list--stack { flex-direction: column; flex-wrap: nowrap; }
@@ -0,0 +1,23 @@
1
+ /* File: packages/ui/src/parts/media-barcode.css | Map: barcode figure -> svg bars -> digits -> sizes (hero, compact) -> error */
2
+ /* Barcode display: EAN-13, EAN-8, UPC-A or Code 128, drawn as SVG by
3
+ packages/ui/src/barcode.js from data-value. Always black bars on white, in both themes, so scanners read it. */
4
+
5
+ .oc-barcode {
6
+ display: inline-flex;
7
+ flex-direction: column;
8
+ align-items: center;
9
+ gap: 0.25rem;
10
+ margin: 0;
11
+ padding: 0.75rem 1rem;
12
+ color: var(--color-black-50);
13
+ background: var(--color-white-50);
14
+ border: 1px solid var(--oc-border);
15
+ border-radius: var(--oc-radius);
16
+ }
17
+ .oc-barcode svg { display: block; inline-size: auto; block-size: var(--oc-barcode-height, 4rem); max-inline-size: 100%; }
18
+ .oc-barcode__digits { font-family: var(--oc-font-mono); font-size: 0.8125rem; letter-spacing: 0.18em; color: inherit; }
19
+ .oc-barcode--hero { padding: 1.25rem 1.5rem; --oc-barcode-height: 6rem; }
20
+ .oc-barcode--hero .oc-barcode__digits { font-size: 1rem; }
21
+ .oc-barcode--compact { padding: 0.375rem 0.5rem; --oc-barcode-height: 2.25rem; }
22
+ .oc-barcode--compact .oc-barcode__digits { font-size: 0.6875rem; letter-spacing: 0.1em; }
23
+ .oc-barcode[data-error] .oc-barcode__digits { color: var(--color-red-70); letter-spacing: 0; }
@@ -56,6 +56,9 @@
56
56
  line-height: 1;
57
57
  cursor: pointer;
58
58
  }
59
+ /* Webicons in the buttons (inline SVG or <iconify-icon>): a fixed size, so nothing is cut off at the round edge. */
60
+ .oc-chat-composer__tool > :is(svg, iconify-icon) { inline-size: 1.25rem; block-size: 1.25rem; font-size: 1.25rem; }
61
+ .oc-chat-composer__send > :is(svg, iconify-icon) { inline-size: 1.125rem; block-size: 1.125rem; font-size: 1.125rem; }
59
62
  .oc-chat-composer__tool:hover { background: var(--oc-surface-strong); color: var(--oc-foreground); }
60
63
  .oc-chat-composer__tool:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
61
64
 
@@ -15,9 +15,11 @@
15
15
  background: transparent;
16
16
  font-size: 1.15rem;
17
17
  line-height: 1;
18
+ color: var(--oc-muted-foreground);
18
19
  cursor: pointer;
19
20
  }
20
- .oc-emoji-picker__trigger:hover { background: var(--oc-surface-strong); }
21
+ .oc-emoji-picker__trigger > :is(svg, iconify-icon) { inline-size: 1.25rem; block-size: 1.25rem; font-size: 1.25rem; }
22
+ .oc-emoji-picker__trigger:hover { color: var(--oc-foreground); background: var(--oc-surface-strong); }
21
23
  .oc-emoji-picker__trigger:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
22
24
 
23
25
  /* The panel: floats above the trigger (implicit anchor of popovertarget), flips when there is no room. */
@@ -0,0 +1,55 @@
1
+ /* File: packages/ui/src/parts/media-gallery.css | Map: gallery grid -> stage and slides (radio + :has, up to 8) -> badge -> thumbnails -> side layout (container query) */
2
+ /* Product gallery without JavaScript: one large photo and a row of thumbnails. Every thumbnail is a label with a
3
+ radio; :has() shows the slide at the same position. Arrow keys move between the photos (native radios). */
4
+
5
+ .oc-gallery { container-type: inline-size; inline-size: 100%; display: grid; gap: 0.75rem; min-inline-size: 0; }
6
+ .oc-gallery__stage {
7
+ position: relative;
8
+ aspect-ratio: var(--oc-gallery-ratio, 4 / 3);
9
+ overflow: hidden;
10
+ background: var(--oc-muted);
11
+ border: 1px solid var(--oc-border);
12
+ border-radius: calc(var(--oc-radius) + 4px);
13
+ }
14
+ .oc-gallery__slide { display: none; margin: 0; block-size: 100%; }
15
+ .oc-gallery__slide > img { display: block; inline-size: 100%; block-size: 100%; object-fit: var(--oc-gallery-fit, contain); }
16
+ .oc-gallery:not(:has(.oc-gallery__thumb input:checked)) .oc-gallery__slide:first-of-type,
17
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(1) input:checked) .oc-gallery__slide:nth-of-type(1),
18
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(2) input:checked) .oc-gallery__slide:nth-of-type(2),
19
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(3) input:checked) .oc-gallery__slide:nth-of-type(3),
20
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(4) input:checked) .oc-gallery__slide:nth-of-type(4),
21
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(5) input:checked) .oc-gallery__slide:nth-of-type(5),
22
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(6) input:checked) .oc-gallery__slide:nth-of-type(6),
23
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(7) input:checked) .oc-gallery__slide:nth-of-type(7),
24
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(8) input:checked) .oc-gallery__slide:nth-of-type(8) { display: block; }
25
+ .oc-gallery__badge { position: absolute; inset: 0.75rem auto auto 0.75rem; z-index: 1; }
26
+
27
+ .oc-gallery__thumbs { display: flex; gap: 0.5rem; min-inline-size: 0; margin: 0; padding: 0.125rem; overflow-x: auto; border: 0; scrollbar-width: thin; }
28
+ .oc-gallery__thumbs > legend { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
29
+ .oc-gallery__thumb {
30
+ position: relative;
31
+ flex: 0 0 auto;
32
+ inline-size: var(--oc-gallery-thumb, 4rem);
33
+ aspect-ratio: 1;
34
+ overflow: hidden;
35
+ background: var(--oc-muted);
36
+ border: 2px solid transparent;
37
+ border-radius: var(--oc-radius);
38
+ outline: 1px solid var(--oc-border);
39
+ outline-offset: -1px;
40
+ cursor: pointer;
41
+ }
42
+ .oc-gallery__thumb > img { display: block; inline-size: 100%; block-size: 100%; object-fit: cover; }
43
+ .oc-gallery__thumb > input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
44
+ .oc-gallery__thumb:hover { outline-color: var(--oc-muted-foreground); }
45
+ .oc-gallery__thumb:has(input:checked) { border-color: var(--oc-primary); outline-color: transparent; }
46
+ .oc-gallery__thumb:has(input:focus-visible) { box-shadow: 0 0 0 2px var(--oc-background), 0 0 0 4px var(--oc-ring); }
47
+
48
+ /* Side: thumbnails in a column on the left once the gallery itself is 32rem wide or more. The grid is always two
49
+ tracks; below 32rem both children span them, so the query only moves the children (a container cannot query itself). */
50
+ .oc-gallery--side { grid-template-columns: auto minmax(0, 1fr); }
51
+ .oc-gallery--side > * { grid-column: 1 / -1; }
52
+ @container (min-width: 32rem) {
53
+ .oc-gallery--side > .oc-gallery__stage { grid-column: 2; grid-row: 1; }
54
+ .oc-gallery--side > .oc-gallery__thumbs { grid-column: 1; grid-row: 1; flex-direction: column; overflow: visible; }
55
+ }
@@ -0,0 +1,99 @@
1
+ /* File: packages/ui/src/parts/media-glass-liquid.css
2
+ Map: liquid sheet (.oc-glass--liquid: clear tint, strong blur + saturation, inner light, specular rim) -> menu bar
3
+ (.oc-glass-bar: items with icon and label, the current one in a raised bubble) -> icon-only and round button -> fallbacks
4
+ Liquid glass in the style of recent desktop and phone systems, in CSS only: the backdrop is blurred and brightened,
5
+ the edge catches light. Put it over a photo or colourful content; on a plain page it looks like a soft card. */
6
+
7
+ .oc-glass.oc-glass--liquid,
8
+ .oc-glass--liquid,
9
+ .oc-glass-bar,
10
+ .oc-site-header--glass .oc-site-header__inner {
11
+ --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.1), oklch(100% 0 0deg / 0.05));
12
+ --oc-liquid-edge: light-dark(oklch(100% 0 0deg / 0.55), oklch(100% 0 0deg / 0.22));
13
+ --oc-liquid-blur: 5px;
14
+ position: relative;
15
+ isolation: isolate;
16
+ background: var(--oc-liquid-tint);
17
+ border: 1px solid var(--oc-liquid-edge);
18
+ /* Light enters at the top left and leaves at the bottom right; a soft glow runs along the inner edge,
19
+ so the sheet reads as a thick, clear lens rather than frosted plastic. */
20
+ box-shadow:
21
+ 0 10px 30px -10px oklch(0% 0 0deg / 0.35),
22
+ inset 2px 2px 1px -2px oklch(100% 0 0deg / 0.95),
23
+ inset -2px -2px 1px -2px oklch(100% 0 0deg / 0.7),
24
+ inset 0 0 10px 1px oklch(100% 0 0deg / 0.22),
25
+ inset 0 -8px 16px -12px oklch(100% 0 0deg / 0.35);
26
+ backdrop-filter: blur(var(--oc-liquid-blur)) saturate(1.9) brightness(1.1) contrast(1.05);
27
+ }
28
+ /* Specular rim: bright on the top-left and bottom-right edge, only on the border ring. */
29
+ .oc-glass--liquid::before,
30
+ .oc-glass-bar::before,
31
+ .oc-site-header--glass .oc-site-header__inner::before {
32
+ content: "";
33
+ position: absolute;
34
+ inset: -1px;
35
+ z-index: 1;
36
+ padding: 1.5px;
37
+ border-radius: inherit;
38
+ background: linear-gradient(135deg, oklch(100% 0 0deg / 1), oklch(100% 0 0deg / 0.15) 25%, oklch(100% 0 0deg / 0) 50%, oklch(100% 0 0deg / 0.15) 75%, oklch(100% 0 0deg / 0.8));
39
+ mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
40
+ mask-composite: exclude;
41
+ pointer-events: none;
42
+ }
43
+
44
+ /* Menu bar: a pill of items, each an icon over a short label. */
45
+ .oc-glass-bar {
46
+ display: inline-flex;
47
+ align-items: center;
48
+ gap: 0.2rem;
49
+ padding: 0.35rem;
50
+ border-radius: 999px;
51
+ }
52
+ .oc-glass-bar__item {
53
+ position: relative;
54
+ display: grid;
55
+ justify-items: center;
56
+ gap: 0.15rem;
57
+ min-inline-size: 4.25rem;
58
+ padding: 0.45rem 0.85rem;
59
+ border: 0;
60
+ border-radius: 999px;
61
+ color: light-dark(oklch(22% 0.02 260deg / 0.78), oklch(100% 0 0deg / 0.82));
62
+ background: none;
63
+ font: inherit;
64
+ font-size: 0.7rem;
65
+ font-weight: 600;
66
+ line-height: 1.1;
67
+ text-decoration: none;
68
+ cursor: pointer;
69
+ transition: background-color 0.25s, color 0.25s, box-shadow 0.25s, scale 0.2s;
70
+ }
71
+ .oc-glass-bar__item > :is(svg, iconify-icon) { inline-size: 1.4rem; block-size: 1.4rem; font-size: 1.4rem; }
72
+ .oc-glass-bar__item:hover { background: light-dark(oklch(100% 0 0deg / 0.28), oklch(100% 0 0deg / 0.08)); }
73
+ .oc-glass-bar__item:active { scale: 0.94; }
74
+ .oc-glass-bar__item:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
75
+ /* The current item sits in a raised, lighter bubble. */
76
+ .oc-glass-bar__item:is([aria-current="page"], [aria-pressed="true"], :has(> input:checked)) {
77
+ color: light-dark(var(--oc-primary), var(--color-white-50));
78
+ background: light-dark(oklch(100% 0 0deg / 0.62), oklch(100% 0 0deg / 0.24));
79
+ box-shadow: inset 0 1px 1px oklch(100% 0 0deg / 0.85), 0 6px 14px -8px oklch(0% 0 0deg / 0.4);
80
+ }
81
+ .oc-glass-bar__item > input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
82
+ .oc-glass-bar__item:has(> input:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
83
+
84
+ /* Icons only, and a separate round button next to the bar (search, compose). */
85
+ .oc-glass-bar--icons .oc-glass-bar__item { min-inline-size: 0; padding: 0.6rem; }
86
+ .oc-glass-bar--round { padding: 0.35rem; border-radius: 999px; }
87
+ .oc-glass-bar--round .oc-glass-bar__item { min-inline-size: 0; padding: 0.6rem; }
88
+ .oc-glass-bar__sep { align-self: stretch; inline-size: 1px; margin: 0.4rem 0.15rem; background: var(--oc-liquid-edge); }
89
+
90
+ /* Less transparency asked for, or no backdrop-filter: a solid bar that still reads as one. */
91
+ @media (prefers-reduced-transparency: reduce) {
92
+ .oc-glass--liquid, .oc-glass-bar { --oc-liquid-tint: var(--oc-card); backdrop-filter: none; }
93
+ }
94
+ @supports not (backdrop-filter: blur(1px)) {
95
+ .oc-glass--liquid, .oc-glass-bar { --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.85), oklch(25% 0.02 260deg / 0.9)); }
96
+ }
97
+ @media (prefers-reduced-motion: reduce) {
98
+ .oc-glass-bar__item { transition: none; }
99
+ }
@@ -0,0 +1,50 @@
1
+ /* File: packages/ui/src/parts/media-glass-options.css
2
+ Map: amount (--clear: more see-through, --frost: more frosted) -> light content on a colourful backdrop -> stage (moving colour blobs to show it off)
3
+ Extras for .oc-glass, .oc-glass--liquid and .oc-glass-bar (media-glass.css, media-glass-liquid.css). The selectors
4
+ carry two classes so they win over the base glass whatever order the parts load in. */
5
+
6
+ /* Amount of glass: --clear lets more of the backdrop through, --frost hides more of it. */
7
+ :is(.oc-glass, .oc-glass-bar).oc-glass--clear {
8
+ --oc-glass-sheet: light-dark(oklch(from var(--oc-background) l c h / 0.32), oklch(from var(--oc-background) l c h / 0.26));
9
+ --oc-glass-blur: 10px;
10
+ --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.05), oklch(100% 0 0deg / 0.03));
11
+ --oc-liquid-blur: 2px;
12
+ }
13
+ :is(.oc-glass, .oc-glass-bar).oc-glass--frost {
14
+ --oc-glass-sheet: light-dark(oklch(from var(--oc-background) l c h / 0.86), oklch(from var(--oc-background) l c h / 0.8));
15
+ --oc-glass-blur: 40px;
16
+ --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.38), oklch(100% 0 0deg / 0.12));
17
+ --oc-liquid-blur: 18px;
18
+ }
19
+
20
+
21
+ /* Light content: white icons and labels for glass over a dark or very colourful backdrop. */
22
+ .oc-glass-bar.oc-glass-bar--light .oc-glass-bar__item {
23
+ color: oklch(100% 0 0deg / 0.92);
24
+ text-shadow: 0 1px 2px oklch(0% 0 0deg / 0.25);
25
+ }
26
+ .oc-glass-bar.oc-glass-bar--light .oc-glass-bar__item:is([aria-current="page"], [aria-pressed="true"], :has(> input:checked)) {
27
+ color: var(--color-white-50);
28
+ background: oklch(100% 0 0deg / 0.2);
29
+ }
30
+ .oc-glass-bar.oc-glass-bar--light .oc-glass-bar__item:hover { background: oklch(100% 0 0deg / 0.12); }
31
+
32
+ /* Stage: round colour shapes that drift slowly, so the change through the glass shows. A background for a hero or a demo. */
33
+ .oc-glass-stage {
34
+ position: relative;
35
+ overflow: hidden;
36
+ background:
37
+ radial-gradient(circle at 22% 72%, var(--color-magenta-50) 0 16%, transparent 16.5%),
38
+ radial-gradient(circle at 82% 68%, var(--color-orange-50) 0 18%, transparent 18.5%),
39
+ radial-gradient(circle at 58% 18%, var(--color-skycyan-50) 0 13%, transparent 13.5%),
40
+ radial-gradient(60% 70% at 30% 30%, var(--color-magenta-60), transparent 70%),
41
+ linear-gradient(120deg, var(--color-purple-60), var(--color-violet-80));
42
+ background-size: 140% 140%;
43
+ animation: oc-glass-stage 18s ease-in-out infinite alternate;
44
+ }
45
+ @keyframes oc-glass-stage {
46
+ to { background-position: 100% 60%; }
47
+ }
48
+ @media (prefers-reduced-motion: reduce) {
49
+ .oc-glass-stage { animation: none; }
50
+ }
@@ -0,0 +1,46 @@
1
+ /* File: packages/ui/src/parts/media-lazy.css | Map: box (aspect ratio, shimmer placeholder or a tiny blurred preview) -> image fades in -> error -> lazy sections
2
+ Lazy loading. .oc-lazy: an image box that keeps its place, shows a shimmer (or --oc-lazy-preview, a tiny blurred
3
+ copy) until the photo is there and then fades it in (lazy.js adds .is-loaded). .oc-lazy-section: content the browser
4
+ only lays out and draws when it comes near the screen (content-visibility). */
5
+
6
+ .oc-lazy {
7
+ --oc-lazy-ratio: 16 / 10;
8
+ position: relative;
9
+ display: block;
10
+ margin: 0;
11
+ overflow: hidden;
12
+ aspect-ratio: var(--oc-lazy-ratio);
13
+ border-radius: var(--oc-radius);
14
+ background:
15
+ linear-gradient(100deg, transparent 30%, oklch(from var(--oc-background) l c h / 0.55) 50%, transparent 70%) -150% 0 / 60% 100% no-repeat,
16
+ var(--oc-muted);
17
+ animation: oc-lazy-shimmer 1.4s ease-in-out infinite;
18
+ }
19
+ /* A tiny blurred preview instead of the shimmer: style="--oc-lazy-preview: url(photo-20px.webp)". */
20
+ .oc-lazy[style*="--oc-lazy-preview"] { animation: none; background: var(--oc-muted); }
21
+ .oc-lazy[style*="--oc-lazy-preview"]::before { content: ""; position: absolute; inset: 0; background: var(--oc-lazy-preview) center / cover no-repeat; filter: blur(14px); scale: 1.15; }
22
+ .oc-lazy > img { position: relative; display: block; inline-size: 100%; block-size: 100%; object-fit: cover; opacity: 0; transition: opacity 0.45s ease; }
23
+ .oc-lazy.is-loaded { animation: none; background: none; }
24
+ .oc-lazy.is-loaded::before { display: none; }
25
+ .oc-lazy.is-loaded > img { opacity: 1; }
26
+ .oc-lazy.is-error { animation: none; }
27
+ .oc-lazy.is-error > img { opacity: 0; }
28
+ .oc-lazy > figcaption { position: absolute; inset: auto 0 0; padding: 0.4rem 0.6rem; font-size: 0.75rem; color: var(--color-white-50); background: linear-gradient(transparent, oklch(0% 0 0deg / 0.55)); }
29
+ /* Without the script (or when it is off) the photo shows as soon as it is there. */
30
+ @media (scripting: none) { .oc-lazy > img { opacity: 1; } }
31
+ @keyframes oc-lazy-shimmer { to { background-position: 250% 0, 0 0; } }
32
+
33
+ /* Sections far down a long page: laid out and drawn only when they come near the screen. */
34
+ .oc-lazy-section { content-visibility: auto; contain-intrinsic-size: auto var(--oc-lazy-size, 600px); }
35
+
36
+ @media (prefers-reduced-motion: reduce) {
37
+ .oc-lazy { animation: none; }
38
+ .oc-lazy > img { transition: none; }
39
+ }
40
+
41
+ /* In a table or a list: a small square thumbnail, and placeholder rows (.oc-lazy-row) while the next page loads. */
42
+ .oc-lazy.oc-lazy--thumb { --oc-lazy-ratio: 1; inline-size: 2.5rem; flex: none; border-radius: calc(var(--oc-radius) - 2px); }
43
+ .oc-lazy-row > :is(td, th) { padding-block: 0.6rem; }
44
+ .oc-lazy-row .oc-lazy-bar { display: block; block-size: 0.75rem; inline-size: var(--oc-lazy-bar, 70%); border-radius: 999px; background: var(--oc-muted); animation: oc-lazy-pulse 1.2s ease-in-out infinite alternate; }
45
+ @keyframes oc-lazy-pulse { to { opacity: 0.45; } }
46
+ @media (prefers-reduced-motion: reduce) { .oc-lazy-row .oc-lazy-bar { animation: none; } }