@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
@@ -1,4 +1,4 @@
1
- /* File: oc-theme.css — generated from tokens/semantic.json by @birdycom/omniacss 0.12.5. Do not edit. */
1
+ /* File: oc-theme.css — generated from tokens/semantic.json by @birdycom/omniacss 0.13.0. Do not edit. */
2
2
  /* Semantic theme tokens: every color is light-dark() of an Omnia palette token (oc-colors.css). */
3
3
  /* Same source as the desktop export: npx omniacss tokens --format json|dtcg|python */
4
4
  :root {
@@ -14,6 +14,7 @@
14
14
  --oc-foreground: light-dark(var(--color-duskblue-60), var(--color-frostblue-10));
15
15
  --oc-muted: light-dark(var(--color-frostblue-60), var(--color-shallowblue-50));
16
16
  --oc-muted-foreground: light-dark(var(--color-stormblue-40), var(--color-mistblue-70));
17
+ --oc-subtle-foreground: light-dark(var(--color-stormblue-10), var(--color-mistblue-95));
17
18
  --oc-card: light-dark(var(--color-white-50), var(--color-shallowblue-70));
18
19
  --oc-popover: light-dark(var(--color-white-50), var(--color-shallowblue-60));
19
20
  --oc-popover-foreground: var(--oc-foreground);
@@ -58,6 +59,7 @@
58
59
  --oc-transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1);
59
60
  --oc-font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Cantarell, "Helvetica Neue", sans-serif;
60
61
  --oc-font-mono: "Cascadia Code", "JetBrains Mono", Consolas, ui-monospace, monospace;
62
+ --oc-font-display: system-ui, -apple-system, "Segoe UI", Roboto, Cantarell, "Helvetica Neue", sans-serif;
61
63
 
62
64
  /* Shadows (x y blur spread; color + alpha per mode in --oc-shadow-<name>-color) */
63
65
  --oc-shadow-sm-color: light-dark(oklch(from var(--color-stormblue-50) l c h / 0.08), oklch(from var(--color-black-50) l c h / 0.4));
@@ -346,7 +348,8 @@ flex-direction: column;
346
348
  justify-content: center;
347
349
  gap: 8px;
348
350
  font-family: inherit;
349
- font-weight: 500;
351
+ /* --oc-btn-weight: a theme can make every button bolder (600). */
352
+ font-weight: var(--oc-btn-weight, 500);
350
353
  font-size: 0.875rem;
351
354
  line-height: 1.25rem;
352
355
  padding: 0.5rem 1rem;
@@ -481,19 +484,68 @@ flex-direction: column;
481
484
  }
482
485
 
483
486
  .oc-btn--lg {
484
- font-size: 1rem;
485
- padding: 0.625rem 1.5rem;
487
+ font-size: var(--oc-btn-lg-size, 1rem);
488
+ padding: var(--oc-btn-lg-padding, 0.625rem 1.5rem);
486
489
  border-radius: calc(var(--oc-radius) + 2px);
487
490
  }
488
491
 
492
+ /* Square, exactly as tall as a text button of the same size, so both line up in a toolbar. */
489
493
  .oc-btn--icon {
490
- padding: 0.5rem;
491
- aspect-ratio: 1;
494
+ --oc-btn-square: calc(2.25rem + 2px);
495
+ padding: 0;
496
+ inline-size: var(--oc-btn-square);
497
+ block-size: var(--oc-btn-square);
498
+ flex-shrink: 0;
499
+ }
500
+
501
+ .oc-btn--icon.oc-btn--xs { --oc-btn-square: calc(1.75rem + 2px); }
502
+ .oc-btn--icon.oc-btn--sm { --oc-btn-square: calc(2rem + 2px); }
503
+ .oc-btn--icon.oc-btn--lg { --oc-btn-square: calc(2.5rem + 2px); }
504
+
505
+ /* Webicons in a button: <iconify-icon> before (or after) the label, the same size as an inline SVG. */
506
+ .oc-btn > iconify-icon { font-size: 1.15em; flex-shrink: 0; }
507
+
508
+ /* Soft — tinted background in a tone, for row actions (edit, view, delete …) and quiet secondary actions.
509
+ --soft-primary | --soft-success | --soft-warning | --soft-destructive | --soft-info */
510
+ .oc-btn[class*="oc-btn--soft-"] {
511
+ background: light-dark(oklch(from var(--oc-btn-tone) l c h / 0.12), oklch(from var(--oc-btn-tone) l c h / 0.22));
512
+ color: var(--oc-btn-tone);
513
+ border-color: transparent;
514
+ }
515
+ .oc-btn[class*="oc-btn--soft-"]:hover {
516
+ background: light-dark(oklch(from var(--oc-btn-tone) l c h / 0.2), oklch(from var(--oc-btn-tone) l c h / 0.32));
517
+ opacity: 1;
518
+ }
519
+ .oc-btn--soft-primary { --oc-btn-tone: var(--oc-primary); }
520
+ .oc-btn--soft-success { --oc-btn-tone: var(--oc-success); }
521
+ .oc-btn--soft-warning { --oc-btn-tone: var(--oc-warning); }
522
+ .oc-btn--soft-destructive { --oc-btn-tone: var(--oc-destructive); }
523
+ .oc-btn--soft-info { --oc-btn-tone: var(--oc-info, light-dark(var(--color-deepskyblue-70), var(--color-skyblue-50))); }
524
+
525
+ /* Plain — only the icon: no surface, no padding; muted until hovered (menus, toolbars, inline actions). */
526
+ .oc-btn--plain {
527
+ padding: 0;
528
+ background: transparent;
529
+ border-color: transparent;
530
+ color: var(--oc-muted-foreground);
492
531
  }
532
+ .oc-btn--plain:hover,
533
+ .oc-btn--plain[aria-expanded="true"] { color: var(--oc-primary); opacity: 1; }
493
534
 
494
- .oc-btn--icon.oc-btn--xs { padding: 0.25rem; }
495
- .oc-btn--icon.oc-btn--sm { padding: 0.375rem; }
496
- .oc-btn--icon.oc-btn--lg { padding: 0.625rem; }
535
+ /* Loading — aria-busy="true": a spinner in front of the label (it replaces a leading icon); not clickable. */
536
+ .oc-btn[aria-busy="true"] { pointer-events: none; }
537
+ .oc-btn[aria-busy="true"]::before {
538
+ content: "";
539
+ inline-size: 1em;
540
+ block-size: 1em;
541
+ flex-shrink: 0;
542
+ border: 2px solid currentColor;
543
+ border-inline-end-color: transparent;
544
+ border-radius: 50%;
545
+ animation: oc-btn-spin 0.7s linear infinite;
546
+ }
547
+ .oc-btn[aria-busy="true"] > :is(svg, iconify-icon):first-child { display: none; }
548
+ @keyframes oc-btn-spin { to { rotate: 360deg; } }
497
549
 
498
550
  /* Button group */
499
551
  .oc-btn-group {
@@ -661,8 +713,10 @@ flex-direction: column;
661
713
  }
662
714
 
663
715
  .oc-field-error {
716
+ margin: 0;
664
717
  font-size: 0.8rem;
665
- color: var(--oc-destructive);
718
+ /* In dark mode lifted to at least OKLCH L 0.78, so the message stays readable on a dark card. */
719
+ color: light-dark(var(--oc-destructive), oklch(from var(--oc-destructive) max(l, 0.78) c h));
666
720
  }
667
721
 
668
722
  /* Field group — wraps multiple fields with consistent spacing */
@@ -675,17 +729,23 @@ flex-direction: column;
675
729
  grid-template-columns: 1fr 1fr;
676
730
  }
677
731
 
678
- /* Text input */
732
+ /* Narrow screens: the fields of a row stack. */
733
+ @media (max-width: 560px) {
734
+ .oc-field-group--row { grid-template-columns: 1fr; }
735
+ }
736
+
737
+ /* Text input. A theme or a form variant (.oc-form--filled) can set the field tokens: --oc-input-bg, --oc-input-border,
738
+ --oc-input-padding, --oc-input-radius, --oc-input-size, --oc-ring-width and --oc-ring-alpha (focus ring). */
679
739
  .oc-input {
680
740
  display: block;
681
741
  width: 100%;
682
742
  font-family: inherit;
683
- font-size: 0.875rem;
743
+ font-size: var(--oc-input-size, 0.875rem);
684
744
  line-height: 1.5;
685
- padding: 0.5rem 0.75rem;
686
- border: 1px solid var(--oc-input);
687
- border-radius: var(--oc-radius);
688
- background: transparent;
745
+ padding: var(--oc-input-padding, 0.5rem 0.75rem);
746
+ border: 1px solid var(--oc-input-border, var(--oc-input));
747
+ border-radius: var(--oc-input-radius, var(--oc-radius));
748
+ background: var(--oc-input-bg, transparent);
689
749
  color: var(--oc-foreground);
690
750
  transition: border-color var(--oc-transition-fast), box-shadow var(--oc-transition-fast);
691
751
  outline: none;
@@ -697,7 +757,7 @@ flex-direction: column;
697
757
 
698
758
  .oc-input:focus {
699
759
  border-color: var(--oc-ring);
700
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--oc-ring) 25%, transparent);
760
+ box-shadow: 0 0 0 var(--oc-ring-width, 2px) color-mix(in srgb, var(--oc-ring) var(--oc-ring-alpha, 25%), transparent);
701
761
  }
702
762
 
703
763
  .oc-input:disabled {
@@ -713,12 +773,13 @@ flex-direction: column;
713
773
 
714
774
  .oc-input[aria-invalid="true"]:focus,
715
775
  .oc-input--error:focus {
716
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--oc-destructive) 25%, transparent);
776
+ box-shadow: 0 0 0 var(--oc-ring-width, 2px) color-mix(in srgb, var(--oc-destructive) var(--oc-ring-alpha, 25%), transparent);
717
777
  }
718
778
 
719
779
  /* Textarea — same base as input */
720
- textarea.oc-input {
721
- min-height: 80px;
780
+ textarea.oc-input,
781
+ .oc-textarea {
782
+ min-height: var(--oc-textarea-min, 80px);
722
783
  resize: vertical;
723
784
  }
724
785
 
@@ -762,12 +823,13 @@ textarea.oc-input {
762
823
  display: block;
763
824
  width: 100%;
764
825
  font-family: inherit;
765
- font-size: 0.875rem;
826
+ font-size: var(--oc-input-size, 0.875rem);
766
827
  line-height: 1.5;
767
- padding: 0.5rem 2rem 0.5rem 0.75rem;
768
- border: 1px solid var(--oc-input);
769
- border-radius: var(--oc-radius);
770
- background: transparent;
828
+ padding: var(--oc-input-padding, 0.5rem 0.75rem);
829
+ padding-inline-end: 2rem;
830
+ border: 1px solid var(--oc-input-border, var(--oc-input));
831
+ border-radius: var(--oc-input-radius, var(--oc-radius));
832
+ background-color: var(--oc-input-bg, transparent);
771
833
  color: var(--oc-foreground);
772
834
  appearance: none;
773
835
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%2371717a' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
@@ -781,8 +843,9 @@ textarea.oc-input {
781
843
 
782
844
  .oc-select:focus {
783
845
  border-color: var(--oc-ring);
784
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--oc-ring) 25%, transparent);
846
+ box-shadow: 0 0 0 var(--oc-ring-width, 2px) color-mix(in srgb, var(--oc-ring) var(--oc-ring-alpha, 25%), transparent);
785
847
  }
848
+ .oc-select[aria-invalid="true"] { border-color: var(--oc-destructive); }
786
849
 
787
850
  /* Select option dark theme — prevents white background in dropdowns */
788
851
  .oc-select option {
@@ -1550,7 +1613,7 @@ textarea.oc-input {
1550
1613
  border: none;
1551
1614
  height: 1px;
1552
1615
  background: var(--oc-border);
1553
- margin: 16px 0;
1616
+ margin: var(--oc-separator-space, 16px) 0;
1554
1617
  }
1555
1618
 
1556
1619
  .oc-separator--vertical {
@@ -1633,6 +1696,8 @@ textarea.oc-input {
1633
1696
  /* Vertical tabs */
1634
1697
  .oc-tab-list--vertical {
1635
1698
  flex-direction: column;
1699
+ /* Every tab as wide as the list, so the active line on the right stays on one vertical line. */
1700
+ align-items: stretch;
1636
1701
  border-bottom: none;
1637
1702
  border-right: 1px solid var(--oc-border);
1638
1703
  }
@@ -1645,7 +1710,9 @@ textarea.oc-input {
1645
1710
  text-align: left;
1646
1711
  }
1647
1712
 
1648
- .oc-tab-list--vertical .oc-tab-trigger.is-active {
1713
+ .oc-tab-list--vertical .oc-tab-trigger.is-active,
1714
+ .oc-tab-list--vertical .oc-tab-trigger[aria-selected="true"] {
1715
+ border-bottom-color: transparent;
1649
1716
  border-right-color: var(--oc-foreground);
1650
1717
  }
1651
1718
 
@@ -1848,6 +1915,12 @@ textarea.oc-input {
1848
1915
  border-radius: calc(var(--oc-radius) + 2px);
1849
1916
  }
1850
1917
  .oc-table-wrap > .oc-table { min-inline-size: 32rem; }
1918
+ /* The rounded frame draws the outer border; the cells keep only the inner lines, so nothing is cut at the corners. */
1919
+ .oc-table-wrap:has(> .oc-table) { border: 1px solid var(--oc-border); }
1920
+ .oc-table-wrap > .oc-table :is(th, td):first-child { border-inline-start: 0; }
1921
+ .oc-table-wrap > .oc-table :is(th, td):last-child { border-inline-end: 0; }
1922
+ .oc-table-wrap > .oc-table > :first-child > tr:first-child > * { border-block-start: 0; }
1923
+ .oc-table-wrap > .oc-table > :last-child > tr:last-child > * { border-block-end: 0; }
1851
1924
 
1852
1925
  /* ===================================================================
1853
1926
  Shared docs utility table styles (dark theme)
@@ -1984,6 +2057,42 @@ a.oc-breadcrumb-item:hover {
1984
2057
  font-weight: 500;
1985
2058
  }
1986
2059
 
2060
+ /* Icons: a webicon or SVG before the label of any item (the link or the item itself). */
2061
+ .oc-breadcrumb-item,
2062
+ .oc-breadcrumb-item a { gap: 0.375rem; }
2063
+ .oc-breadcrumb-item a { display: inline-flex; align-items: center; }
2064
+ .oc-breadcrumb-item :is(svg, iconify-icon) { font-size: 1.1em; inline-size: 1.1em; block-size: 1.1em; flex-shrink: 0; }
2065
+
2066
+ /* --chevron: a › chevron between the items instead of "/". */
2067
+ .oc-breadcrumb--chevron .oc-breadcrumb-item + .oc-breadcrumb-item::before {
2068
+ content: "";
2069
+ inline-size: 1em;
2070
+ block-size: 1em;
2071
+ margin: 0 0.25rem;
2072
+ background: currentColor;
2073
+ color: var(--oc-muted-foreground);
2074
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.59 16.58L13.17 12L8.59 7.41L10 6l6 6l-6 6z'/%3E%3C/svg%3E") center / contain no-repeat;
2075
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.59 16.58L13.17 12L8.59 7.41L10 6l6 6l-6 6z'/%3E%3C/svg%3E") center / contain no-repeat;
2076
+ }
2077
+
2078
+ /* Collapsed middle of a long trail: a "…" button (opens a menu with the hidden levels). */
2079
+ .oc-breadcrumb-ellipsis {
2080
+ display: inline-flex;
2081
+ align-items: center;
2082
+ justify-content: center;
2083
+ inline-size: 1.75rem;
2084
+ block-size: 1.5rem;
2085
+ padding: 0;
2086
+ border: 0;
2087
+ border-radius: calc(var(--oc-radius) - 2px);
2088
+ background: transparent;
2089
+ color: var(--oc-muted-foreground);
2090
+ font: inherit;
2091
+ cursor: pointer;
2092
+ }
2093
+ .oc-breadcrumb-ellipsis:hover { background: var(--oc-accent); color: var(--oc-foreground); }
2094
+ .oc-breadcrumb-ellipsis:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
2095
+
1987
2096
  /* ===================================================================
1988
2097
  PROGRESS BAR
1989
2098
  =================================================================== */
@@ -3670,6 +3779,9 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3670
3779
  background: var(--oc-gradient-brand);
3671
3780
  -webkit-background-clip: text;
3672
3781
  background-clip: text;
3782
+ /* The gradient only paints the box: keep italic or tight last letters inside it without moving the text. */
3783
+ padding-inline-end: 0.08em;
3784
+ margin-inline-end: -0.08em;
3673
3785
  color: transparent;
3674
3786
  }
3675
3787
  .oc-text-accent { background-image: linear-gradient(100deg, var(--oc-accent-from), var(--oc-accent-to)); }
@@ -3691,7 +3803,8 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3691
3803
  /* border-box: the gradient also runs under the transparent 1px border of .oc-btn; from the padding box it would
3692
3804
  repeat in that border and show a thin stripe of the other end's color on both sides. */
3693
3805
  background: linear-gradient(100deg, var(--oc-accent-from), var(--oc-accent-to)) border-box;
3694
- color: var(--color-white-50);
3806
+ /* --oc-btn-gradient-ink: dark text on a light accent (set it with the accent). */
3807
+ color: var(--oc-btn-gradient-ink, var(--color-white-50));
3695
3808
  box-shadow: 0 10px 30px -10px var(--oc-accent-to);
3696
3809
  transition: transform 0.2s var(--oc-ease-out), box-shadow 0.2s;
3697
3810
  }
@@ -3718,7 +3831,8 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3718
3831
 
3719
3832
  /* Backdrop: slowly drifting color blobs + fine grid behind the page.
3720
3833
  <div class="oc-backdrop" aria-hidden="true"><i class="oc-backdrop__blob"></i><i class="oc-backdrop__grid"></i></div>
3721
- .oc-backdrop--local keeps it inside a positioned section instead of the viewport. */
3834
+ .oc-backdrop--local keeps it inside a positioned section instead of the viewport.
3835
+ A theme sets the colors: --oc-backdrop-1, -2, -3 (the blobs), --oc-backdrop-3-opacity and --oc-backdrop-grid (the lines). */
3722
3836
  .oc-backdrop {
3723
3837
  position: fixed;
3724
3838
  inset: 0;
@@ -3739,24 +3853,27 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3739
3853
  opacity: var(--oc-backdrop-opacity, 0.35);
3740
3854
  animation: oc-drift 24s ease-in-out infinite alternate;
3741
3855
  }
3742
- .oc-backdrop::before { background: var(--color-blue-40); inset-block-start: -18rem; inset-inline-start: -12rem; }
3743
- .oc-backdrop::after { background: var(--color-purple-30); inset-block-start: 10rem; inset-inline-end: -16rem; animation-delay: -8s; }
3744
- .oc-backdrop > .oc-backdrop__blob { background: var(--color-teal-40); inset-block-end: -20rem; inset-inline-start: 30%; animation-delay: -16s; opacity: calc(var(--oc-backdrop-opacity, 0.35) * 0.6); }
3856
+ .oc-backdrop::before { background: var(--oc-backdrop-1, var(--color-blue-40)); inset-block-start: -18rem; inset-inline-start: -12rem; }
3857
+ .oc-backdrop::after { background: var(--oc-backdrop-2, var(--color-purple-30)); inset-block-start: 10rem; inset-inline-end: -16rem; animation-delay: -8s; }
3858
+ .oc-backdrop > .oc-backdrop__blob { background: var(--oc-backdrop-3, var(--color-teal-40)); inset-block-end: -20rem; inset-inline-start: 30%; animation-delay: -16s; opacity: var(--oc-backdrop-3-opacity, calc(var(--oc-backdrop-opacity, 0.35) * 0.6)); }
3745
3859
  .oc-backdrop > .oc-backdrop__grid {
3746
3860
  position: absolute;
3747
3861
  inset: 0;
3748
3862
  background-image:
3749
- linear-gradient(oklch(from var(--oc-foreground) l c h / 0.05) 1px, transparent 1px),
3750
- linear-gradient(90deg, oklch(from var(--oc-foreground) l c h / 0.05) 1px, transparent 1px);
3863
+ linear-gradient(var(--oc-backdrop-grid, oklch(from var(--oc-foreground) l c h / 0.05)) 1px, transparent 1px),
3864
+ linear-gradient(90deg, var(--oc-backdrop-grid, oklch(from var(--oc-foreground) l c h / 0.05)) 1px, transparent 1px);
3751
3865
  background-size: 64px 64px;
3752
3866
  mask-image: radial-gradient(ellipse at 50% 0%, black 20%, transparent 70%);
3753
3867
  }
3754
3868
  @keyframes oc-drift { to { transform: translate(6rem, 4rem) scale(1.15); } }
3755
3869
 
3756
3870
  /* Container, section spacing and display headings */
3757
- .oc-container { inline-size: min(100% - 2.5rem, var(--oc-container, 1200px)); margin-inline: auto; }
3871
+ /* --oc-container is the width, --oc-container-gutter the room on each side on a narrower screen (1.25rem). */
3872
+ .oc-container { inline-size: min(100% - 2 * var(--oc-container-gutter, 1.25rem), var(--oc-container, 1200px)); margin-inline: auto; }
3758
3873
  .oc-section-pad { padding-block: clamp(4rem, 9vw, 7rem); }
3759
3874
  .oc-section-pad--tight { padding-block: clamp(2rem, 5vw, 3.5rem); }
3875
+ /* The hero of a home page: a little less room on top and below than a section. */
3876
+ .oc-section-pad--hero { padding-block: clamp(3.5rem, 9vw, 7rem) clamp(3rem, 6vw, 5rem); }
3760
3877
  .oc-display {
3761
3878
  margin: 0 0 1.5rem;
3762
3879
  font-size: clamp(2.6rem, 5.6vw, 4.6rem);
@@ -3766,20 +3883,67 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3766
3883
  text-wrap: balance;
3767
3884
  }
3768
3885
  .oc-display--md { font-size: clamp(1.9rem, 4vw, 3rem); }
3886
+ .oc-display--sm { font-size: clamp(1.5rem, 3vw, 2.1rem); }
3769
3887
  .oc-lead { margin: 0 0 1rem; color: var(--oc-muted-foreground); font-size: 1.15rem; line-height: 1.65; max-inline-size: 42rem; text-wrap: pretty; }
3770
- .oc-split { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr)); gap: 3rem; align-items: center; }
3771
-
3772
- /* Code window: editor frame with traffic lights and a file name (always dark, like an editor) */
3888
+ /* Larger and narrower, for a home hero. */
3889
+ .oc-lead--lg { font-size: 1.2rem; max-inline-size: 36rem; }
3890
+ .oc-split { display: grid; grid-template-columns: var(--oc-split-cols, repeat(auto-fit, minmax(min(100%, 24rem), 1fr))); gap: var(--oc-split-gap, 3rem); align-items: center; }
3891
+ /* Ratio presets (one column up to 960px): --lead makes the text column wider, --media the image column.
3892
+ Your own ratio: set --oc-split-cols in a class (and to 1fr in a media query for small screens). */
3893
+ .oc-split--lead { --oc-split-cols: 1.25fr 1fr; }
3894
+ .oc-split--media { --oc-split-cols: 1fr 1.15fr; }
3895
+ .oc-split__media { display: grid; place-items: center; }
3896
+ /* Columns aligned at the top (a fact list next to a long text). */
3897
+ .oc-split--top { align-items: start; }
3898
+ /* --oc-split-cols-wide: your own ratio that only applies above 960px, so the split still stacks on a phone. */
3899
+ @media (min-width: 961px) {
3900
+ .oc-split { grid-template-columns: var(--oc-split-cols-wide, var(--oc-split-cols, repeat(auto-fit, minmax(min(100%, 24rem), 1fr)))); }
3901
+ }
3902
+ @media (max-width: 960px) {
3903
+ .oc-split--lead, .oc-split--media { --oc-split-cols: 1fr; }
3904
+ /* The image (a product mark, a screenshot) comes first on a small screen, aligned at the start, and an xl tile shrinks. */
3905
+ .oc-split__media { order: -1; justify-items: start; }
3906
+ .oc-split__media .oc-icon-tile--xl { --oc-tile-size: 7rem; }
3907
+ }
3908
+
3909
+ /* Code window: editor frame with traffic lights and a file name. It follows the page theme: a light frame and light
3910
+ syntax colors in light mode, the Omnia Dark editor colors in dark mode. --dark keeps it dark on a light page (the
3911
+ hero blocks, where it reads as an editor screenshot). */
3773
3912
  .oc-code-window {
3774
3913
  position: relative;
3775
3914
  border: 1px solid var(--oc-border-strong);
3776
3915
  border-radius: var(--oc-radius-lg);
3777
- background: oklch(from var(--color-shallowblue-60) l c h / 0.9);
3916
+ background: light-dark(var(--oc-card), oklch(from var(--color-shallowblue-60) l c h / 0.9));
3778
3917
  box-shadow: 0 40px 80px -30px oklch(from var(--color-black-50) l c h / 0.6);
3779
3918
  backdrop-filter: blur(12px);
3780
3919
  overflow: hidden;
3781
- color-scheme: dark;
3782
- }
3920
+ /* Syntax colors (oc-code-syntax.css) for both themes; the code sits on the window surface. */
3921
+ --oc-syntax-bg: transparent;
3922
+ --oc-syntax-bg-preview: transparent;
3923
+ --oc-syntax-fg: light-dark(var(--color-shallowblue-60), var(--color-silver-60));
3924
+ --oc-syntax-comment: light-dark(var(--color-gray-60), var(--color-purple-2));
3925
+ --oc-syntax-string: light-dark(var(--color-green-80), var(--color-green-2));
3926
+ --oc-syntax-keyword: light-dark(var(--color-purple-70), var(--color-purple-2));
3927
+ --oc-syntax-variable: light-dark(var(--color-skycyan-90), var(--color-skycyan-30));
3928
+ --oc-syntax-function: light-dark(var(--color-skycyan-90), var(--color-skycyan-60));
3929
+ --oc-syntax-number: light-dark(var(--color-peru-80), var(--color-peru-40));
3930
+ --oc-syntax-operator: light-dark(var(--color-red-70), var(--color-red-50));
3931
+ --oc-syntax-constant: light-dark(var(--color-skycyan-90), var(--color-skycyan-70));
3932
+ --oc-syntax-storage: light-dark(var(--color-green-80), var(--color-green-2));
3933
+ --oc-syntax-tag: light-dark(var(--color-brown-70), var(--color-brown-10));
3934
+ --oc-syntax-attr: light-dark(var(--color-skycyan-90), var(--color-skycyan-70));
3935
+ --oc-syntax-prop: light-dark(var(--color-green-80), var(--color-green-2));
3936
+ --oc-syntax-value: light-dark(var(--color-teal-80), var(--color-teal-10));
3937
+ --oc-syntax-cls: light-dark(var(--color-darkgoldenrod-80), var(--color-wheat-70));
3938
+ --oc-syntax-id: light-dark(var(--color-skycyan-90), var(--color-skycyan-70));
3939
+ --oc-syntax-unit: light-dark(var(--color-peru-80), var(--color-peru-40));
3940
+ --oc-syntax-key: light-dark(var(--color-brown-70), var(--color-brown-10));
3941
+ --oc-code-block-line-number-color: light-dark(var(--color-gray-40), oklch(from var(--color-white-2) l c h / 0.25));
3942
+ }
3943
+ .oc-code-window--dark { color-scheme: dark; box-shadow: 0 40px 80px -30px oklch(from var(--color-black-50) l c h / 0.6); }
3944
+ .oc-code-window:not(.oc-code-window--dark) { box-shadow: 0 24px 60px -30px oklch(from var(--color-black-50) l c h / 0.25); }
3945
+ /* The code block inside follows the window instead of its own always-dark theme. */
3946
+ .oc-code-window :is(.oc-code-block, pre.code-preview) { color-scheme: inherit; }
3783
3947
  .oc-code-window__bar { display: flex; align-items: center; gap: 0.45rem; padding: 0.8rem 1rem; border-block-end: 1px solid var(--oc-border); }
3784
3948
  .oc-code-window__bar > i { inline-size: 0.7rem; aspect-ratio: 1; border-radius: 50%; background: var(--color-red-50); }
3785
3949
  .oc-code-window__bar > i:nth-child(2) { background: var(--color-orange-60); }
@@ -3828,7 +3992,12 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3828
3992
  @keyframes oc-float { 50% { translate: 0 -12px; } }
3829
3993
 
3830
3994
  /* Product / feature card with hover lift and accent glow */
3831
- .oc-card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--oc-card-min, 17rem)), 1fr)); gap: 1.25rem; }
3995
+ .oc-card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--oc-card-min, 17rem)), 1fr)); gap: var(--oc-card-gap, 1.25rem); }
3996
+ /* Wide cards (projects, case studies): from 32rem, three in a row on a wide screen. */
3997
+ .oc-card-grid--wide { --oc-card-min: 32rem; --oc-card-gap: 1.5rem; }
3998
+ /* --fit: the cards stretch over the full width when there are fewer than fit in a row (auto-fit instead of auto-fill). */
3999
+ .oc-card-grid--fit { grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--oc-card-min, 17rem)), 1fr)); }
4000
+ @media (min-width: 1280px) { .oc-card-grid--wide { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
3832
4001
  .oc-product-card {
3833
4002
  position: relative;
3834
4003
  display: flex;
@@ -3842,7 +4011,7 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3842
4011
  var(--oc-surface);
3843
4012
  color: var(--oc-foreground);
3844
4013
  text-decoration: none;
3845
- transition: transform 0.35s var(--oc-ease-out), border-color 0.35s, --oc-glow 0.35s;
4014
+ transition: translate 0.35s var(--oc-ease-out), border-color 0.35s, --oc-glow 0.35s;
3846
4015
  }
3847
4016
  @property --oc-glow {
3848
4017
  syntax: "<number>";
@@ -3851,7 +4020,8 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3851
4020
  }
3852
4021
  .oc-product-card:hover {
3853
4022
  --oc-glow: 0.22;
3854
- transform: translateY(-6px);
4023
+ /* The lift uses translate; a fly-in (.oc-reveal--fly) animates the transform, so both keep working. */
4024
+ translate: 0 calc(-1 * var(--oc-hover-lift, 6px));
3855
4025
  border-color: oklch(from var(--oc-accent-from) l c h / 0.55);
3856
4026
  }
3857
4027
  .oc-product-card__top { display: flex; justify-content: space-between; align-items: flex-start; margin-block-end: 0.75rem; }
@@ -3907,9 +4077,17 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3907
4077
  }
3908
4078
  .oc-stat-strip__item { padding: 2rem 1.5rem; border-inline-start: 1px solid var(--oc-border); }
3909
4079
  .oc-stat-strip__item:first-child { border-inline-start: 0; }
3910
- .oc-stat-strip__value { font-size: clamp(2.2rem, 4vw, 3rem); font-weight: 700; line-height: 1.1; }
4080
+ .oc-stat-strip__value { font-family: var(--oc-font-display, inherit); font-size: clamp(2.2rem, 4vw, 3rem); font-weight: 700; line-height: 1.1; }
3911
4081
  .oc-stat-strip__suffix { color: var(--oc-brand-1); }
3912
4082
  .oc-stat-strip__label { display: block; color: var(--oc-muted-foreground); font-size: 0.92rem; }
4083
+ /* Plain: no lines and no background of its own, for a strip inside an .oc-band (full width) or a card. */
4084
+ .oc-stat-strip--plain { border-block: 0; background: none; }
4085
+ /* Four figures on a narrower screen: two rows of two instead of three and one. */
4086
+ @media (max-width: 960px) {
4087
+ .oc-stat-strip:has(> .oc-stat-strip__item:nth-child(4):last-child) { grid-template-columns: 1fr 1fr; }
4088
+ .oc-stat-strip:has(> .oc-stat-strip__item:nth-child(4):last-child) > .oc-stat-strip__item:nth-child(3) { border-inline-start: 0; }
4089
+ .oc-stat-strip:has(> .oc-stat-strip__item:nth-child(4):last-child) > .oc-stat-strip__item:nth-child(n + 3) { border-block-start: 1px solid var(--oc-border); }
4090
+ }
3913
4091
 
3914
4092
  /* Spotlight: large highlighted panel with copy + module list */
3915
4093
  .oc-spotlight {
@@ -3927,8 +4105,12 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3927
4105
  }
3928
4106
  .oc-spotlight__title { display: flex; align-items: center; gap: 1rem; margin: 1.25rem 0 0.5rem; font-size: clamp(2.2rem, 5vw, 3.5rem); line-height: 1.1; }
3929
4107
  .oc-spotlight__tagline { color: light-dark(var(--oc-accent-from), oklch(from var(--oc-accent-from) max(l, 0.72) c h)); font-size: 1.2rem; font-weight: 600; }
4108
+ /* --oc-spotlight-cols: your own ratio above 960px (copy | modules); on a smaller screen the spotlight stacks. */
4109
+ @media (min-width: 961px) {
4110
+ .oc-spotlight { grid-template-columns: var(--oc-spotlight-cols, repeat(auto-fit, minmax(min(100%, 22rem), 1fr))); }
4111
+ }
3930
4112
  .oc-spotlight__modules { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr)); gap: 0.75rem; margin: 0; padding: 0; list-style: none; }
3931
- .oc-spotlight__modules > li { display: flex; align-items: center; gap: 0.75rem; padding: 0.9rem; border: 1px solid var(--oc-border); border-radius: var(--oc-radius-md); background: var(--oc-surface); font-size: 0.95rem; }
4113
+ .oc-spotlight__modules > li { display: flex; align-items: center; gap: 0.75rem; padding: 0.9rem; border: 1px solid var(--oc-border); border-radius: var(--oc-radius-md); background: var(--oc-spotlight-module-bg, var(--oc-surface)); font-size: 0.95rem; }
3932
4114
 
3933
4115
  /* Steps: numbered process with a gradient tick (numbers come from a CSS counter) */
3934
4116
  .oc-steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: 1.25rem; margin: 0; padding: 0; list-style: none; counter-reset: oc-step; }
@@ -3961,6 +4143,9 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3961
4143
  /* Tag list (tech stack) and check list */
3962
4144
  .oc-tag-list { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 0; padding: 0; list-style: none; }
3963
4145
  .oc-tag-list > li { padding: 0.55rem 1.1rem; border: 1px solid oklch(from var(--oc-accent-from) l c h / 0.35); border-radius: 999px; background: oklch(from var(--oc-accent-from) l c h / 0.1); font-family: var(--oc-font-mono); font-size: 0.88rem; }
4146
+ /* Neutral chips (no accent color) and large chips in the display font, e.g. a strip of brand names. */
4147
+ .oc-tag-list--neutral > li { border-color: var(--oc-border); background: var(--oc-surface); color: var(--oc-foreground); }
4148
+ .oc-tag-list--lg > li { padding: 0.45rem 1rem; font-family: var(--oc-font-display, inherit); font-size: 1rem; font-weight: 700; letter-spacing: 0.02em; }
3964
4149
  .oc-check-list { display: grid; gap: 0.5rem; margin: 1.1rem 0 0; padding: 0; list-style: none; font-size: 0.93rem; }
3965
4150
  .oc-check-list > li { display: flex; gap: 0.5rem; align-items: baseline; }
3966
4151
  .oc-check-list > li::before { content: "✓"; color: var(--oc-brand-1); font-weight: 700; }
@@ -3995,6 +4180,8 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3995
4180
  .oc-site-header__nav a:not(.oc-btn):hover,
3996
4181
  .oc-site-header__nav a[aria-current="page"] { color: var(--oc-foreground); }
3997
4182
  .oc-site-header__toggle { display: none; padding: 0.5rem 0.7rem; border: 1px solid var(--oc-border); border-radius: var(--oc-radius-md); background: var(--oc-surface); color: var(--oc-foreground); cursor: pointer; }
4183
+ /* The icon in the menu button is a block, so the button is as tall as a line of text (no inline gap under it). */
4184
+ .oc-site-header__toggle :is(svg, img, iconify-icon, .oc-icon) { display: block; }
3998
4185
  @media (min-width: 861px) {
3999
4186
  /* Desktop: the popover nav is always visible in the header bar. */
4000
4187
  .oc-site-header__nav[popover] { display: flex; }
@@ -4019,6 +4206,8 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
4019
4206
  backdrop-filter: blur(16px);
4020
4207
  }
4021
4208
  .oc-site-header__nav[popover] a:not(.oc-btn) { padding: 0.75rem 0; font-size: 1.1rem; }
4209
+ /* The call to action (.oc-site-header__cta, a button) gets room above it in the open menu. */
4210
+ .oc-site-header__cta { margin-block-start: 0.75rem; }
4022
4211
  }
4023
4212
 
4024
4213
  /* Site footer: brand column + link columns + bottom bar */
@@ -4026,21 +4215,33 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
4026
4215
  margin-block-start: 4rem;
4027
4216
  padding-block-start: 4rem;
4028
4217
  border-block-start: 1px solid var(--oc-border);
4029
- background: linear-gradient(to bottom, transparent, var(--oc-surface-strong));
4218
+ background: var(--oc-footer-bg, linear-gradient(to bottom, transparent, var(--oc-surface-strong)));
4030
4219
  color: var(--oc-muted-foreground);
4031
4220
  font-size: 0.95rem;
4221
+ container-type: inline-size;
4032
4222
  }
4033
- .oc-site-footer__grid { display: grid; grid-template-columns: 1.6fr repeat(3, 1fr); gap: 2.5rem; }
4223
+ /* Columns: --oc-footer-cols (about + 3 link columns). Footer narrower than 66rem (about a 1060px screen):
4224
+ --oc-footer-cols-md columns (3) with the about column over the full width; narrower than 54rem: two columns.
4225
+ Container queries on the footer, so a footer in a narrow column or a preview follows its own width. */
4226
+ .oc-site-footer__grid { display: grid; grid-template-columns: var(--oc-footer-cols, 1.6fr repeat(3, 1fr)); gap: 2.5rem; }
4034
4227
  .oc-site-footer__about p { margin-block: 1rem 0; max-inline-size: 22rem; }
4035
4228
  .oc-site-footer__title { margin: 0 0 1rem; color: var(--oc-foreground); font-size: 0.95rem; font-weight: 700; }
4036
4229
  .oc-site-footer__list { display: flex; flex-direction: column; gap: 0.5rem; margin: 0; padding: 0; list-style: none; font-style: normal; }
4037
4230
  .oc-site-footer a { color: inherit; text-decoration: none; }
4038
4231
  .oc-site-footer a:hover { color: var(--oc-foreground); }
4039
- .oc-site-footer__bottom { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1rem; margin-block-start: 3rem; padding-block: 1.5rem; border-block-start: 1px solid var(--oc-border); font-size: 0.85rem; }
4040
- @media (max-width: 860px) {
4041
- .oc-site-footer__grid { grid-template-columns: 1fr 1fr; }
4232
+ .oc-site-footer__bottom { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1rem; margin-block-start: 3rem; padding-block: 1.5rem; border-block-start: 1px solid var(--oc-border); color: var(--oc-footer-bottom-color, inherit); font-size: 0.85rem; }
4233
+ /* A second title further down in a column (e.g. "Tools" under the projects). */
4234
+ .oc-site-footer__subtitle { margin-block-start: 1.75rem; }
4235
+ /* Legal links in the bottom bar (Terms · Privacy · Cookies) and the build number after them. */
4236
+ .oc-site-footer__legal { display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; margin: 0; padding: 0; list-style: none; }
4237
+ .oc-site-footer__build { opacity: 0.7; font-variant-numeric: tabular-nums; }
4238
+ @container (max-width: 66rem) {
4239
+ .oc-site-footer__grid { grid-template-columns: repeat(var(--oc-footer-cols-md, 3), minmax(0, 1fr)); }
4042
4240
  .oc-site-footer__about { grid-column: 1 / -1; }
4043
4241
  }
4242
+ @container (max-width: 54rem) {
4243
+ .oc-site-footer__grid { grid-template-columns: 1fr 1fr; }
4244
+ }
4044
4245
 
4045
4246
  /* Reveal on scroll: elements slide in when they enter the viewport (scroll-driven, no JS; visible without support) */
4046
4247
  @supports (animation-timeline: view()) {
@@ -4437,7 +4638,21 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
4437
4638
  .oc-slider:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 4px oklch(from var(--oc-ring) l c h / 0.28); }
4438
4639
  .oc-slider:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 4px oklch(from var(--oc-ring) l c h / 0.28); }
4439
4640
  .oc-slider:disabled { opacity: 0.5; cursor: not-allowed; }
4440
- .oc-slider--vertical { writing-mode: vertical-lr; direction: rtl; inline-size: var(--oc-slider-thumb); block-size: 10rem; }
4641
+ /* In a vertical writing mode the inline axis is vertical: inline-size is the height of the slider. */
4642
+ .oc-slider--vertical { writing-mode: vertical-lr; direction: rtl; inline-size: 10rem; block-size: var(--oc-slider-thumb); }
4643
+ /* Scale under a stepped slider: one mark per step (or per labelled value), centered under the thumb positions.
4644
+ The padding keeps the first and last mark under the thumb center at min and max. */
4645
+ .oc-slider-scale {
4646
+ display: flex;
4647
+ justify-content: space-between;
4648
+ padding-inline: calc(var(--oc-slider-thumb, 1.125rem) / 2);
4649
+ margin-block-start: 0.25rem;
4650
+ font-size: 0.6875rem;
4651
+ font-variant-numeric: tabular-nums;
4652
+ color: var(--oc-muted-foreground);
4653
+ }
4654
+ .oc-slider-scale > * { display: flex; flex-direction: column; align-items: center; gap: 0.125rem; inline-size: 0; }
4655
+ .oc-slider-scale > *::before { content: ""; inline-size: 1px; block-size: 0.375rem; background: var(--oc-border); }
4441
4656
  .oc-slider--vertical::-webkit-slider-runnable-track {
4442
4657
  background: linear-gradient(to top, var(--oc-primary) var(--oc-slider-progress), var(--oc-muted) var(--oc-slider-progress));
4443
4658
  }
@@ -5440,93 +5655,6 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5440
5655
  @keyframes oc-busy-in { from { opacity: 0; } }
5441
5656
  @media (prefers-reduced-motion: reduce) { .oc-busy-overlay[open], .oc-busy-overlay[open]::backdrop { animation-duration: 0s; } }
5442
5657
 
5443
- /* parts/app-column-menu.css */
5444
- /* File: packages/ui/src/parts/app-column-menu.css | Map: menu panel -> title + list -> item states -> footer -> CSS-only column hiding (scope) */
5445
- /* Column menu: a popover with one checkbox per table column (column visibility).
5446
- CSS-only hiding: wrap the menu and the table in .oc-column-menu-scope and give each checkbox
5447
- data-col="<column number>". An unchecked box hides that column (th and td) through :has(). */
5448
-
5449
- /* Panel look. Without the popover attribute it is a normal block (e.g. inside a sheet or sidebar). */
5450
- .oc-column-menu {
5451
- inline-size: 14rem;
5452
- max-inline-size: calc(100vw - 1rem);
5453
- padding: 0.375rem;
5454
- font-size: 0.875rem;
5455
- color: var(--oc-popover-foreground);
5456
- background: var(--oc-popover);
5457
- border: 1px solid var(--oc-border);
5458
- border-radius: var(--oc-radius);
5459
- box-shadow: 0 8px 24px oklch(from var(--color-black-60) l c h / 0.15);
5460
- }
5461
- /* As a popover: in the top layer, tethered to its trigger like the dropdown. */
5462
- .oc-column-menu[popover] {
5463
- position: fixed;
5464
- inset: auto;
5465
- margin: 4px 0 0;
5466
- position-area: block-end span-inline-start;
5467
- position-try-fallbacks: flip-block, flip-inline;
5468
- transition: opacity var(--oc-transition-fast), display var(--oc-transition-fast) allow-discrete, overlay var(--oc-transition-fast) allow-discrete;
5469
- }
5470
- .oc-column-menu[popover]:popover-open {
5471
- display: block;
5472
- @starting-style { opacity: 0; }
5473
- }
5474
-
5475
- /* Small heading above the list ("Columns"). */
5476
- .oc-column-menu-title {
5477
- margin: 0;
5478
- padding: 0.375rem 0.5rem 0.25rem;
5479
- font-size: 0.75rem;
5480
- font-weight: 600;
5481
- color: var(--oc-muted-foreground);
5482
- }
5483
-
5484
- /* The list is a <form>, so a native <button type="reset"> restores the default columns. */
5485
- .oc-column-menu-list { display: grid; gap: 1px; margin: 0; max-block-size: 18rem; overflow-y: auto; }
5486
-
5487
- /* One row: label around a checkbox and the column name. */
5488
- .oc-column-menu-item {
5489
- display: flex;
5490
- align-items: center;
5491
- gap: 0.5rem;
5492
- padding: 0.375rem 0.5rem;
5493
- border-radius: calc(var(--oc-radius) - 2px);
5494
- cursor: pointer;
5495
- user-select: none;
5496
- }
5497
- .oc-column-menu-item:hover { background: var(--oc-accent); }
5498
- .oc-column-menu-item:has(:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: -2px; }
5499
- .oc-column-menu-item input { margin: 0; accent-color: var(--oc-primary); }
5500
- /* Locked column (always visible): <input disabled checked>. */
5501
- .oc-column-menu-item:has(input:disabled) { color: var(--oc-muted-foreground); cursor: default; }
5502
- .oc-column-menu-item:has(input:disabled):hover { background: none; }
5503
-
5504
- /* Footer row with "Reset" (type="reset") or other small buttons. */
5505
- .oc-column-menu-footer {
5506
- display: flex;
5507
- justify-content: flex-end;
5508
- gap: 0.25rem;
5509
- margin-block-start: 0.375rem;
5510
- padding-block-start: 0.375rem;
5511
- border-block-start: 1px solid var(--oc-border);
5512
- }
5513
-
5514
- /* ---- CSS-only column hiding (columns 1 to 10) ---- */
5515
- .oc-column-menu-scope:has([data-col="1"]:not(:checked)) .oc-table :is(th, td):nth-child(1),
5516
- .oc-column-menu-scope:has([data-col="2"]:not(:checked)) .oc-table :is(th, td):nth-child(2),
5517
- .oc-column-menu-scope:has([data-col="3"]:not(:checked)) .oc-table :is(th, td):nth-child(3),
5518
- .oc-column-menu-scope:has([data-col="4"]:not(:checked)) .oc-table :is(th, td):nth-child(4),
5519
- .oc-column-menu-scope:has([data-col="5"]:not(:checked)) .oc-table :is(th, td):nth-child(5),
5520
- .oc-column-menu-scope:has([data-col="6"]:not(:checked)) .oc-table :is(th, td):nth-child(6),
5521
- .oc-column-menu-scope:has([data-col="7"]:not(:checked)) .oc-table :is(th, td):nth-child(7),
5522
- .oc-column-menu-scope:has([data-col="8"]:not(:checked)) .oc-table :is(th, td):nth-child(8),
5523
- .oc-column-menu-scope:has([data-col="9"]:not(:checked)) .oc-table :is(th, td):nth-child(9),
5524
- .oc-column-menu-scope:has([data-col="10"]:not(:checked)) .oc-table :is(th, td):nth-child(10) { display: none; }
5525
-
5526
- @media (prefers-reduced-motion: reduce) {
5527
- .oc-column-menu[popover] { transition: none; }
5528
- }
5529
-
5530
5658
  /* parts/app-confirm-dialog.css */
5531
5659
  /* File: packages/ui/src/parts/app-confirm-dialog.css | Map: confirm modifier (icon ring) -> close button -> typed confirm field -> confirm lock -> three-button footer -> narrow screens */
5532
5660
  /* Confirm dialog: a variant of the alert dialog (dialog.oc-modal + .oc-alert-dialog).
@@ -5664,9 +5792,213 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5664
5792
  }
5665
5793
  @media (prefers-reduced-motion: reduce) { .oc-cookie-consent { transition: none; } }
5666
5794
 
5795
+ /* parts/app-data-table-footer.css */
5796
+ /* File: packages/ui/src/parts/app-data-table-footer.css | Map: footer grid (count, rows per page, pager) -> page buttons -> narrow screens */
5797
+ /* Footer of the list table (app-data-table.css): the count on the left, rows per page in the middle, the pager on the right. */
5798
+
5799
+ /* Footer: count on the left, rows-per-page in the middle, pager on the right. */
5800
+ .oc-data-table-footer {
5801
+ display: grid;
5802
+ grid-template-columns: 1fr auto 1fr;
5803
+ align-items: center;
5804
+ gap: 0.75rem;
5805
+ }
5806
+ .oc-data-table-summary { margin: 0; font-size: 0.8125rem; color: var(--oc-muted-foreground); }
5807
+ .oc-data-table-per-page { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem; }
5808
+ .oc-data-table-per-page .oc-select { inline-size: auto; }
5809
+ .oc-data-table-pager { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.25rem; }
5810
+ .oc-data-table-page {
5811
+ min-inline-size: 2rem;
5812
+ padding: 0.25rem 0.45rem;
5813
+ font: inherit;
5814
+ font-size: 0.8125rem;
5815
+ color: var(--oc-foreground);
5816
+ background: var(--oc-card);
5817
+ border: 1px solid var(--oc-border);
5818
+ border-radius: calc(var(--oc-radius) - 2px);
5819
+ cursor: pointer;
5820
+ }
5821
+ .oc-data-table-page[aria-current="page"] {
5822
+ color: var(--oc-primary);
5823
+ background: oklch(from var(--oc-primary) l c h / 0.1);
5824
+ border-color: oklch(from var(--oc-primary) l c h / 0.35);
5825
+ }
5826
+ .oc-data-table-page:disabled { opacity: 0.4; cursor: default; }
5827
+ .oc-data-table-page:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
5828
+ .oc-data-table-ellipsis { padding-inline: 0.25rem; color: var(--oc-muted-foreground); }
5829
+
5830
+ @media (max-width: 40rem) {
5831
+ .oc-data-table-footer { grid-template-columns: 1fr; }
5832
+ .oc-data-table-pager { justify-content: flex-start; }
5833
+ }
5834
+
5835
+ /* parts/app-data-table-rich.css */
5836
+ /* File: packages/ui/src/parts/app-data-table-rich.css | Map: control height -> header (sort icons, columns) -> type tabs -> filter row -> name, truncate and count cells -> card fields */
5837
+ /* The rich relations list on top of app-data-table.css: tabs that filter a column, filter selects with an icon,
5838
+ rich cells and the label/value lines of the card view. Behaviour: data-oc-col-filter in src/data-table.js. */
5839
+
5840
+ /* Every control is 2rem high: search, filter selects, trash pills, icon buttons, rows per page and the pager. */
5841
+ .oc-data-table :is(.oc-data-table-toolbar, .oc-data-table-filters, .oc-data-table-footer) :is(.oc-input, .oc-select) { block-size: 2rem; padding-block: 0; font-size: 0.8125rem; }
5842
+ .oc-data-table .oc-data-table-page { min-block-size: 2rem; }
5843
+ .oc-data-table :is(.oc-data-table-toolbar, .oc-data-table-filters, thead) .oc-btn--icon { --oc-btn-square: 2rem; }
5844
+ .oc-data-table-toolbar .oc-filter-pill {
5845
+ display: inline-flex;
5846
+ align-items: center;
5847
+ block-size: 2rem;
5848
+ padding-inline: 0.625rem;
5849
+ border-radius: calc(var(--oc-radius) - 2px);
5850
+ }
5851
+
5852
+ /* Header: the sort mark only on the sorted column (and on hover); the actions header holds the column button. */
5853
+ /* The sort mark always keeps its 1rem, so sorting another column never moves the header: empty while unsorted,
5854
+ faint chevrons on hover, the accent sort icon on the sorted column. */
5855
+ :is(.oc-data-table, .oc-block-datatable) th:has(.oc-table-sort) { cursor: pointer; }
5856
+ :is(.oc-data-table, .oc-block-datatable) th .oc-table-sort::after { content: ""; flex-shrink: 0; inline-size: 1rem; block-size: 1rem; opacity: 1; }
5857
+ :is(.oc-data-table, .oc-block-datatable) th:not([aria-sort]):hover .oc-table-sort::after {
5858
+ background: var(--oc-muted-foreground);
5859
+ opacity: 0.6;
5860
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 18.17L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15M12 5.83L15.17 9l1.41-1.41L12 3L7.41 7.59L8.83 9z'/%3E%3C/svg%3E") center / contain no-repeat;
5861
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 18.17L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15M12 5.83L15.17 9l1.41-1.41L12 3L7.41 7.59L8.83 9z'/%3E%3C/svg%3E") center / contain no-repeat;
5862
+ }
5863
+ /* Sorted column: a bold label and a sort icon in the accent color. aria-sort on a header in the HTML
5864
+ is the column sorted when the page opens. */
5865
+ :is(.oc-data-table, .oc-block-datatable) th[aria-sort] .oc-table-sort { color: var(--oc-foreground); }
5866
+ :is(.oc-data-table, .oc-block-datatable) th[aria-sort] .oc-table-sort::after {
5867
+ content: "";
5868
+ inline-size: 1rem;
5869
+ block-size: 1rem;
5870
+ opacity: 1;
5871
+ background: var(--oc-primary);
5872
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 17h3l-4 4l-4-4h3V3h2M2 17h10v2H2M6 5v2H2V5m0 6h7v2H2z'/%3E%3C/svg%3E") center / contain no-repeat;
5873
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 17h3l-4 4l-4-4h3V3h2M2 17h10v2H2M6 5v2H2V5m0 6h7v2H2z'/%3E%3C/svg%3E") center / contain no-repeat;
5874
+ }
5875
+ :is(.oc-data-table, .oc-block-datatable) th[aria-sort="descending"] .oc-table-sort::after {
5876
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 7h3l-4-4l-4 4h3v14h2M2 17h10v2H2M6 5v2H2V5m0 6h7v2H2z'/%3E%3C/svg%3E");
5877
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 7h3l-4-4l-4 4h3v14h2M2 17h10v2H2M6 5v2H2V5m0 6h7v2H2z'/%3E%3C/svg%3E");
5878
+ }
5879
+ .oc-data-table th[data-oc-col="actions"] .oc-data-table-th { justify-content: flex-end; gap: 0.5rem; font-weight: 500; color: var(--oc-muted-foreground); }
5880
+ .oc-data-table td[data-oc-col="actions"] { text-align: end; }
5881
+ /* Row actions per status: data-oc-when="active" (edit, view) only on live rows, data-oc-when="deleted" (restore,
5882
+ delete permanently) only on rows in the trash. */
5883
+ .oc-data-table tr[data-oc-status="deleted"] [data-oc-when="active"],
5884
+ .oc-data-table tr:not([data-oc-status="deleted"]) [data-oc-when="deleted"] { display: none; }
5885
+ /* The column chooser in the actions header: a large plain icon (1.75rem), no button frame. */
5886
+ .oc-data-table-columns-btn { font-size: 1.75rem; }
5887
+ .oc-data-table-columns-btn > :is(svg, iconify-icon) { font-size: 1em; inline-size: 1em; block-size: 1em; }
5888
+
5889
+ /* Tabs above the toolbar (All / Customers / Companies …): icon, label and a count; they filter one column. */
5890
+ .oc-data-table-tabs { display: flex; inline-size: 100%; overflow-x: auto; scrollbar-width: none; }
5891
+ .oc-data-table-tabs .oc-tab-trigger { flex-shrink: 0; }
5892
+ .oc-data-table-tabs .oc-tab-trigger { display: inline-flex; align-items: center; gap: 0.4rem; }
5893
+ .oc-data-table-tabs .oc-tab-trigger :is(svg, iconify-icon) { font-size: 1.1em; inline-size: 1.1em; block-size: 1.1em; }
5894
+ .oc-data-table-tabs .oc-tab-trigger[aria-selected="true"] { color: var(--oc-primary); border-bottom-color: var(--oc-primary); }
5895
+ .oc-data-table-tab-count {
5896
+ min-inline-size: 1.25rem;
5897
+ padding: 0 0.375rem;
5898
+ font-size: 0.6875rem;
5899
+ line-height: 1.125rem;
5900
+ text-align: center;
5901
+ color: var(--oc-muted-foreground);
5902
+ background: var(--oc-muted);
5903
+ border-radius: 999px;
5904
+ }
5905
+ .oc-data-table-tabs [aria-selected="true"] .oc-data-table-tab-count { color: var(--oc-primary); background: oklch(from var(--oc-primary) l c h / 0.12); }
5906
+ /* A count in the color of its type (the same tone as the type badge in the table). */
5907
+ .oc-data-table-tab-count:is(.oc-data-table-tab-count--success, .oc-data-table-tab-count--info, .oc-data-table-tab-count--warning) {
5908
+ color: var(--oc-tab-tone);
5909
+ background: oklch(from var(--oc-tab-tone) l c h / 0.12);
5910
+ box-shadow: inset 0 0 0 1px oklch(from var(--oc-tab-tone) l c h / 0.35);
5911
+ }
5912
+ .oc-data-table-tab-count--success { --oc-tab-tone: var(--oc-success); }
5913
+ .oc-data-table-tab-count--info { --oc-tab-tone: var(--oc-info, light-dark(var(--color-deepskyblue-70), var(--color-skyblue-50))); }
5914
+ .oc-data-table-tab-count--warning { --oc-tab-tone: var(--oc-warning); }
5915
+
5916
+ /* Filter row: selects with a leading icon (status, type …) and the column button on the right. */
5917
+ .oc-data-table-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
5918
+ .oc-data-table-filters[hidden] { display: none; }
5919
+ .oc-data-table-filters > :last-child:is(.oc-btn) { margin-inline-start: auto; }
5920
+ .oc-data-table-filter { position: relative; display: inline-flex; align-items: center; }
5921
+ .oc-data-table-filter > :is(svg, iconify-icon) {
5922
+ position: absolute;
5923
+ inset-inline-start: 0.625rem;
5924
+ font-size: 1rem;
5925
+ inline-size: 1rem;
5926
+ block-size: 1rem;
5927
+ color: var(--oc-muted-foreground);
5928
+ pointer-events: none;
5929
+ }
5930
+ .oc-data-table-filter > .oc-select { inline-size: auto; min-inline-size: 11rem; padding-inline-start: 2.125rem; }
5931
+
5932
+ /* Cells. Name: avatar, name with a badge, and a muted second line (VAT number, e-mail …). */
5933
+ .oc-data-table-name { display: flex; align-items: center; gap: 0.625rem; min-inline-size: 0; }
5934
+ .oc-data-table-name .oc-avatar { inline-size: 2.25rem; block-size: 2.25rem; font-size: 0.75rem; }
5935
+ .oc-data-table-name-text { display: grid; min-inline-size: 0; }
5936
+ .oc-data-table-name-line { display: flex; align-items: center; gap: 0.375rem; font-weight: 600; }
5937
+ .oc-data-table-name-sub { font-size: 0.75rem; color: var(--oc-muted-foreground); }
5938
+ /* One line with an ellipsis; put the full text in title="". */
5939
+ .oc-data-table-truncate { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-inline-size: var(--oc-data-table-truncate, 18rem); }
5940
+ /* A count that opens the related list (projects, invoices …); muted while it is 0. */
5941
+ .oc-data-table-count { font-variant-numeric: tabular-nums; }
5942
+ .oc-data-table-count[data-count="0"] { color: var(--oc-muted-foreground); opacity: 0.75; }
5943
+ /* The sort is visible in the header (icon, bold label); the summary line is for screen readers only. */
5944
+ .oc-data-table-sort-summary {
5945
+ position: absolute;
5946
+ inline-size: 1px;
5947
+ block-size: 1px;
5948
+ margin: 0;
5949
+ overflow: hidden;
5950
+ clip-path: inset(50%);
5951
+ white-space: nowrap;
5952
+ }
5953
+ .oc-data-table-file { display: none; }
5954
+
5955
+ /* The other visible columns as label / value lines, and the row actions at the bottom. */
5956
+ .oc-data-table-card-fields { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.25rem 0.75rem; margin: 0; font-size: 0.8125rem; }
5957
+ .oc-data-table-card-fields dt { color: var(--oc-muted-foreground); }
5958
+ .oc-data-table-card-fields dd { margin: 0; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
5959
+ .oc-data-table-card-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.375rem; margin-block-start: auto; }
5960
+
5961
+ /* Code cells (article numbers, SKUs): monospace and a step smaller. */
5962
+ .oc-data-table-code { font-family: var(--oc-font-mono); font-size: 0.75rem; }
5963
+
5964
+ /* Version 2, ".oc-data-table--bordered": a tinted header row, a line between the columns and the
5965
+ sort chevrons always visible; the filter select has its icon in a zone of its own. The rest is the same table. */
5966
+ .oc-data-table.oc-data-table--bordered .oc-table :is(th, td) { border-inline-end: 1px solid var(--oc-border); }
5967
+ .oc-data-table.oc-data-table--bordered .oc-table :is(th, td):last-child { border-inline-end: 0; }
5968
+ .oc-data-table.oc-data-table--bordered .oc-table thead th {
5969
+ font-weight: 500;
5970
+ background: color-mix(in oklch, var(--oc-card), var(--oc-foreground) 4%);
5971
+ }
5972
+ .oc-data-table.oc-data-table--bordered .oc-table thead th[data-oc-col="actions"] {
5973
+ background: color-mix(in oklch, var(--oc-card), var(--oc-foreground) 4%);
5974
+ }
5975
+ .oc-data-table--bordered td[data-oc-col="actions"] { text-align: center; }
5976
+ .oc-data-table--bordered th[data-oc-col="actions"] .oc-data-table-th { justify-content: center; }
5977
+ .oc-data-table--bordered th:not([aria-sort]) .oc-table-sort::after,
5978
+ .oc-data-table--bordered th:not([aria-sort]):hover .oc-table-sort::after {
5979
+ content: "";
5980
+ inline-size: 0.875rem;
5981
+ block-size: 0.875rem;
5982
+ opacity: 0.5;
5983
+ background: currentColor;
5984
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 18.17L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15M12 5.83L15.17 9l1.41-1.41L12 3L7.41 7.59L8.83 9z'/%3E%3C/svg%3E") center / contain no-repeat;
5985
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 18.17L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15M12 5.83L15.17 9l1.41-1.41L12 3L7.41 7.59L8.83 9z'/%3E%3C/svg%3E") center / contain no-repeat;
5986
+ }
5987
+ .oc-data-table--bordered .oc-data-table-filter::after {
5988
+ content: "";
5989
+ position: absolute;
5990
+ inset-block: 0;
5991
+ inset-inline-start: 2rem;
5992
+ inline-size: 1px;
5993
+ background: var(--oc-border);
5994
+ pointer-events: none;
5995
+ }
5996
+ .oc-data-table--bordered .oc-data-table-filter > .oc-select { padding-inline-start: 2.75rem; }
5997
+
5667
5998
  /* parts/app-data-table.css */
5668
- /* File: packages/ui/src/parts/app-data-table.css | Map: shell -> toolbar -> frame and scroll -> header resize -> select and actions columns -> deleted row -> bulk bar -> card view -> footer and pager -> compact density */
5669
- /* List table from Korrekt and OmniaDesk. The frame, toolbar and footer are styling.
5999
+ /* File: packages/ui/src/parts/app-data-table.css | Map: shell -> toolbar -> frame and scroll -> header label -> select and actions columns -> deleted row -> bulk bar -> card view -> compact density */
6000
+ /* List table for business apps. The frame, toolbar and footer are styling. Tabs, filter row, rich cells and
6001
+ card fields: app-data-table-rich.css; footer and pager: app-data-table-footer.css.
5670
6002
  Behaviour (search, sort, pages, resize, drag, trash, bulk) lives in src/data-table.js.
5671
6003
  Set --oc-data-table-max on the shell when the body should scroll and the header should stick. */
5672
6004
 
@@ -5679,13 +6011,16 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5679
6011
  background: var(--oc-card);
5680
6012
  border: 1px solid var(--oc-border);
5681
6013
  border-radius: var(--oc-radius);
6014
+ /* Full width, and in a grid or flex parent the table scrolls inside the frame instead of stretching the card. */
6015
+ inline-size: 100%;
6016
+ min-inline-size: 0;
5682
6017
  }
5683
6018
 
5684
- /* Toolbar: title + gear, search, trash pills, table/cards toggle, page actions. */
6019
+ /* Toolbar: title + gear/filter button, search (takes the free width), trash pills, table/cards toggle, page actions. */
5685
6020
  .oc-data-table-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
5686
6021
  .oc-data-table-heading { display: flex; align-items: center; gap: 0.5rem; min-inline-size: 0; }
5687
6022
  .oc-data-table-title { font-size: 0.875rem; font-weight: 600; }
5688
- .oc-data-table-search { flex: 1 1 12rem; min-inline-size: 10rem; max-inline-size: 22rem; }
6023
+ .oc-data-table-search { flex: 1 1 10rem; min-inline-size: 9rem; }
5689
6024
  .oc-data-table-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-inline-start: auto; }
5690
6025
  .oc-data-table-view { display: inline-flex; gap: 0.25rem; }
5691
6026
  .oc-data-table-view [aria-pressed="true"] {
@@ -5693,55 +6028,52 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5693
6028
  background: oklch(from var(--oc-primary) l c h / 0.1);
5694
6029
  border-color: oklch(from var(--oc-primary) l c h / 0.35);
5695
6030
  }
5696
- .oc-data-table-sort-summary { flex-basis: 100%; margin: 0; font-size: 0.75rem; color: var(--oc-muted-foreground); }
5697
- .oc-data-table-sort-summary:empty { display: none; }
5698
- .oc-data-table-file { display: none; }
6031
+ /* In the toolbar the trash pills read as one switch: the chosen one is filled. */
6032
+ .oc-data-table-toolbar .oc-filter-pills { border-inline-start: 0; padding-inline-start: 0; }
6033
+ .oc-data-table-toolbar .oc-filter-pill[aria-current="true"] {
6034
+ color: var(--oc-primary-foreground);
6035
+ background: var(--oc-primary);
6036
+ border-color: var(--oc-primary);
6037
+ }
5699
6038
 
5700
6039
  /* The table sits in its own rounded frame. The scroll box keeps the header sticky. */
5701
6040
  .oc-data-table-frame {
6041
+ min-inline-size: 0;
5702
6042
  overflow: hidden;
5703
- background: var(--oc-background);
6043
+ background: var(--oc-card);
5704
6044
  border: 1px solid var(--oc-border);
5705
6045
  border-radius: calc(var(--oc-radius) - 2px);
5706
6046
  }
5707
6047
  .oc-data-table-scroll { overflow: auto; max-block-size: var(--oc-data-table-max, none); }
5708
6048
  .oc-data-table .oc-table :is(th, td) { border-inline: 0; border-block-start: 0; }
6049
+ /* Columns size to their content and cells stay on one line: a narrow screen scrolls the frame sideways instead of
6050
+ squeezing names and phone numbers onto two lines. .oc-data-table-wrap on a cell lets long text wrap again. */
6051
+ .oc-data-table .oc-data-table-table { table-layout: auto; }
6052
+ .oc-data-table .oc-data-table-table :is(th, td) { white-space: nowrap; vertical-align: middle; padding-inline: 0.625rem; }
6053
+ .oc-data-table .oc-data-table-table td.oc-data-table-wrap { white-space: normal; }
5709
6054
  .oc-data-table .oc-table thead th {
5710
6055
  position: sticky;
5711
6056
  inset-block-start: 0;
5712
6057
  z-index: 1;
5713
- block-size: 2.5rem;
5714
- font-size: 0.75rem;
5715
- font-weight: 500;
6058
+ block-size: 2.75rem;
6059
+ font-size: 0.8125rem;
6060
+ font-weight: 600;
6061
+ color: var(--oc-foreground);
5716
6062
  background: var(--oc-card);
5717
6063
  }
5718
- .oc-data-table .oc-table tbody tr:nth-child(even) { background: var(--oc-surface); }
6064
+ /* Striped rows (every second visible row), the row under the pointer a step stronger. Solid colors, so the sticky
6065
+ actions cell can take the same background (inherit) and nothing scrolls through it. */
6066
+ .oc-data-table {
6067
+ --oc-data-table-stripe: color-mix(in oklch, var(--oc-card), var(--oc-foreground) 3.5%);
6068
+ --oc-data-table-hover: color-mix(in oklch, var(--oc-card), var(--oc-primary) 8%);
6069
+ }
6070
+ .oc-data-table .oc-table tbody tr:nth-child(even of :not([hidden])) { background: var(--oc-data-table-stripe); }
6071
+ .oc-data-table .oc-table tbody tr:hover { background: var(--oc-data-table-hover); }
6072
+ .oc-data-table--plain .oc-table tbody tr:nth-child(even of :not([hidden])):not(:hover) { background: none; }
5719
6073
  .oc-data-table .oc-table tbody tr:last-child > td { border-block-end: 0; }
5720
6074
 
5721
- /* Header label and the drag handle that sets the column width. */
5722
- .oc-data-table-th { display: flex; align-items: center; gap: 0.25rem; position: relative; min-inline-size: 0; }
5723
- .oc-data-table-resize {
5724
- position: absolute;
5725
- inset-block: 0;
5726
- inset-inline-end: -0.45rem;
5727
- inline-size: 0.55rem;
5728
- padding: 0;
5729
- background: none;
5730
- border: 0;
5731
- cursor: col-resize;
5732
- touch-action: none;
5733
- }
5734
- .oc-data-table-resize::after {
5735
- content: "";
5736
- position: absolute;
5737
- inset-block: 0.4rem;
5738
- inset-inline-start: 0.25rem;
5739
- inline-size: 1px;
5740
- background: var(--oc-border);
5741
- }
5742
- .oc-data-table.is-resizing { cursor: col-resize; user-select: none; }
5743
- /* After a resize the table grows with its columns and the frame scrolls sideways. */
5744
- .oc-data-table-table--sized { inline-size: max-content; min-inline-size: 100%; }
6075
+ /* Header label (column width by dragging is OmniaUI Pro: app-data-table-pro.css). */
6076
+ .oc-data-table-th { display: flex; align-items: center; gap: 0.25rem; min-inline-size: 0; }
5745
6077
 
5746
6078
  /* Narrow control columns. The actions column stays on the right while the rest scrolls. */
5747
6079
  .oc-data-table .oc-table :is(th, td)[data-oc-col="select"],
@@ -5757,8 +6089,8 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5757
6089
  background: var(--oc-card);
5758
6090
  box-shadow: -1px 0 0 var(--oc-border);
5759
6091
  }
5760
- .oc-data-table .oc-table thead th[data-oc-col="actions"] { z-index: 3; }
5761
- .oc-data-table .oc-table tbody tr:nth-child(even) > td[data-oc-col="actions"] { background: var(--oc-surface); }
6092
+ .oc-data-table .oc-table thead th[data-oc-col="actions"] { z-index: 3; background: var(--oc-card); }
6093
+ .oc-data-table .oc-table tbody tr:is(:nth-child(even of :not([hidden])), :hover) > td[data-oc-col="actions"] { background: inherit; }
5762
6094
 
5763
6095
  /* Trash and archive rows stay readable but quieter than active rows. */
5764
6096
  .oc-data-table tr[data-oc-status="deleted"],
@@ -5787,55 +6119,24 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5787
6119
  padding: 0.75rem;
5788
6120
  }
5789
6121
  .oc-data-table-card {
5790
- padding: 0.75rem;
6122
+ display: flex;
6123
+ flex-direction: column;
6124
+ gap: 0.75rem;
6125
+ padding: 0.875rem;
5791
6126
  background: var(--oc-card);
5792
6127
  border: 1px solid var(--oc-border);
5793
6128
  border-radius: calc(var(--oc-radius) - 2px);
5794
6129
  }
5795
6130
  .oc-data-table-card-title { margin: 0; font-size: 0.875rem; font-weight: 600; }
5796
-
6131
+ .oc-data-table-card-title .oc-data-table-name-sub { font-weight: 400; }
5797
6132
  .oc-data-table-empty { margin: 0; padding: 2rem 1rem; text-align: center; color: var(--oc-muted-foreground); }
5798
6133
  .oc-data-table-empty[hidden] { display: none; }
5799
6134
 
5800
- /* Footer: count on the left, rows-per-page in the middle, pager on the right. */
5801
- .oc-data-table-footer {
5802
- display: grid;
5803
- grid-template-columns: 1fr auto 1fr;
5804
- align-items: center;
5805
- gap: 0.75rem;
5806
- }
5807
- .oc-data-table-summary { margin: 0; font-size: 0.8125rem; color: var(--oc-muted-foreground); }
5808
- .oc-data-table-per-page { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem; }
5809
- .oc-data-table-per-page .oc-select { inline-size: auto; }
5810
- .oc-data-table-pager { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.25rem; }
5811
- .oc-data-table-page {
5812
- min-inline-size: 2rem;
5813
- padding: 0.25rem 0.45rem;
5814
- font: inherit;
5815
- font-size: 0.8125rem;
5816
- color: var(--oc-foreground);
5817
- background: var(--oc-card);
5818
- border: 1px solid var(--oc-border);
5819
- border-radius: calc(var(--oc-radius) - 2px);
5820
- cursor: pointer;
5821
- }
5822
- .oc-data-table-page[aria-current="page"] {
5823
- color: var(--oc-primary);
5824
- background: oklch(from var(--oc-primary) l c h / 0.1);
5825
- border-color: oklch(from var(--oc-primary) l c h / 0.35);
5826
- }
5827
- .oc-data-table-page:disabled { opacity: 0.4; cursor: default; }
5828
- .oc-data-table-page:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
5829
- .oc-data-table-ellipsis { padding-inline: 0.25rem; color: var(--oc-muted-foreground); }
5830
6135
 
5831
6136
  /* Design choice from the gear menu: tighter rows, like the compact article lists. */
5832
6137
  .oc-data-table--compact .oc-table :is(th, td) { padding: 0.25rem 0.5rem; font-size: 0.8125rem; }
5833
6138
  .oc-data-table--compact .oc-table thead th { block-size: 2rem; }
5834
6139
 
5835
- @media (max-width: 40rem) {
5836
- .oc-data-table-footer { grid-template-columns: 1fr; }
5837
- .oc-data-table-pager { justify-content: flex-start; }
5838
- }
5839
6140
  @media (prefers-reduced-motion: reduce) {
5840
6141
  .oc-data-table .oc-table tbody tr { transition: none; }
5841
6142
  }
@@ -5972,6 +6273,73 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5972
6273
  .oc-list-toolbar-view { transition: none; }
5973
6274
  }
5974
6275
 
6276
+ /* parts/app-name-card.css */
6277
+ /* File: packages/ui/src/parts/app-name-card.css | Map: card (details) -> head (chevron, name, rename, amount, actions) -> rename input -> body -> nested cards */
6278
+ /* Name card: a collapsible card for one part of a quote or plan,
6279
+ with its name, a rename pencil, an optional amount and a delete button. Native <details>: it opens without script;
6280
+ packages/ui/src/name-card.js adds the rename (pencil -> input, Enter saves, Escape cancels). */
6281
+
6282
+ .oc-name-card {
6283
+ background: var(--oc-card);
6284
+ border: 1px solid var(--oc-border);
6285
+ border-radius: var(--oc-radius);
6286
+ }
6287
+ .oc-name-card + .oc-name-card { margin-block-start: 0.5rem; }
6288
+ .oc-name-card__head {
6289
+ display: flex;
6290
+ align-items: center;
6291
+ gap: 0.5rem;
6292
+ min-block-size: 2.75rem;
6293
+ padding: 0.375rem 0.5rem 0.375rem 0.75rem;
6294
+ list-style: none;
6295
+ cursor: pointer;
6296
+ }
6297
+ .oc-name-card__head::-webkit-details-marker { display: none; }
6298
+ .oc-name-card__head::before {
6299
+ content: "";
6300
+ flex-shrink: 0;
6301
+ inline-size: 1rem;
6302
+ block-size: 1rem;
6303
+ background: var(--oc-muted-foreground);
6304
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.59 16.58L13.17 12L8.59 7.41L10 6l6 6l-6 6z'/%3E%3C/svg%3E") center / contain no-repeat;
6305
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.59 16.58L13.17 12L8.59 7.41L10 6l6 6l-6 6z'/%3E%3C/svg%3E") center / contain no-repeat;
6306
+ transition: rotate var(--oc-transition-fast);
6307
+ }
6308
+ .oc-name-card[open] > .oc-name-card__head::before { rotate: 90deg; }
6309
+ .oc-name-card__head:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: -2px; border-radius: var(--oc-radius); }
6310
+ .oc-name-card__title { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
6311
+ .oc-name-card__meta { font-size: 0.75rem; color: var(--oc-muted-foreground); white-space: nowrap; }
6312
+ .oc-name-card__head .oc-btn { flex-shrink: 0; }
6313
+ /* Amount and actions sit on the right; the amount is tabular so a column of cards lines up. */
6314
+ .oc-name-card__amount { margin-inline-start: auto; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
6315
+ .oc-name-card__actions { display: inline-flex; flex-shrink: 0; gap: 0.25rem; margin-inline-start: auto; }
6316
+ .oc-name-card__amount + .oc-name-card__actions { margin-inline-start: 0.5rem; }
6317
+
6318
+ /* Rename: the title becomes an input of the same size (hidden toggles between the two). */
6319
+ .oc-name-card__input {
6320
+ flex: 1;
6321
+ min-inline-size: 0;
6322
+ block-size: 2rem;
6323
+ padding: 0 0.5rem;
6324
+ font: inherit;
6325
+ font-weight: 600;
6326
+ color: var(--oc-foreground);
6327
+ background: var(--oc-background);
6328
+ border: 1px solid var(--oc-ring);
6329
+ border-radius: calc(var(--oc-radius) - 2px);
6330
+ }
6331
+ .oc-name-card__input[hidden],
6332
+ .oc-name-card__title[hidden] { display: none; }
6333
+
6334
+ .oc-name-card__body { padding: 0.75rem 0.75rem 0.875rem 2.25rem; border-block-start: 1px solid var(--oc-border); }
6335
+ /* Nested cards (the parts of a title) use a quieter surface; --total colors the amount of the main card. */
6336
+ .oc-name-card__body > .oc-name-card { background: var(--oc-surface); }
6337
+ .oc-name-card--total > .oc-name-card__head .oc-name-card__amount { color: var(--oc-primary); }
6338
+
6339
+ @media (prefers-reduced-motion: reduce) {
6340
+ .oc-name-card__head::before { transition: none; }
6341
+ }
6342
+
5975
6343
  /* parts/app-page-header.css */
5976
6344
  /* File: packages/ui/src/parts/app-page-header.css | Map: header (container) -> back link row -> main row (text + actions) -> title + subtitle + meta -> wide layout (@container) -> modifiers (bordered, sticky, sm) */
5977
6345
  /* Page header: back link + title + subtitle + actions at the top of an app page.
@@ -6028,6 +6396,47 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
6028
6396
  .oc-page-header--sm { padding-block: 0.5rem 0.75rem; }
6029
6397
  .oc-page-header--sm .oc-page-header-title { font-size: 1.125rem; }
6030
6398
 
6399
+ /* parts/app-result.css */
6400
+ /* File: packages/ui/src/parts/app-result.css */
6401
+ /* Map: result -> icon tile -> title -> text -> actions */
6402
+ /* The outcome of an action in the place where the form was: "Thank you, your message was sent" with an icon tile, a
6403
+ short text and a button ("Send another message"). Give it role="status" and tabindex="-1" and move the focus to it.
6404
+ --center centers it; the tile takes its color from an .oc-accent--* preset. */
6405
+
6406
+ .oc-result {
6407
+ display: grid;
6408
+ justify-items: start;
6409
+ gap: 0.75rem;
6410
+ }
6411
+ .oc-result:focus { outline: none; }
6412
+ .oc-result[hidden] { display: none; }
6413
+ .oc-result > .oc-icon-tile { margin-block-end: 0.35rem; }
6414
+ .oc-result__title {
6415
+ margin: 0;
6416
+ font-size: 1.2rem;
6417
+ line-height: 1.25;
6418
+ }
6419
+ .oc-result__text {
6420
+ margin: 0;
6421
+ color: var(--oc-muted-foreground);
6422
+ }
6423
+ .oc-result__text a {
6424
+ color: var(--oc-primary);
6425
+ text-decoration: underline;
6426
+ text-underline-offset: 3px;
6427
+ }
6428
+ .oc-result__actions {
6429
+ display: flex;
6430
+ flex-wrap: wrap;
6431
+ gap: 0.75rem;
6432
+ margin-block-start: 0.5rem;
6433
+ }
6434
+ .oc-result--center {
6435
+ justify-items: center;
6436
+ text-align: center;
6437
+ }
6438
+ .oc-result--center .oc-result__actions { justify-content: center; }
6439
+
6031
6440
  /* parts/app-severity-card.css */
6032
6441
  /* File: packages/ui/src/parts/app-severity-card.css | Map: card (details) -> head (summary) -> icon + title + tags -> level badge + chevron -> body + actions -> severity modifiers -> resolved state -> reduced motion */
6033
6442
  /* Severity card: risk-colored expandable card (security finding, incident, audit item).
@@ -6456,6 +6865,11 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6456
6865
  min-block-size: 0;
6457
6866
  timeline-scope: --oc-titlebar-scroll;
6458
6867
  }
6868
+ /* A card with a title bar on top: the bar runs edge to edge, its last button follows the card corner. */
6869
+ .oc-card:has(> .oc-titlebar:first-child) { padding: 0; --oc-titlebar-radius: calc(var(--oc-radius) + 3px); }
6870
+ /* As the heading of a card section (bar + .oc-card-content): the title lines up with the content below. */
6871
+ .oc-card > .oc-titlebar:first-child:has(+ .oc-card-content) { padding-inline-start: 24px; }
6872
+ .oc-card > .oc-titlebar:first-child:has(+ .oc-card-content) .oc-titlebar__title { font-size: 0.9375rem; }
6459
6873
 
6460
6874
  .oc-titlebar {
6461
6875
  display: flex;
@@ -6463,7 +6877,7 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6463
6877
  align-items: center;
6464
6878
  gap: 2px;
6465
6879
  min-block-size: 46px;
6466
- padding: 0 8px 0 14px;
6880
+ padding: 0 0 0 14px;
6467
6881
  border-block-end: 1px solid transparent;
6468
6882
  -webkit-user-select: none;
6469
6883
  user-select: none;
@@ -6480,7 +6894,7 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6480
6894
  /* Title area: grows, cuts the subtitle off with …. */
6481
6895
  .oc-titlebar__main { display: flex; flex: 1; align-items: center; gap: 8px; min-inline-size: 0; }
6482
6896
  .oc-titlebar__icon { flex: none; inline-size: 18px; block-size: 18px; }
6483
- .oc-titlebar__title { flex: none; font-size: 0.8667rem; font-weight: 650; letter-spacing: -0.01em; }
6897
+ .oc-titlebar__title { flex: none; margin: 0; font-size: 0.8667rem; font-weight: 650; letter-spacing: -0.01em; }
6484
6898
  .oc-titlebar__subtitle {
6485
6899
  min-inline-size: 0;
6486
6900
  overflow: hidden;
@@ -6490,17 +6904,19 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6490
6904
  white-space: nowrap;
6491
6905
  }
6492
6906
  .oc-titlebar__subtitle::before { content: "·"; margin-inline-end: 8px; }
6493
- .oc-titlebar__actions { display: flex; flex: none; align-items: center; gap: 2px; }
6907
+ /* Window buttons sit in the top right corner (as on a desktop window), not centred on the title line. */
6908
+ .oc-titlebar__actions { display: flex; flex: none; align-self: flex-start; align-items: flex-start; margin-inline-start: auto; }
6494
6909
 
6495
- /* Compact icon buttons: muted, a light surface on hover, the close button red. */
6910
+ /* Window buttons: muted, a light surface on hover, the close button red. The last one follows the
6911
+ rounded corner of the card or window around the bar (--oc-titlebar-radius). */
6496
6912
  .oc-titlebar__btn {
6497
6913
  display: inline-grid;
6498
6914
  place-items: center;
6499
- inline-size: 30px;
6500
- block-size: 30px;
6915
+ inline-size: 40px;
6916
+ block-size: 34px;
6501
6917
  padding: 0;
6502
6918
  border: 0;
6503
- border-radius: 8px;
6919
+ border-radius: 0;
6504
6920
  background: transparent;
6505
6921
  color: var(--oc-muted-foreground);
6506
6922
  font: inherit;
@@ -6511,6 +6927,7 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6511
6927
  .oc-titlebar__btn:hover { background: var(--oc-glass-panel-strong, var(--oc-accent)); color: var(--oc-foreground); }
6512
6928
  .oc-titlebar__btn:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: -2px; }
6513
6929
  .oc-titlebar__btn--close:hover { background: var(--oc-destructive); color: var(--oc-destructive-foreground); }
6930
+ .oc-titlebar__actions > .oc-titlebar__btn:last-child { border-start-end-radius: var(--oc-titlebar-radius, calc(var(--oc-radius) - 1px)); }
6514
6931
 
6515
6932
  /* Body: the part that scrolls under the bar. */
6516
6933
  .oc-titlebar-body {
@@ -6535,7 +6952,7 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6535
6952
 
6536
6953
  /* Sizes */
6537
6954
  .oc-titlebar--sm { min-block-size: 36px; padding-inline-start: 10px; }
6538
- .oc-titlebar--sm .oc-titlebar__btn { inline-size: 26px; block-size: 26px; border-radius: 6px; }
6955
+ .oc-titlebar--sm .oc-titlebar__btn { inline-size: 34px; block-size: 28px; }
6539
6956
 
6540
6957
  @media (prefers-reduced-motion: reduce) {
6541
6958
  .oc-titlebar-window > .oc-titlebar { animation: none; }
@@ -7060,6 +7477,229 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7060
7477
  .oc-chart-line__dot, .oc-chart-line__tip { transition: none; }
7061
7478
  }
7062
7479
 
7480
+ /* parts/data-meter-row.css */
7481
+ /* File: packages/ui/src/parts/data-meter-row.css | Map: row grid -> label and value -> bar and fill -> levels -> reset line -> list */
7482
+ /* Usage meter row: a label, the usage as text, a bar and when it resets. The fill comes from
7483
+ --oc-meter (0–100%); data-level="warn" or "critical" changes the color. */
7484
+
7485
+ .oc-meter-row {
7486
+ display: grid;
7487
+ grid-template-columns: minmax(0, 1fr) auto;
7488
+ align-items: baseline;
7489
+ gap: 0.375rem 0.75rem;
7490
+ }
7491
+ .oc-meter-row__label { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.875rem; font-weight: 600; }
7492
+ .oc-meter-row__value { font-size: 0.8125rem; font-variant-numeric: tabular-nums; color: var(--oc-muted-foreground); white-space: nowrap; }
7493
+ .oc-meter-row__value strong { color: var(--oc-foreground); }
7494
+ .oc-meter-row__bar {
7495
+ grid-column: 1 / -1;
7496
+ block-size: 0.5rem;
7497
+ overflow: hidden;
7498
+ background: var(--oc-muted);
7499
+ border-radius: 999px;
7500
+ }
7501
+ .oc-meter-row__bar::before {
7502
+ content: "";
7503
+ display: block;
7504
+ inline-size: clamp(0%, var(--oc-meter, 0%), 100%);
7505
+ block-size: 100%;
7506
+ background: var(--oc-meter-color, var(--oc-success));
7507
+ border-radius: inherit;
7508
+ transition: inline-size 0.4s ease;
7509
+ }
7510
+ .oc-meter-row[data-level="warn"] { --oc-meter-color: var(--oc-warning); }
7511
+ .oc-meter-row[data-level="critical"] { --oc-meter-color: var(--oc-destructive); }
7512
+ .oc-meter-row[data-level="critical"] .oc-meter-row__value strong { color: var(--oc-destructive); }
7513
+ .oc-meter-row__reset { grid-column: 1 / -1; display: inline-flex; align-items: center; gap: 0.25rem; font-size: 0.75rem; color: var(--oc-muted-foreground); }
7514
+ .oc-meter-list { display: grid; gap: 1rem; }
7515
+ .oc-meter-list--compact { gap: 0.625rem; }
7516
+ .oc-meter-list--compact .oc-meter-row__bar { block-size: 0.375rem; }
7517
+
7518
+ @media (prefers-reduced-motion: reduce) {
7519
+ .oc-meter-row__bar::before { transition: none; }
7520
+ }
7521
+
7522
+ /* parts/data-price.css */
7523
+ /* File: packages/ui/src/parts/data-price.css | Map: price block -> main line (amount, old price, discount) -> VAT note and alternative -> sizes and sale */
7524
+ /* A price as shops and quotes show it: the amount, an optional old price and discount badge, a VAT note
7525
+ ("excl. VAT") and the other amount ("€ 121,00 incl. VAT"). Numbers use tabular digits. */
7526
+
7527
+ .oc-price { display: inline-grid; gap: 0.125rem; min-inline-size: 0; }
7528
+ .oc-price__main { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.5rem; }
7529
+ .oc-price__amount { font-size: 1.25rem; font-weight: 700; line-height: 1.2; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; white-space: nowrap; }
7530
+ .oc-price__old { font-size: 0.875rem; color: var(--oc-muted-foreground); text-decoration: line-through; text-decoration-thickness: 1px; font-variant-numeric: tabular-nums; white-space: nowrap; }
7531
+ .oc-price__main > .oc-badge { align-self: center; }
7532
+ .oc-price__note { font-size: 0.75rem; font-weight: 500; color: var(--oc-muted-foreground); }
7533
+ .oc-price__alt { font-size: 0.8125rem; color: var(--oc-muted-foreground); font-variant-numeric: tabular-nums; }
7534
+
7535
+ .oc-price--sm .oc-price__amount { font-size: 1rem; }
7536
+ .oc-price--lg .oc-price__amount { font-size: 1.75rem; }
7537
+ .oc-price--sale .oc-price__amount { color: var(--oc-destructive); }
7538
+ /* Inline: amount and note on one line (table cells, lists). */
7539
+ .oc-price--inline { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.5rem; }
7540
+ .oc-price--inline .oc-price__amount { font-size: inherit; font-weight: 600; }
7541
+ td[data-numeric] > .oc-price, .oc-price--end { justify-items: end; text-align: end; }
7542
+ td[data-numeric] > .oc-price .oc-price__main, .oc-price--end .oc-price__main { justify-content: flex-end; }
7543
+
7544
+ /* parts/data-ribbon.css */
7545
+ /* File: packages/ui/src/parts/data-ribbon.css | Map: host (positioned, clipped) -> ribbon band -> four corners -> tones -> size -> RTL */
7546
+ /* Corner ribbon: a diagonal band across one corner of a card, an image or a document ("Paid", "New", "Sale").
7547
+ Put .oc-ribbon as a child of the box; the box gets position: relative and clips the band (zero specificity,
7548
+ so your own position or overflow wins). The text is real text, so screen readers read it. */
7549
+
7550
+ :where(:has(> .oc-ribbon)) { position: relative; overflow: clip; }
7551
+
7552
+ .oc-ribbon {
7553
+ /* Distance from the corner to the middle of the band, along both edges. */
7554
+ --oc-ribbon-inset: 2.25rem;
7555
+ --oc-ribbon-x: 1;
7556
+ --oc-ribbon-y: -1;
7557
+ --oc-ribbon-turn: 45deg;
7558
+ --oc-ribbon-bg: var(--oc-primary);
7559
+ --oc-ribbon-fg: var(--oc-primary-foreground);
7560
+ position: absolute;
7561
+ z-index: 2;
7562
+ inset-block-start: var(--oc-ribbon-inset);
7563
+ inset-inline-end: var(--oc-ribbon-inset);
7564
+ inline-size: calc(var(--oc-ribbon-inset) * 2.83 + 3rem);
7565
+ padding: 0.3em 0;
7566
+ overflow: hidden;
7567
+ color: var(--oc-ribbon-fg);
7568
+ background: var(--oc-ribbon-bg);
7569
+ box-shadow: 0 1px 3px oklch(0 0 0 / 0.18);
7570
+ font-size: 0.75rem;
7571
+ font-weight: 700;
7572
+ line-height: 1.25;
7573
+ letter-spacing: 0.08em;
7574
+ text-align: center;
7575
+ text-overflow: ellipsis;
7576
+ text-transform: uppercase;
7577
+ white-space: nowrap;
7578
+ pointer-events: none;
7579
+ translate: calc(50% * var(--oc-ribbon-x)) calc(50% * var(--oc-ribbon-y));
7580
+ rotate: var(--oc-ribbon-turn);
7581
+ }
7582
+
7583
+ /* Corners: top end is the default. */
7584
+ .oc-ribbon--top-start {
7585
+ inset-inline-end: auto;
7586
+ inset-inline-start: var(--oc-ribbon-inset);
7587
+ --oc-ribbon-x: -1;
7588
+ --oc-ribbon-turn: -45deg;
7589
+ }
7590
+ .oc-ribbon--bottom-end {
7591
+ inset-block-start: auto;
7592
+ inset-block-end: var(--oc-ribbon-inset);
7593
+ --oc-ribbon-y: 1;
7594
+ --oc-ribbon-turn: -45deg;
7595
+ }
7596
+ .oc-ribbon--bottom-start {
7597
+ inset-block-start: auto;
7598
+ inset-block-end: var(--oc-ribbon-inset);
7599
+ inset-inline-end: auto;
7600
+ inset-inline-start: var(--oc-ribbon-inset);
7601
+ --oc-ribbon-x: -1;
7602
+ --oc-ribbon-y: 1;
7603
+ }
7604
+
7605
+ /* Tones. */
7606
+ .oc-ribbon--success { --oc-ribbon-bg: var(--oc-success); --oc-ribbon-fg: var(--oc-success-foreground); }
7607
+ .oc-ribbon--warning { --oc-ribbon-bg: var(--oc-warning); --oc-ribbon-fg: var(--oc-warning-foreground); }
7608
+ .oc-ribbon--destructive { --oc-ribbon-bg: var(--oc-destructive); --oc-ribbon-fg: var(--oc-destructive-foreground); }
7609
+ .oc-ribbon--muted { --oc-ribbon-bg: var(--oc-muted); --oc-ribbon-fg: var(--oc-muted-foreground); }
7610
+
7611
+ /* Sizes: a smaller band for a product card, a larger one for a document or a hero image. */
7612
+ .oc-ribbon--sm { --oc-ribbon-inset: 1.6rem; font-size: 0.625rem; }
7613
+ .oc-ribbon--lg { --oc-ribbon-inset: 2.9rem; font-size: 0.875rem; }
7614
+
7615
+ /* Right to left: start and end swap sides, so the band leans the other way. */
7616
+ .oc-ribbon:dir(rtl) { translate: calc(-50% * var(--oc-ribbon-x)) calc(50% * var(--oc-ribbon-y)); rotate: calc(var(--oc-ribbon-turn) * -1); }
7617
+
7618
+ @media (forced-colors: active) {
7619
+ .oc-ribbon { forced-color-adjust: none; color: Canvas; background: CanvasText; }
7620
+ }
7621
+
7622
+ /* parts/data-spec-list.css */
7623
+ /* File: packages/ui/src/parts/data-spec-list.css | Map: list (container) -> groups in columns -> group title -> rows (term / value) -> stack modifier */
7624
+ /* Technical specifications in named groups (Dimensions, Electrical, Package …): label / value rows with tabular
7625
+ digits. Two columns of groups once the list is wide enough (container query), one column with --stack. */
7626
+
7627
+ .oc-spec-list { container-type: inline-size; inline-size: 100%; display: grid; gap: 1.25rem; min-inline-size: 0; }
7628
+ @container (min-width: 40rem) {
7629
+ .oc-spec-list:not(.oc-spec-list--stack) { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 1.25rem 2rem; }
7630
+ }
7631
+ .oc-spec-group { min-inline-size: 0; }
7632
+ .oc-spec-group__title {
7633
+ display: flex;
7634
+ align-items: center;
7635
+ gap: 0.5rem;
7636
+ margin: 0 0 0.25rem;
7637
+ padding-block-end: 0.375rem;
7638
+ font-size: 0.75rem;
7639
+ font-weight: 600;
7640
+ letter-spacing: 0.04em;
7641
+ text-transform: uppercase;
7642
+ color: var(--oc-muted-foreground);
7643
+ border-block-end: 1px solid var(--oc-border);
7644
+ }
7645
+ .oc-spec-group__title > :is(svg, iconify-icon) { inline-size: 1rem; block-size: 1rem; font-size: 1rem; }
7646
+ .oc-spec-group__rows { margin: 0; }
7647
+ .oc-spec-row {
7648
+ display: grid;
7649
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
7650
+ gap: 1rem;
7651
+ padding: 0.4rem 0;
7652
+ font-size: 0.875rem;
7653
+ border-block-end: 1px dashed var(--oc-border);
7654
+ }
7655
+ .oc-spec-row:last-child { border-block-end: 0; }
7656
+ .oc-spec-row > dt { margin: 0; color: var(--oc-muted-foreground); }
7657
+ .oc-spec-row > dd { margin: 0; min-inline-size: 0; font-weight: 500; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
7658
+ .oc-spec-row > dd > small { margin-inline-start: 0.25rem; font-weight: 400; color: var(--oc-muted-foreground); }
7659
+
7660
+ /* parts/data-stock.css */
7661
+ /* File: packages/ui/src/parts/data-stock.css | Map: indicator (dot + text) -> levels (ok, low, out, order) -> badge variant -> count */
7662
+ /* Stock status: a colored dot with a short text ("In stock", "Only 3 left", "Out of stock", "On order").
7663
+ data-level picks the color; the text always says it too, so color is never the only signal. */
7664
+
7665
+ .oc-stock {
7666
+ --oc-stock-color: var(--oc-success);
7667
+ /* Inline text (not flex), so "Only <b>3</b> left" keeps normal word spacing. */
7668
+ display: inline-block;
7669
+ font-size: 0.8125rem;
7670
+ font-weight: 500;
7671
+ line-height: 1.3;
7672
+ color: var(--oc-foreground);
7673
+ white-space: nowrap;
7674
+ }
7675
+ .oc-stock::before {
7676
+ content: "";
7677
+ display: inline-block;
7678
+ margin-inline-end: 0.4rem;
7679
+ vertical-align: 0.05em;
7680
+ inline-size: 0.5rem;
7681
+ block-size: 0.5rem;
7682
+ background: var(--oc-stock-color);
7683
+ border-radius: 50%;
7684
+ box-shadow: 0 0 0 3px oklch(from var(--oc-stock-color) l c h / 0.18);
7685
+ }
7686
+ .oc-stock[data-level="low"] { --oc-stock-color: var(--oc-warning); }
7687
+ .oc-stock[data-level="out"] { --oc-stock-color: var(--oc-destructive); }
7688
+ .oc-stock[data-level="order"] { --oc-stock-color: var(--oc-info, light-dark(var(--color-deepskyblue-70), var(--color-skyblue-50))); }
7689
+ .oc-stock__count { font-weight: 700; font-variant-numeric: tabular-nums; }
7690
+ .oc-stock__note { font-weight: 400; color: var(--oc-muted-foreground); }
7691
+
7692
+ /* Badge: a tinted pill (table cells, product cards). */
7693
+ .oc-stock--badge {
7694
+ padding: 0.125rem 0.5rem 0.125rem 0.45rem;
7695
+ font-size: 0.75rem;
7696
+ color: color-mix(in oklch, var(--oc-stock-color) 75%, var(--oc-foreground));
7697
+ background: oklch(from var(--oc-stock-color) l c h / 0.12);
7698
+ border: 1px solid oklch(from var(--oc-stock-color) l c h / 0.3);
7699
+ border-radius: 999px;
7700
+ }
7701
+ .oc-stock--badge::before { inline-size: 0.4rem; block-size: 0.4rem; margin-inline-end: 0.3rem; vertical-align: 0.1em; box-shadow: none; }
7702
+
7063
7703
  /* parts/forms-affix-input.css */
7064
7704
  /* File: packages/ui/src/parts/forms-affix-input.css
7065
7705
  Map: --affix group (frame on the wrapper) -> inner input -> inline affix (€, %, @) -> states -> --end alignment
@@ -7088,7 +7728,7 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7088
7728
  min-inline-size: 0;
7089
7729
  }
7090
7730
 
7091
- /* Symbol column: same chrome as the Korrekt / OmniaDesk fields (€, %, @, barcode icon).
7731
+ /* Symbol column: the same chrome as the other fields (€, %, @, barcode icon).
7092
7732
  A divider separates the symbol from the value. */
7093
7733
  .oc-input-group.oc-input-group--affix .oc-input-addon {
7094
7734
  align-self: stretch;
@@ -7137,6 +7777,51 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7137
7777
  }
7138
7778
  .oc-input-group--affix:has(.oc-input:disabled) .oc-input { opacity: 1; }
7139
7779
 
7780
+ /* parts/forms-avatar-picker.css */
7781
+ /* File: packages/ui/src/parts/forms-avatar-picker.css | Map: picker -> preview (initials or image) -> file input -> text and buttons -> drop state -> square and logo */
7782
+ /* Logo or avatar picker: a preview, "Upload" and "Remove", a hint, and dropping a file
7783
+ on the preview. The file input is a real <input type="file">; packages/ui/src/avatar-picker.js shows the preview. */
7784
+
7785
+ .oc-avatar-picker { display: flex; align-items: center; gap: 1rem; }
7786
+ .oc-avatar-picker__preview {
7787
+ position: relative;
7788
+ display: inline-flex;
7789
+ align-items: center;
7790
+ justify-content: center;
7791
+ flex-shrink: 0;
7792
+ inline-size: var(--oc-avatar-picker-size, 4.5rem);
7793
+ block-size: var(--oc-avatar-picker-size, 4.5rem);
7794
+ overflow: hidden;
7795
+ font-size: 1.25rem;
7796
+ font-weight: 600;
7797
+ color: var(--oc-muted-foreground);
7798
+ background: var(--oc-muted);
7799
+ border: 1px dashed var(--oc-border-strong, var(--oc-border));
7800
+ border-radius: 50%;
7801
+ cursor: pointer;
7802
+ }
7803
+ .oc-avatar-picker--square .oc-avatar-picker__preview { border-radius: var(--oc-radius); }
7804
+ .oc-avatar-picker__preview img { inline-size: 100%; block-size: 100%; object-fit: cover; }
7805
+ .oc-avatar-picker--logo .oc-avatar-picker__preview img { object-fit: contain; padding: 0.375rem; background: var(--color-white-50); }
7806
+ .oc-avatar-picker__preview:has(img) { border-style: solid; }
7807
+ .oc-avatar-picker__preview:hover,
7808
+ .oc-avatar-picker.is-dragover .oc-avatar-picker__preview {
7809
+ color: var(--oc-primary);
7810
+ background: oklch(from var(--oc-primary) l c h / 0.08);
7811
+ border-color: var(--oc-primary);
7812
+ }
7813
+ .oc-avatar-picker__preview:has(:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: 2px; }
7814
+ .oc-avatar-picker__file { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
7815
+ .oc-avatar-picker__text { display: grid; gap: 0.375rem; min-inline-size: 0; }
7816
+ .oc-avatar-picker__label { font-size: 0.875rem; font-weight: 600; }
7817
+ .oc-avatar-picker__hint { font-size: 0.75rem; color: var(--oc-muted-foreground); }
7818
+ .oc-avatar-picker__actions { display: flex; flex-wrap: wrap; gap: 0.375rem; }
7819
+ /* "Remove" only while there is an image. */
7820
+ .oc-avatar-picker:not(:has(.oc-avatar-picker__preview img)) [data-oc-avatar-remove] { display: none; }
7821
+ /* A refused file (wrong type or too large): red outline and the reason under the buttons. */
7822
+ .oc-avatar-picker.is-invalid .oc-avatar-picker__preview { border-color: var(--oc-destructive); }
7823
+ .oc-avatar-picker__error { font-size: 0.75rem; font-weight: 500; color: var(--oc-destructive); }
7824
+
7140
7825
  /* parts/forms-barcode-input.css */
7141
7826
  /* File: packages/ui/src/parts/forms-barcode-input.css
7142
7827
  Map: wrapper (field + preview stacked) -> monospace field -> preview area -> placeholder bars -> code caption -> empty / invalid states
@@ -7406,6 +8091,61 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7406
8091
  /* Small swatches. */
7407
8092
  .oc-color-select--sm .oc-color-select-swatch { inline-size: 1.25rem; block-size: 1.25rem; }
7408
8093
 
8094
+ /* parts/forms-date-field.css */
8095
+ /* File: packages/ui/src/parts/forms-date-field.css | Map: field frame -> icon zone -> native date input -> ISO week label -> sizes and states */
8096
+ /* Date field: calendar icon | line | date, with the ISO week at the end when wanted.
8097
+ The date is a native <input type="date"> (keyboard, picker and locale from the browser). The week label is filled
8098
+ by packages/ui/src/date-field.js (or by your own code). */
8099
+
8100
+ .oc-date-field {
8101
+ display: inline-flex;
8102
+ align-items: stretch;
8103
+ block-size: 2.5rem;
8104
+ background: var(--oc-background);
8105
+ border: 1px solid var(--oc-input, var(--oc-border));
8106
+ border-radius: var(--oc-radius);
8107
+ overflow: hidden;
8108
+ }
8109
+ .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); }
8110
+ .oc-date-field__icon {
8111
+ display: inline-flex;
8112
+ align-items: center;
8113
+ justify-content: center;
8114
+ inline-size: 2.5rem;
8115
+ font-size: 1.125rem;
8116
+ color: var(--oc-muted-foreground);
8117
+ border-inline-end: 1px solid var(--oc-border);
8118
+ }
8119
+ .oc-date-field__input {
8120
+ min-inline-size: 0;
8121
+ padding: 0 0.75rem;
8122
+ font: inherit;
8123
+ font-size: 0.875rem;
8124
+ color: var(--oc-foreground);
8125
+ background: transparent;
8126
+ border: 0;
8127
+ outline: none;
8128
+ }
8129
+ .oc-date-field__input::-webkit-calendar-picker-indicator { opacity: 0.55; cursor: pointer; }
8130
+ /* ISO week: a quiet label at the end ("Week 45"); hidden while there is no date. */
8131
+ .oc-date-field__week {
8132
+ display: inline-flex;
8133
+ align-items: center;
8134
+ padding-inline: 0.75rem;
8135
+ font-size: 0.75rem;
8136
+ font-weight: 600;
8137
+ font-variant-numeric: tabular-nums;
8138
+ color: var(--oc-primary);
8139
+ background: oklch(from var(--oc-primary) l c h / 0.08);
8140
+ border-inline-start: 1px solid var(--oc-border);
8141
+ white-space: nowrap;
8142
+ }
8143
+ .oc-date-field__week:empty { display: none; }
8144
+ .oc-date-field--sm { block-size: 2rem; }
8145
+ .oc-date-field--sm .oc-date-field__icon { inline-size: 2rem; font-size: 1rem; }
8146
+ .oc-date-field:has(.oc-date-field__input:disabled) { opacity: 0.55; }
8147
+ .oc-date-field:has(.oc-date-field__input[aria-invalid="true"]) { border-color: var(--oc-destructive); }
8148
+
7409
8149
  /* parts/forms-decimal-input.css */
7410
8150
  /* File: packages/ui/src/parts/forms-decimal-input.css
7411
8151
  Map: field (right-aligned tabular digits) -> unit suffix wrapper -> states -> --start alignment
@@ -7459,6 +8199,21 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7459
8199
  /* Disabled wrapper. */
7460
8200
  .oc-decimal-field:has(.oc-decimal-input:disabled) { opacity: 0.5; cursor: not-allowed; }
7461
8201
 
8202
+ /* parts/forms-density.css */
8203
+ /* File: packages/ui/src/parts/forms-density.css | Map: compact scope -> inputs, selects, date fields, textareas -> buttons -> field gaps */
8204
+ /* Form density: .oc-density-compact on a form or any wrapper makes its controls
8205
+ 2rem high instead of 2.5rem, for dense editors (invoice templates, inline tables). The default needs no class. */
8206
+
8207
+ .oc-density-compact :is(.oc-input, .oc-select, .oc-date-field) { block-size: 2rem; font-size: 0.8125rem; }
8208
+ .oc-density-compact :is(.oc-input, .oc-select) { padding-block: 0; }
8209
+ .oc-density-compact .oc-textarea { padding-block: 0.375rem; font-size: 0.8125rem; }
8210
+ .oc-density-compact .oc-date-field__icon { inline-size: 2rem; font-size: 1rem; }
8211
+ .oc-density-compact .oc-date-field__input { font-size: 0.8125rem; }
8212
+ .oc-density-compact .oc-btn:not(.oc-btn--icon, .oc-btn--plain, .oc-btn--inline) { padding-block: 0.3125rem; font-size: 0.8125rem; }
8213
+ .oc-density-compact .oc-btn--icon { --oc-btn-square: 2rem; }
8214
+ .oc-density-compact .oc-field { gap: 0.25rem; }
8215
+ .oc-density-compact .oc-field-label { font-size: 0.75rem; }
8216
+
7462
8217
  /* parts/forms-filter-counts.css */
7463
8218
  /* File: packages/ui/src/parts/forms-filter-counts.css
7464
8219
  Map: group (grid of tiles) -> tile (label + hidden radio) -> value / label / hint -> tones -> states -> --compact
@@ -7555,38 +8310,182 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7555
8310
  .oc-filter-counts--compact .oc-filter-counts-label { font-size: 0.8rem; font-weight: 500; }
7556
8311
  .oc-filter-counts--compact .oc-filter-counts-hint { display: none; }
7557
8312
 
7558
- /* parts/forms-language-select.css */
7559
- /* File: packages/ui/src/parts/forms-language-select.css
7560
- Map: wrapper (icon + native select) -> icon -> select control -> chevron -> states -> --ghost / --currency
7561
- Language select: compact locale (or currency) switcher for a site header.
7562
- A native <select> inside a small frame with a leading icon or symbol. The app reacts to "change". */
8313
+ /* parts/forms-form-section.css */
8314
+ /* File: packages/ui/src/parts/forms-form-section.css | Map: section (details) -> summary (icon, title, hint, status, chevron) -> body grid -> invalid state */
8315
+ /* Collapsible form section: a long form in named parts that open and close.
8316
+ Native <details>; sections with the same name="" keep one open at a time. */
7563
8317
 
7564
- /* Wrapper: inline frame; the select is stretched over it so the whole pill is clickable. */
7565
- .oc-language-select {
7566
- position: relative;
7567
- display: inline-flex;
7568
- align-items: center;
7569
- gap: 0.375rem;
7570
- padding-inline: 0.625rem 0.5rem;
7571
- border: 1px solid var(--oc-input);
7572
- border-radius: 999px;
7573
- color: var(--oc-foreground);
7574
- font-size: 0.8rem;
7575
- transition: border-color var(--oc-transition-fast), background var(--oc-transition-fast);
8318
+ .oc-form-section {
8319
+ background: var(--oc-card);
8320
+ border: 1px solid var(--oc-border);
8321
+ border-radius: var(--oc-radius);
7576
8322
  }
7577
-
7578
- .oc-language-select:hover { background: var(--oc-surface); }
7579
-
7580
- .oc-language-select:focus-within {
7581
- border-color: var(--oc-ring);
7582
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--oc-ring) 25%, transparent);
8323
+ .oc-form-section + .oc-form-section { margin-block-start: 0.75rem; }
8324
+ .oc-form-section__summary {
8325
+ display: grid;
8326
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
8327
+ align-items: center;
8328
+ gap: 0.25rem 0.75rem;
8329
+ padding: 0.875rem 1rem;
8330
+ list-style: none;
8331
+ cursor: pointer;
7583
8332
  }
7584
-
7585
- /* Leading icon (inline SVG globe) or symbol (€, $). Decorative: aria-hidden="true". */
7586
- .oc-language-select-icon {
8333
+ .oc-form-section__summary::-webkit-details-marker { display: none; }
8334
+ .oc-form-section__summary:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: -2px; border-radius: var(--oc-radius); }
8335
+ .oc-form-section__icon {
7587
8336
  display: inline-flex;
7588
- flex: none;
7589
- inline-size: 1rem;
8337
+ align-items: center;
8338
+ justify-content: center;
8339
+ inline-size: 2.25rem;
8340
+ block-size: 2.25rem;
8341
+ font-size: 1.25rem;
8342
+ color: var(--oc-primary);
8343
+ background: oklch(from var(--oc-primary) l c h / 0.1);
8344
+ border-radius: calc(var(--oc-radius) - 2px);
8345
+ }
8346
+ .oc-form-section__heading { display: grid; gap: 0.125rem; min-inline-size: 0; }
8347
+ .oc-form-section__title { margin: 0; font-size: 0.9375rem; font-weight: 600; }
8348
+ .oc-form-section__hint { margin: 0; font-size: 0.8125rem; color: var(--oc-muted-foreground); }
8349
+ .oc-form-section__status { font-size: 0.75rem; white-space: nowrap; }
8350
+ .oc-form-section__summary::after {
8351
+ content: "";
8352
+ inline-size: 1.125rem;
8353
+ block-size: 1.125rem;
8354
+ background: var(--oc-muted-foreground);
8355
+ -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;
8356
+ 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;
8357
+ transition: rotate var(--oc-transition-fast);
8358
+ }
8359
+ .oc-form-section[open] > .oc-form-section__summary::after { rotate: 180deg; }
8360
+ .oc-form-section__body {
8361
+ display: grid;
8362
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
8363
+ gap: 1rem;
8364
+ padding: 1rem;
8365
+ border-block-start: 1px solid var(--oc-border);
8366
+ }
8367
+ .oc-form-section__body > .oc-form-section__wide { grid-column: 1 / -1; }
8368
+ /* A section with an invalid field gets a red edge, also while closed, so an error is never hidden. */
8369
+ .oc-form-section:has(:user-invalid, [aria-invalid="true"]) { border-color: var(--oc-destructive); }
8370
+ .oc-form-section:has(:user-invalid, [aria-invalid="true"]) .oc-form-section__icon {
8371
+ color: var(--oc-destructive);
8372
+ background: oklch(from var(--oc-destructive) l c h / 0.1);
8373
+ }
8374
+
8375
+ @media (prefers-reduced-motion: reduce) {
8376
+ .oc-form-section__summary::after { transition: none; }
8377
+ }
8378
+
8379
+ /* parts/forms-form.css */
8380
+ /* File: packages/ui/src/parts/forms-form.css */
8381
+ /* Map: form grid -> label hint -> actions + note -> busy -> filled variant */
8382
+ /* 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
8383
+ note, the busy state while it sends (aria-busy on the form), and .oc-form--filled: fields with a fill and a stronger
8384
+ border, for a form on a card, a photo or a dark band. All through the field tokens of .oc-input. */
8385
+
8386
+ .oc-form {
8387
+ display: grid;
8388
+ gap: var(--oc-form-gap, 1.1rem);
8389
+ }
8390
+ .oc-form .oc-field-group { gap: var(--oc-form-gap, 1.1rem); }
8391
+ .oc-form .oc-field {
8392
+ gap: 0.4rem;
8393
+ margin-bottom: 0;
8394
+ }
8395
+
8396
+ /* <label class="oc-field-label">Phone <span class="oc-field-label__hint">(optional)</span></label> */
8397
+ .oc-field-label__hint {
8398
+ color: var(--oc-muted-foreground);
8399
+ font-weight: 400;
8400
+ }
8401
+
8402
+ /* Submit button with a short note next to it (wraps under it on a narrow form). */
8403
+ .oc-form-actions {
8404
+ display: flex;
8405
+ flex-wrap: wrap;
8406
+ align-items: center;
8407
+ gap: 1rem 1.5rem;
8408
+ margin-block-start: 0.4rem;
8409
+ }
8410
+ .oc-form-note {
8411
+ flex: 1 1 16rem;
8412
+ margin: 0;
8413
+ color: var(--oc-muted-foreground);
8414
+ font-size: 0.85rem;
8415
+ }
8416
+ .oc-form-note a {
8417
+ color: var(--oc-primary);
8418
+ text-decoration: underline;
8419
+ text-underline-offset: 3px;
8420
+ }
8421
+ .oc-form-note a:hover { color: var(--oc-foreground); }
8422
+
8423
+ /* While the form sends (form aria-busy="true"): the submit button dims and shows a progress cursor. */
8424
+ .oc-form[aria-busy="true"] :is([type="submit"], button:not([type])) {
8425
+ opacity: 0.7;
8426
+ cursor: progress;
8427
+ }
8428
+
8429
+ /* Filled fields: a fill (dark in dark mode), the strong border, more room and a wider focus ring. */
8430
+ .oc-form--filled {
8431
+ --oc-input-bg: light-dark(oklch(from var(--oc-foreground) l c h / 0.03), oklch(0 0 0 / 0.25));
8432
+ --oc-input-border: var(--oc-border-strong);
8433
+ --oc-input-padding: 0.75rem 0.9rem;
8434
+ --oc-input-radius: var(--oc-radius-md, 0.75rem);
8435
+ --oc-input-size: 1rem;
8436
+ --oc-ring-width: 3px;
8437
+ --oc-textarea-min: 9rem;
8438
+ }
8439
+ .oc-form--filled .oc-field-label {
8440
+ font-size: 0.9rem;
8441
+ font-weight: 600;
8442
+ line-height: 1.4;
8443
+ }
8444
+ .oc-form--filled .oc-field-error { font-size: 0.85rem; }
8445
+ .oc-form--filled .oc-select option { background: var(--oc-card); }
8446
+
8447
+ /* Honeypot: a field for bots, off screen for people. Not display:none (some bots skip hidden fields). Give the wrapper
8448
+ aria-hidden="true" and the input tabindex="-1" autocomplete="off"; the server ignores a form where it is filled. */
8449
+ .oc-honeypot {
8450
+ position: absolute;
8451
+ inset-inline-start: -10000px;
8452
+ inline-size: 1px;
8453
+ block-size: 1px;
8454
+ overflow: hidden;
8455
+ }
8456
+
8457
+ /* parts/forms-language-select.css */
8458
+ /* File: packages/ui/src/parts/forms-language-select.css
8459
+ Map: wrapper (icon + native select) -> icon -> select control -> chevron -> states -> --ghost / --currency
8460
+ Language select: compact locale (or currency) switcher for a site header.
8461
+ A native <select> inside a small frame with a leading icon or symbol. The app reacts to "change". */
8462
+
8463
+ /* Wrapper: inline frame; the select is stretched over it so the whole pill is clickable. */
8464
+ .oc-language-select {
8465
+ position: relative;
8466
+ display: inline-flex;
8467
+ align-items: center;
8468
+ gap: 0.375rem;
8469
+ padding-inline: 0.625rem 0.5rem;
8470
+ border: 1px solid var(--oc-input);
8471
+ border-radius: 999px;
8472
+ color: var(--oc-foreground);
8473
+ font-size: 0.8rem;
8474
+ transition: border-color var(--oc-transition-fast), background var(--oc-transition-fast);
8475
+ }
8476
+
8477
+ .oc-language-select:hover { background: var(--oc-surface); }
8478
+
8479
+ .oc-language-select:focus-within {
8480
+ border-color: var(--oc-ring);
8481
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--oc-ring) 25%, transparent);
8482
+ }
8483
+
8484
+ /* Leading icon (inline SVG globe) or symbol (€, $). Decorative: aria-hidden="true". */
8485
+ .oc-language-select-icon {
8486
+ display: inline-flex;
8487
+ flex: none;
8488
+ inline-size: 1rem;
7590
8489
  justify-content: center;
7591
8490
  color: var(--oc-muted-foreground);
7592
8491
  font-weight: 600;
@@ -7890,6 +8789,146 @@ button.oc-language-select:hover { background: var(--oc-surface); }
7890
8789
  font-size: 0.8rem;
7891
8790
  }
7892
8791
 
8792
+ /* parts/forms-quantity.css */
8793
+ /* File: packages/ui/src/parts/forms-quantity.css | Map: frame -> step buttons -> number input (no spinners) -> focus and disabled -> sizes */
8794
+ /* Quantity field: minus, a number input and plus in one frame. Without a script it is a normal number input
8795
+ (the browser's arrows and keyboard); form-extras.js makes the buttons step within min / max / step. */
8796
+
8797
+ .oc-quantity {
8798
+ display: inline-flex;
8799
+ inline-size: fit-content;
8800
+ justify-self: start;
8801
+ align-items: stretch;
8802
+ block-size: 2.25rem;
8803
+ background: var(--oc-background);
8804
+ border: 1px solid var(--oc-input, var(--oc-border));
8805
+ border-radius: var(--oc-radius);
8806
+ overflow: hidden;
8807
+ }
8808
+ .oc-quantity__step {
8809
+ display: inline-grid;
8810
+ place-items: center;
8811
+ flex-shrink: 0;
8812
+ inline-size: 2.25rem;
8813
+ padding: 0;
8814
+ font: inherit;
8815
+ color: var(--oc-muted-foreground);
8816
+ background: transparent;
8817
+ border: 0;
8818
+ cursor: pointer;
8819
+ }
8820
+ .oc-quantity__step > :is(svg, iconify-icon) { inline-size: 1rem; block-size: 1rem; font-size: 1rem; }
8821
+ .oc-quantity__step:hover { color: var(--oc-foreground); background: var(--oc-accent); }
8822
+ .oc-quantity__step:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: -2px; }
8823
+ .oc-quantity__step:disabled { opacity: 0.35; cursor: not-allowed; background: transparent; }
8824
+ .oc-quantity__input {
8825
+ inline-size: 3.25rem;
8826
+ min-inline-size: 0;
8827
+ padding: 0 0.25rem;
8828
+ font: inherit;
8829
+ font-size: 0.875rem;
8830
+ font-weight: 600;
8831
+ font-variant-numeric: tabular-nums;
8832
+ text-align: center;
8833
+ color: var(--oc-foreground);
8834
+ background: transparent;
8835
+ border: 0;
8836
+ border-inline: 1px solid var(--oc-border);
8837
+ -moz-appearance: textfield;
8838
+ appearance: textfield;
8839
+ }
8840
+ .oc-quantity__input::-webkit-outer-spin-button,
8841
+ .oc-quantity__input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
8842
+ .oc-quantity__input:focus { outline: none; }
8843
+ .oc-quantity:has(.oc-quantity__input:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
8844
+ .oc-quantity:has(.oc-quantity__input:disabled) { opacity: 0.5; }
8845
+ .oc-quantity--sm { block-size: 2rem; }
8846
+ .oc-quantity--sm .oc-quantity__step { inline-size: 2rem; }
8847
+ .oc-quantity--sm .oc-quantity__input { inline-size: 2.75rem; font-size: 0.8125rem; }
8848
+
8849
+ /* parts/forms-rating.css */
8850
+ /* File: packages/ui/src/parts/forms-rating.css | Map: star mask -> rating (display) -> sizes -> summary line -> rating input (radios, hover preview) -> focus */
8851
+ /* Star rating without JavaScript. .oc-rating shows a score (--oc-rating, halves and decimals work); .oc-rating-input
8852
+ is five radio buttons drawn as stars: :has() fills up to the checked or hovered star. */
8853
+
8854
+ .oc-rating,
8855
+ .oc-rating-input {
8856
+ --oc-rating-size: 1.125rem;
8857
+ --oc-rating-color: var(--oc-warning);
8858
+ --oc-rating-empty: oklch(from var(--oc-muted-foreground) l c h / 0.28);
8859
+ --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");
8860
+ }
8861
+
8862
+ /* Display: one bar of five stars; the fill is the score out of --oc-rating-max (default 5). */
8863
+ .oc-rating {
8864
+ --oc-rating: 0;
8865
+ --oc-rating-max: 5;
8866
+ display: inline-block;
8867
+ flex-shrink: 0;
8868
+ inline-size: calc(var(--oc-rating-size) * var(--oc-rating-max));
8869
+ block-size: var(--oc-rating-size);
8870
+ vertical-align: middle;
8871
+ background: linear-gradient(90deg, var(--oc-rating-color) calc(var(--oc-rating) / var(--oc-rating-max) * 100%), var(--oc-rating-empty) 0);
8872
+ -webkit-mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
8873
+ mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
8874
+ }
8875
+ .oc-rating--sm, .oc-rating-input.oc-rating-input--sm { --oc-rating-size: 0.875rem; }
8876
+ .oc-rating--lg, .oc-rating-input.oc-rating-input--lg { --oc-rating-size: 1.5rem; }
8877
+
8878
+ /* Summary: stars, the score and the number of reviews on one line. */
8879
+ .oc-rating-summary { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.5rem; font-size: 0.875rem; }
8880
+ .oc-rating-summary__score { font-weight: 700; font-variant-numeric: tabular-nums; }
8881
+ .oc-rating-summary__count { color: var(--oc-muted-foreground); }
8882
+ a.oc-rating-summary__count { text-decoration: underline; text-underline-offset: 0.2em; }
8883
+ a.oc-rating-summary__count:hover { color: var(--oc-foreground); }
8884
+
8885
+ /* Input: a fieldset with five labels (radio inside). The stars are one masked bar behind the labels. */
8886
+ .oc-rating-input {
8887
+ position: relative;
8888
+ display: inline-flex;
8889
+ inline-size: calc(var(--oc-rating-size) * 5 + 0.5rem);
8890
+ block-size: calc(var(--oc-rating-size) + 0.5rem);
8891
+ margin: 0;
8892
+ padding: 0.25rem;
8893
+ border: 0;
8894
+ border-radius: calc(var(--oc-radius) - 2px);
8895
+ }
8896
+ .oc-rating-input::before {
8897
+ content: "";
8898
+ position: absolute;
8899
+ inset: 0.25rem;
8900
+ 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);
8901
+ -webkit-mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
8902
+ mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
8903
+ pointer-events: none;
8904
+ }
8905
+ .oc-rating-input > legend { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
8906
+ .oc-rating-input > label { position: relative; z-index: 1; flex: 0 0 var(--oc-rating-size); cursor: pointer; }
8907
+ .oc-rating-input > label > input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
8908
+ .oc-rating-input > label > span { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
8909
+
8910
+ .oc-rating-input:has(label:nth-of-type(1) input:checked) { --oc-rating-pick: 1; }
8911
+ .oc-rating-input:has(label:nth-of-type(2) input:checked) { --oc-rating-pick: 2; }
8912
+ .oc-rating-input:has(label:nth-of-type(3) input:checked) { --oc-rating-pick: 3; }
8913
+ .oc-rating-input:has(label:nth-of-type(4) input:checked) { --oc-rating-pick: 4; }
8914
+ .oc-rating-input:has(label:nth-of-type(5) input:checked) { --oc-rating-pick: 5; }
8915
+ /* Hover preview (pointer devices only, so a tap does not leave a preview behind). */
8916
+ @media (hover: hover) {
8917
+ .oc-rating-input:has(label:nth-of-type(1):hover) { --oc-rating-hover: 1; }
8918
+ .oc-rating-input:has(label:nth-of-type(2):hover) { --oc-rating-hover: 2; }
8919
+ .oc-rating-input:has(label:nth-of-type(3):hover) { --oc-rating-hover: 3; }
8920
+ .oc-rating-input:has(label:nth-of-type(4):hover) { --oc-rating-hover: 4; }
8921
+ .oc-rating-input:has(label:nth-of-type(5):hover) { --oc-rating-hover: 5; }
8922
+ }
8923
+ .oc-rating-input:has(input:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
8924
+ .oc-rating-input:has(input:disabled) { opacity: 0.5; }
8925
+ .oc-rating-input:has(input:disabled) > label,
8926
+ .oc-rating-input:has(input:disabled) > label > input { cursor: not-allowed; }
8927
+
8928
+ @media (forced-colors: active) {
8929
+ .oc-rating, .oc-rating-input::before { --oc-rating-color: CanvasText; --oc-rating-empty: GrayText; forced-color-adjust: none; }
8930
+ }
8931
+
7893
8932
  /* parts/forms-time-input.css */
7894
8933
  /* File: packages/ui/src/parts/forms-time-input.css
7895
8934
  Map: 24h text field (HH:mm) -> invalid state -> slot list (radio chips) -> slot states -> --sm
@@ -8064,7 +9103,7 @@ button.oc-language-select:hover { background: var(--oc-surface); }
8064
9103
  /* File: packages/ui/src/parts/media-attachment.css | Map: list -> file chip (icon by type, name, size, open/remove) -> type tones
8065
9104
  -> uploading progress + error -> image preview (in a message) -> thumbnail chip (in a composer) -> reduced motion
8066
9105
  Attachment: a file chip or an image preview inside a chat message or a composer. The icon tile shows the extension and is
8067
- tinted per type (--pdf, --doc, --sheet, --zip, --image). Based on the attachment picker of the Korrekt question chat. */
9106
+ tinted per type (--pdf, --doc, --sheet, --zip, --image). */
8068
9107
 
8069
9108
  .oc-attachment-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; padding: 0; list-style: none; }
8070
9109
  .oc-attachment-list--stack { flex-direction: column; flex-wrap: nowrap; }
@@ -8222,6 +9261,31 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
8222
9261
  .oc-attachment, .oc-attachment--uploading::after { transition: none; }
8223
9262
  }
8224
9263
 
9264
+ /* parts/media-barcode.css */
9265
+ /* File: packages/ui/src/parts/media-barcode.css | Map: barcode figure -> svg bars -> digits -> sizes (hero, compact) -> error */
9266
+ /* Barcode display: EAN-13, EAN-8, UPC-A or Code 128, drawn as SVG by
9267
+ packages/ui/src/barcode.js from data-value. Always black bars on white, in both themes, so scanners read it. */
9268
+
9269
+ .oc-barcode {
9270
+ display: inline-flex;
9271
+ flex-direction: column;
9272
+ align-items: center;
9273
+ gap: 0.25rem;
9274
+ margin: 0;
9275
+ padding: 0.75rem 1rem;
9276
+ color: var(--color-black-50);
9277
+ background: var(--color-white-50);
9278
+ border: 1px solid var(--oc-border);
9279
+ border-radius: var(--oc-radius);
9280
+ }
9281
+ .oc-barcode svg { display: block; inline-size: auto; block-size: var(--oc-barcode-height, 4rem); max-inline-size: 100%; }
9282
+ .oc-barcode__digits { font-family: var(--oc-font-mono); font-size: 0.8125rem; letter-spacing: 0.18em; color: inherit; }
9283
+ .oc-barcode--hero { padding: 1.25rem 1.5rem; --oc-barcode-height: 6rem; }
9284
+ .oc-barcode--hero .oc-barcode__digits { font-size: 1rem; }
9285
+ .oc-barcode--compact { padding: 0.375rem 0.5rem; --oc-barcode-height: 2.25rem; }
9286
+ .oc-barcode--compact .oc-barcode__digits { font-size: 0.6875rem; letter-spacing: 0.1em; }
9287
+ .oc-barcode[data-error] .oc-barcode__digits { color: var(--color-red-70); letter-spacing: 0; }
9288
+
8225
9289
  /* parts/media-browser-frame.css */
8226
9290
  /* File: packages/ui/src/parts/media-browser-frame.css | Map: frame -> chrome bar (dots, address pill, actions)
8227
9291
  -> view (screenshot slot, ratio) -> caption -> minimal / dark / tilt modifiers -> motion
@@ -8441,6 +9505,9 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
8441
9505
  line-height: 1;
8442
9506
  cursor: pointer;
8443
9507
  }
9508
+ /* Webicons in the buttons (inline SVG or <iconify-icon>): a fixed size, so nothing is cut off at the round edge. */
9509
+ .oc-chat-composer__tool > :is(svg, iconify-icon) { inline-size: 1.25rem; block-size: 1.25rem; font-size: 1.25rem; }
9510
+ .oc-chat-composer__send > :is(svg, iconify-icon) { inline-size: 1.125rem; block-size: 1.125rem; font-size: 1.125rem; }
8444
9511
  .oc-chat-composer__tool:hover { background: var(--oc-surface-strong); color: var(--oc-foreground); }
8445
9512
  .oc-chat-composer__tool:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
8446
9513
 
@@ -8645,9 +9712,11 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
8645
9712
  background: transparent;
8646
9713
  font-size: 1.15rem;
8647
9714
  line-height: 1;
9715
+ color: var(--oc-muted-foreground);
8648
9716
  cursor: pointer;
8649
9717
  }
8650
- .oc-emoji-picker__trigger:hover { background: var(--oc-surface-strong); }
9718
+ .oc-emoji-picker__trigger > :is(svg, iconify-icon) { inline-size: 1.25rem; block-size: 1.25rem; font-size: 1.25rem; }
9719
+ .oc-emoji-picker__trigger:hover { color: var(--oc-foreground); background: var(--oc-surface-strong); }
8651
9720
  .oc-emoji-picker__trigger:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
8652
9721
 
8653
9722
  /* The panel: floats above the trigger (implicit anchor of popovertarget), flips when there is no room. */
@@ -8851,6 +9920,216 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
8851
9920
  .oc-flip-card--hover:is(:hover, :focus-within) .oc-flip-card__face--back { opacity: 1; visibility: visible; }
8852
9921
  }
8853
9922
 
9923
+ /* parts/media-gallery.css */
9924
+ /* 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) */
9925
+ /* Product gallery without JavaScript: one large photo and a row of thumbnails. Every thumbnail is a label with a
9926
+ radio; :has() shows the slide at the same position. Arrow keys move between the photos (native radios). */
9927
+
9928
+ .oc-gallery { container-type: inline-size; inline-size: 100%; display: grid; gap: 0.75rem; min-inline-size: 0; }
9929
+ .oc-gallery__stage {
9930
+ position: relative;
9931
+ aspect-ratio: var(--oc-gallery-ratio, 4 / 3);
9932
+ overflow: hidden;
9933
+ background: var(--oc-muted);
9934
+ border: 1px solid var(--oc-border);
9935
+ border-radius: calc(var(--oc-radius) + 4px);
9936
+ }
9937
+ .oc-gallery__slide { display: none; margin: 0; block-size: 100%; }
9938
+ .oc-gallery__slide > img { display: block; inline-size: 100%; block-size: 100%; object-fit: var(--oc-gallery-fit, contain); }
9939
+ .oc-gallery:not(:has(.oc-gallery__thumb input:checked)) .oc-gallery__slide:first-of-type,
9940
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(1) input:checked) .oc-gallery__slide:nth-of-type(1),
9941
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(2) input:checked) .oc-gallery__slide:nth-of-type(2),
9942
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(3) input:checked) .oc-gallery__slide:nth-of-type(3),
9943
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(4) input:checked) .oc-gallery__slide:nth-of-type(4),
9944
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(5) input:checked) .oc-gallery__slide:nth-of-type(5),
9945
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(6) input:checked) .oc-gallery__slide:nth-of-type(6),
9946
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(7) input:checked) .oc-gallery__slide:nth-of-type(7),
9947
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(8) input:checked) .oc-gallery__slide:nth-of-type(8) { display: block; }
9948
+ .oc-gallery__badge { position: absolute; inset: 0.75rem auto auto 0.75rem; z-index: 1; }
9949
+
9950
+ .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; }
9951
+ .oc-gallery__thumbs > legend { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
9952
+ .oc-gallery__thumb {
9953
+ position: relative;
9954
+ flex: 0 0 auto;
9955
+ inline-size: var(--oc-gallery-thumb, 4rem);
9956
+ aspect-ratio: 1;
9957
+ overflow: hidden;
9958
+ background: var(--oc-muted);
9959
+ border: 2px solid transparent;
9960
+ border-radius: var(--oc-radius);
9961
+ outline: 1px solid var(--oc-border);
9962
+ outline-offset: -1px;
9963
+ cursor: pointer;
9964
+ }
9965
+ .oc-gallery__thumb > img { display: block; inline-size: 100%; block-size: 100%; object-fit: cover; }
9966
+ .oc-gallery__thumb > input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
9967
+ .oc-gallery__thumb:hover { outline-color: var(--oc-muted-foreground); }
9968
+ .oc-gallery__thumb:has(input:checked) { border-color: var(--oc-primary); outline-color: transparent; }
9969
+ .oc-gallery__thumb:has(input:focus-visible) { box-shadow: 0 0 0 2px var(--oc-background), 0 0 0 4px var(--oc-ring); }
9970
+
9971
+ /* Side: thumbnails in a column on the left once the gallery itself is 32rem wide or more. The grid is always two
9972
+ tracks; below 32rem both children span them, so the query only moves the children (a container cannot query itself). */
9973
+ .oc-gallery--side { grid-template-columns: auto minmax(0, 1fr); }
9974
+ .oc-gallery--side > * { grid-column: 1 / -1; }
9975
+ @container (min-width: 32rem) {
9976
+ .oc-gallery--side > .oc-gallery__stage { grid-column: 2; grid-row: 1; }
9977
+ .oc-gallery--side > .oc-gallery__thumbs { grid-column: 1; grid-row: 1; flex-direction: column; overflow: visible; }
9978
+ }
9979
+
9980
+ /* parts/media-glass-liquid.css */
9981
+ /* File: packages/ui/src/parts/media-glass-liquid.css
9982
+ Map: liquid sheet (.oc-glass--liquid: clear tint, strong blur + saturation, inner light, specular rim) -> menu bar
9983
+ (.oc-glass-bar: items with icon and label, the current one in a raised bubble) -> icon-only and round button -> fallbacks
9984
+ Liquid glass in the style of recent desktop and phone systems, in CSS only: the backdrop is blurred and brightened,
9985
+ the edge catches light. Put it over a photo or colourful content; on a plain page it looks like a soft card. */
9986
+
9987
+ .oc-glass.oc-glass--liquid,
9988
+ .oc-glass--liquid,
9989
+ .oc-glass-bar,
9990
+ .oc-site-header--glass .oc-site-header__inner {
9991
+ --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.1), oklch(100% 0 0deg / 0.05));
9992
+ --oc-liquid-edge: light-dark(oklch(100% 0 0deg / 0.55), oklch(100% 0 0deg / 0.22));
9993
+ --oc-liquid-blur: 5px;
9994
+ position: relative;
9995
+ isolation: isolate;
9996
+ background: var(--oc-liquid-tint);
9997
+ border: 1px solid var(--oc-liquid-edge);
9998
+ /* Light enters at the top left and leaves at the bottom right; a soft glow runs along the inner edge,
9999
+ so the sheet reads as a thick, clear lens rather than frosted plastic. */
10000
+ box-shadow:
10001
+ 0 10px 30px -10px oklch(0% 0 0deg / 0.35),
10002
+ inset 2px 2px 1px -2px oklch(100% 0 0deg / 0.95),
10003
+ inset -2px -2px 1px -2px oklch(100% 0 0deg / 0.7),
10004
+ inset 0 0 10px 1px oklch(100% 0 0deg / 0.22),
10005
+ inset 0 -8px 16px -12px oklch(100% 0 0deg / 0.35);
10006
+ backdrop-filter: blur(var(--oc-liquid-blur)) saturate(1.9) brightness(1.1) contrast(1.05);
10007
+ }
10008
+ /* Specular rim: bright on the top-left and bottom-right edge, only on the border ring. */
10009
+ .oc-glass--liquid::before,
10010
+ .oc-glass-bar::before,
10011
+ .oc-site-header--glass .oc-site-header__inner::before {
10012
+ content: "";
10013
+ position: absolute;
10014
+ inset: -1px;
10015
+ z-index: 1;
10016
+ padding: 1.5px;
10017
+ border-radius: inherit;
10018
+ 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));
10019
+ mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
10020
+ mask-composite: exclude;
10021
+ pointer-events: none;
10022
+ }
10023
+
10024
+ /* Menu bar: a pill of items, each an icon over a short label. */
10025
+ .oc-glass-bar {
10026
+ display: inline-flex;
10027
+ align-items: center;
10028
+ gap: 0.2rem;
10029
+ padding: 0.35rem;
10030
+ border-radius: 999px;
10031
+ }
10032
+ .oc-glass-bar__item {
10033
+ position: relative;
10034
+ display: grid;
10035
+ justify-items: center;
10036
+ gap: 0.15rem;
10037
+ min-inline-size: 4.25rem;
10038
+ padding: 0.45rem 0.85rem;
10039
+ border: 0;
10040
+ border-radius: 999px;
10041
+ color: light-dark(oklch(22% 0.02 260deg / 0.78), oklch(100% 0 0deg / 0.82));
10042
+ background: none;
10043
+ font: inherit;
10044
+ font-size: 0.7rem;
10045
+ font-weight: 600;
10046
+ line-height: 1.1;
10047
+ text-decoration: none;
10048
+ cursor: pointer;
10049
+ transition: background-color 0.25s, color 0.25s, box-shadow 0.25s, scale 0.2s;
10050
+ }
10051
+ .oc-glass-bar__item > :is(svg, iconify-icon) { inline-size: 1.4rem; block-size: 1.4rem; font-size: 1.4rem; }
10052
+ .oc-glass-bar__item:hover { background: light-dark(oklch(100% 0 0deg / 0.28), oklch(100% 0 0deg / 0.08)); }
10053
+ .oc-glass-bar__item:active { scale: 0.94; }
10054
+ .oc-glass-bar__item:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
10055
+ /* The current item sits in a raised, lighter bubble. */
10056
+ .oc-glass-bar__item:is([aria-current="page"], [aria-pressed="true"], :has(> input:checked)) {
10057
+ color: light-dark(var(--oc-primary), var(--color-white-50));
10058
+ background: light-dark(oklch(100% 0 0deg / 0.62), oklch(100% 0 0deg / 0.24));
10059
+ box-shadow: inset 0 1px 1px oklch(100% 0 0deg / 0.85), 0 6px 14px -8px oklch(0% 0 0deg / 0.4);
10060
+ }
10061
+ .oc-glass-bar__item > input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
10062
+ .oc-glass-bar__item:has(> input:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
10063
+
10064
+ /* Icons only, and a separate round button next to the bar (search, compose). */
10065
+ .oc-glass-bar--icons .oc-glass-bar__item { min-inline-size: 0; padding: 0.6rem; }
10066
+ .oc-glass-bar--round { padding: 0.35rem; border-radius: 999px; }
10067
+ .oc-glass-bar--round .oc-glass-bar__item { min-inline-size: 0; padding: 0.6rem; }
10068
+ .oc-glass-bar__sep { align-self: stretch; inline-size: 1px; margin: 0.4rem 0.15rem; background: var(--oc-liquid-edge); }
10069
+
10070
+ /* Less transparency asked for, or no backdrop-filter: a solid bar that still reads as one. */
10071
+ @media (prefers-reduced-transparency: reduce) {
10072
+ .oc-glass--liquid, .oc-glass-bar { --oc-liquid-tint: var(--oc-card); backdrop-filter: none; }
10073
+ }
10074
+ @supports not (backdrop-filter: blur(1px)) {
10075
+ .oc-glass--liquid, .oc-glass-bar { --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.85), oklch(25% 0.02 260deg / 0.9)); }
10076
+ }
10077
+ @media (prefers-reduced-motion: reduce) {
10078
+ .oc-glass-bar__item { transition: none; }
10079
+ }
10080
+
10081
+ /* parts/media-glass-options.css */
10082
+ /* File: packages/ui/src/parts/media-glass-options.css
10083
+ Map: amount (--clear: more see-through, --frost: more frosted) -> light content on a colourful backdrop -> stage (moving colour blobs to show it off)
10084
+ Extras for .oc-glass, .oc-glass--liquid and .oc-glass-bar (media-glass.css, media-glass-liquid.css). The selectors
10085
+ carry two classes so they win over the base glass whatever order the parts load in. */
10086
+
10087
+ /* Amount of glass: --clear lets more of the backdrop through, --frost hides more of it. */
10088
+ :is(.oc-glass, .oc-glass-bar).oc-glass--clear {
10089
+ --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));
10090
+ --oc-glass-blur: 10px;
10091
+ --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.05), oklch(100% 0 0deg / 0.03));
10092
+ --oc-liquid-blur: 2px;
10093
+ }
10094
+ :is(.oc-glass, .oc-glass-bar).oc-glass--frost {
10095
+ --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));
10096
+ --oc-glass-blur: 40px;
10097
+ --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.38), oklch(100% 0 0deg / 0.12));
10098
+ --oc-liquid-blur: 18px;
10099
+ }
10100
+
10101
+
10102
+ /* Light content: white icons and labels for glass over a dark or very colourful backdrop. */
10103
+ .oc-glass-bar.oc-glass-bar--light .oc-glass-bar__item {
10104
+ color: oklch(100% 0 0deg / 0.92);
10105
+ text-shadow: 0 1px 2px oklch(0% 0 0deg / 0.25);
10106
+ }
10107
+ .oc-glass-bar.oc-glass-bar--light .oc-glass-bar__item:is([aria-current="page"], [aria-pressed="true"], :has(> input:checked)) {
10108
+ color: var(--color-white-50);
10109
+ background: oklch(100% 0 0deg / 0.2);
10110
+ }
10111
+ .oc-glass-bar.oc-glass-bar--light .oc-glass-bar__item:hover { background: oklch(100% 0 0deg / 0.12); }
10112
+
10113
+ /* Stage: round colour shapes that drift slowly, so the change through the glass shows. A background for a hero or a demo. */
10114
+ .oc-glass-stage {
10115
+ position: relative;
10116
+ overflow: hidden;
10117
+ background:
10118
+ radial-gradient(circle at 22% 72%, var(--color-magenta-50) 0 16%, transparent 16.5%),
10119
+ radial-gradient(circle at 82% 68%, var(--color-orange-50) 0 18%, transparent 18.5%),
10120
+ radial-gradient(circle at 58% 18%, var(--color-skycyan-50) 0 13%, transparent 13.5%),
10121
+ radial-gradient(60% 70% at 30% 30%, var(--color-magenta-60), transparent 70%),
10122
+ linear-gradient(120deg, var(--color-purple-60), var(--color-violet-80));
10123
+ background-size: 140% 140%;
10124
+ animation: oc-glass-stage 18s ease-in-out infinite alternate;
10125
+ }
10126
+ @keyframes oc-glass-stage {
10127
+ to { background-position: 100% 60%; }
10128
+ }
10129
+ @media (prefers-reduced-motion: reduce) {
10130
+ .oc-glass-stage { animation: none; }
10131
+ }
10132
+
8854
10133
  /* parts/media-glass.css */
8855
10134
  /* File: packages/ui/src/parts/media-glass.css
8856
10135
  Map: tokens (light-dark) -> sheet (.oc-glass: see-through, blurred, light top edge, soft shadow) -> panel (.oc-glass-panel:
@@ -8994,6 +10273,54 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
8994
10273
  .oc-image-zoom__canvas { transition: inline-size var(--oc-transition-normal); }
8995
10274
  }
8996
10275
 
10276
+ /* parts/media-lazy.css */
10277
+ /* 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
10278
+ Lazy loading. .oc-lazy: an image box that keeps its place, shows a shimmer (or --oc-lazy-preview, a tiny blurred
10279
+ copy) until the photo is there and then fades it in (lazy.js adds .is-loaded). .oc-lazy-section: content the browser
10280
+ only lays out and draws when it comes near the screen (content-visibility). */
10281
+
10282
+ .oc-lazy {
10283
+ --oc-lazy-ratio: 16 / 10;
10284
+ position: relative;
10285
+ display: block;
10286
+ margin: 0;
10287
+ overflow: hidden;
10288
+ aspect-ratio: var(--oc-lazy-ratio);
10289
+ border-radius: var(--oc-radius);
10290
+ background:
10291
+ linear-gradient(100deg, transparent 30%, oklch(from var(--oc-background) l c h / 0.55) 50%, transparent 70%) -150% 0 / 60% 100% no-repeat,
10292
+ var(--oc-muted);
10293
+ animation: oc-lazy-shimmer 1.4s ease-in-out infinite;
10294
+ }
10295
+ /* A tiny blurred preview instead of the shimmer: style="--oc-lazy-preview: url(photo-20px.webp)". */
10296
+ .oc-lazy[style*="--oc-lazy-preview"] { animation: none; background: var(--oc-muted); }
10297
+ .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; }
10298
+ .oc-lazy > img { position: relative; display: block; inline-size: 100%; block-size: 100%; object-fit: cover; opacity: 0; transition: opacity 0.45s ease; }
10299
+ .oc-lazy.is-loaded { animation: none; background: none; }
10300
+ .oc-lazy.is-loaded::before { display: none; }
10301
+ .oc-lazy.is-loaded > img { opacity: 1; }
10302
+ .oc-lazy.is-error { animation: none; }
10303
+ .oc-lazy.is-error > img { opacity: 0; }
10304
+ .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)); }
10305
+ /* Without the script (or when it is off) the photo shows as soon as it is there. */
10306
+ @media (scripting: none) { .oc-lazy > img { opacity: 1; } }
10307
+ @keyframes oc-lazy-shimmer { to { background-position: 250% 0, 0 0; } }
10308
+
10309
+ /* Sections far down a long page: laid out and drawn only when they come near the screen. */
10310
+ .oc-lazy-section { content-visibility: auto; contain-intrinsic-size: auto var(--oc-lazy-size, 600px); }
10311
+
10312
+ @media (prefers-reduced-motion: reduce) {
10313
+ .oc-lazy { animation: none; }
10314
+ .oc-lazy > img { transition: none; }
10315
+ }
10316
+
10317
+ /* In a table or a list: a small square thumbnail, and placeholder rows (.oc-lazy-row) while the next page loads. */
10318
+ .oc-lazy.oc-lazy--thumb { --oc-lazy-ratio: 1; inline-size: 2.5rem; flex: none; border-radius: calc(var(--oc-radius) - 2px); }
10319
+ .oc-lazy-row > :is(td, th) { padding-block: 0.6rem; }
10320
+ .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; }
10321
+ @keyframes oc-lazy-pulse { to { opacity: 0.45; } }
10322
+ @media (prefers-reduced-motion: reduce) { .oc-lazy-row .oc-lazy-bar { animation: none; } }
10323
+
8997
10324
  /* parts/media-lightbox.css */
8998
10325
  /* File: packages/ui/src/parts/media-lightbox.css | Map: thumbnails -> dialog + backdrop -> bar -> track + slides
8999
10326
  -> nav buttons -> caption + counter -> single image -> small screens -> motion
@@ -9094,6 +10421,10 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
9094
10421
  .oc-lightbox__nav { grid-row: 1; justify-self: center; }
9095
10422
  .oc-lightbox__nav--prev { grid-column: 1; }
9096
10423
  .oc-lightbox__nav--next { grid-column: 3; }
10424
+ .oc-lightbox__close :is(svg, iconify-icon, .oc-icon) { inline-size: 1.1rem; block-size: 1.1rem; font-size: 1.1rem; }
10425
+ /* A <picture> (animated WebP with a still image for "reduce motion") gets the frame; the image fits in it. */
10426
+ .oc-lightbox__media > picture { display: grid; overflow: hidden; }
10427
+ .oc-lightbox__media > picture > img { max-inline-size: 100%; max-block-size: calc(100dvh - 9rem); object-fit: contain; }
9097
10428
 
9098
10429
  /* ---- Caption with the "2 / 5" counter ---- */
9099
10430
  .oc-lightbox__caption {
@@ -9172,7 +10503,7 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
9172
10503
  people, status, actions) -> wallpaper (masked doodles in a token color) -> scroll area (starts at the bottom)
9173
10504
  -> composer footer -> sending overlay (aria-busy) -> reduced motion
9174
10505
  Message thread: one conversation. The scroll area uses flex-direction: column-reverse, so it opens at the newest message
9175
- and stays there when messages are added, without a script. Based on the question chat of the Korrekt app. */
10506
+ and stays there when messages are added, without a script. */
9176
10507
 
9177
10508
  .oc-message-thread {
9178
10509
  --oc-message-thread-height: 32rem;
@@ -9310,7 +10641,7 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
9310
10641
  -> grouping (:has) -> hover actions -> date divider + system line -> reduced motion
9311
10642
  Message: one chat bubble. Incoming on the left with the avatar, outgoing on the right in the primary tint.
9312
10643
  Consecutive messages of one sender group by themselves: the avatar shows once (on the last one), the name once (on the
9313
- first one). Based on the question chat of the Korrekt app. */
10644
+ first one). */
9314
10645
 
9315
10646
  .oc-message-list {
9316
10647
  --oc-message-in: var(--oc-card);
@@ -9880,6 +11211,382 @@ a.oc-message__quote:focus-visible { outline: 2px solid var(--oc-ring); outline-o
9880
11211
  .oc-watermark { --oc-watermark-opacity: 0.2; print-color-adjust: exact; }
9881
11212
  }
9882
11213
 
11214
+ /* parts/nav-topbar-variants.css */
11215
+ /* File: packages/ui/src/parts/nav-topbar-variants.css
11216
+ Map: brand (logo tile, room before the menu) -> centred menu -> plain links -> dark / brand colour -> floating pill
11217
+ -> strip above the bar -> footer bar (split, links, dark)
11218
+ Variants of the site header (.oc-topbar in docs-ui.css) and the footer bar (.oc-footerbar). Colours are tokens. */
11219
+
11220
+ /* Brand: a logo tile and the name, always with room before the menu. */
11221
+ .oc-topbar-brand { display: inline-flex; align-items: center; gap: 0.55rem; margin-inline-end: 1rem; }
11222
+ .oc-topbar-logo {
11223
+ display: grid;
11224
+ place-items: center;
11225
+ flex: none;
11226
+ inline-size: 1.85rem;
11227
+ block-size: 1.85rem;
11228
+ border-radius: 9px;
11229
+ color: var(--color-white-50);
11230
+ background: linear-gradient(135deg, var(--oc-accent-from, var(--oc-primary)), var(--oc-accent-to, var(--oc-primary)));
11231
+ font-size: 0.95rem;
11232
+ }
11233
+ .oc-topbar-logo > :is(svg, iconify-icon) { inline-size: 1.1rem; block-size: 1.1rem; font-size: 1.1rem; }
11234
+ .oc-topbar-nav { min-inline-size: 0; }
11235
+ .oc-topbar-actions .oc-btn { white-space: nowrap; }
11236
+
11237
+ /* Centred menu: brand left, menu in the middle, actions right (from 761px; smaller screens keep the burger). */
11238
+ @media (min-width: 761px) {
11239
+ .oc-topbar--center { display: grid; grid-template-columns: 1fr auto 1fr; }
11240
+ .oc-topbar--center .oc-topbar-brand { justify-self: start; }
11241
+ .oc-topbar--center .oc-topbar-actions { justify-self: end; }
11242
+ }
11243
+
11244
+ /* Plain links: no pill frames, the current page underlined. */
11245
+ .oc-topbar--plain .oc-navmenu--site .oc-navmenu-link {
11246
+ border-color: transparent;
11247
+ border-radius: 0;
11248
+ background: none;
11249
+ color: var(--oc-muted-foreground);
11250
+ box-shadow: inset 0 -2px 0 transparent;
11251
+ }
11252
+ .oc-topbar--plain .oc-navmenu--site .oc-navmenu-link:hover { color: var(--oc-foreground); border-color: transparent; background: none; }
11253
+ .oc-topbar--plain .oc-navmenu--site .oc-navmenu-link[aria-current="page"] {
11254
+ color: var(--oc-foreground);
11255
+ border-color: transparent;
11256
+ background: none;
11257
+ box-shadow: inset 0 -2px 0 var(--oc-primary);
11258
+ }
11259
+
11260
+ /* Dark bar and brand-colour bar: light text, links as soft pills. */
11261
+ .oc-topbar:is(.oc-topbar--dark, .oc-topbar--brand) { color: var(--color-white-50); border-block-end-color: transparent; }
11262
+ .oc-topbar.oc-topbar--dark { background: var(--oc-dark-band, var(--color-duskblue-95)); }
11263
+ .oc-topbar--brand { background: linear-gradient(100deg, var(--oc-accent-from, var(--oc-primary)), var(--oc-accent-to, var(--oc-primary))); }
11264
+ .oc-topbar:is(.oc-topbar--dark, .oc-topbar--brand) .oc-navmenu--site .oc-navmenu-link {
11265
+ color: oklch(from var(--color-white-50) l c h / 0.8);
11266
+ border-color: transparent;
11267
+ background: none;
11268
+ }
11269
+ .oc-topbar:is(.oc-topbar--dark, .oc-topbar--brand) .oc-navmenu--site .oc-navmenu-link:is(:hover, [aria-current="page"]) {
11270
+ color: var(--color-white-50);
11271
+ border-color: transparent;
11272
+ background: oklch(from var(--color-white-50) l c h / 0.14);
11273
+ }
11274
+ .oc-topbar--dark .oc-topbar-logo { background: oklch(from var(--color-white-50) l c h / 0.14); }
11275
+ .oc-topbar--brand .oc-topbar-logo { color: var(--oc-primary); background: var(--color-white-50); }
11276
+ .oc-topbar:is(.oc-topbar--dark, .oc-topbar--brand) .oc-topbar-burger { color: inherit; border-color: oklch(from var(--color-white-50) l c h / 0.3); }
11277
+
11278
+ /* Floating: a rounded bar with room around it and a soft shadow (over a hero or a photo). */
11279
+ .oc-topbar--floating {
11280
+ margin: 0.75rem;
11281
+ border: 1px solid var(--oc-border);
11282
+ border-radius: 16px;
11283
+ box-shadow: 0 14px 34px -18px oklch(from var(--color-black-60) l c h / 0.35);
11284
+ }
11285
+
11286
+ /* Strip: a small row above the bar (contact, language, account). */
11287
+ .oc-topbar-strip {
11288
+ display: flex;
11289
+ flex-wrap: wrap;
11290
+ align-items: center;
11291
+ justify-content: space-between;
11292
+ gap: 0.25rem 1rem;
11293
+ padding: 0.35rem clamp(14px, 2vw, 28px);
11294
+ font-size: 0.78rem;
11295
+ color: var(--oc-muted-foreground);
11296
+ background: var(--oc-muted);
11297
+ }
11298
+ .oc-topbar-strip a { color: inherit; text-decoration: none; }
11299
+ .oc-topbar-strip a:hover { color: var(--oc-foreground); }
11300
+ .oc-topbar-strip > span { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 1rem; }
11301
+
11302
+ /* Footer bar: text left and links right (--split), a link list, a dark bar. */
11303
+ .oc-footerbar--split { justify-content: space-between; gap: 1rem; text-align: start; }
11304
+ .oc-footerbar--tall { block-size: auto; min-block-size: var(--ocm-footer-height, 2.25rem); padding-block: 0.85rem; flex-wrap: wrap; }
11305
+ .oc-footerbar-links { display: inline-flex; flex-wrap: wrap; gap: 0.25rem 1rem; margin: 0; padding: 0; list-style: none; }
11306
+ .oc-footerbar-links a { color: var(--oc-muted-foreground); text-decoration: none; }
11307
+ .oc-footerbar-links a:hover { color: var(--oc-foreground); }
11308
+ .oc-footerbar.oc-footerbar--dark { color: oklch(from var(--color-white-50) l c h / 0.7); background: var(--oc-dark-band, var(--color-duskblue-95)); border-block-start-color: transparent; }
11309
+ .oc-footerbar--dark :is(a, .oc-footerbar-links a) { color: oklch(from var(--color-white-50) l c h / 0.85); }
11310
+ .oc-footerbar--dark .oc-footerbar-links a:hover { color: var(--color-white-50); }
11311
+
11312
+ /* parts/site-base.css */
11313
+ /* File: packages/ui/src/parts/site-base.css */
11314
+ /* Map: skip link -> icon */
11315
+ /* Two small site helpers: a skip link for keyboard users (hidden above the page until it gets the focus) and a basic
11316
+ inline icon (svg or iconify-icon) that is as large as the text around it. */
11317
+
11318
+ /* <a class="oc-skip-link" href="#main">Skip to content</a> as the first element in <body>. */
11319
+ .oc-skip-link {
11320
+ position: absolute;
11321
+ inset-inline-start: 1rem;
11322
+ inset-block-start: 1rem;
11323
+ /* Fully above the edge, whatever its height; drops in on focus. */
11324
+ translate: 0 calc(-100% - 2rem);
11325
+ z-index: 100;
11326
+ padding: 0.6rem 1rem;
11327
+ border-radius: var(--oc-radius-md, 0.75rem);
11328
+ background: var(--oc-foreground);
11329
+ color: var(--oc-background);
11330
+ font-weight: 600;
11331
+ text-decoration: none;
11332
+ transition: translate var(--oc-transition-fast);
11333
+ }
11334
+ .oc-skip-link:focus { translate: none; }
11335
+
11336
+ /* Inline icon: 1.25em (1.1em in a button), never squeezed in a flex row. */
11337
+ .oc-icon {
11338
+ inline-size: 1.25em;
11339
+ block-size: 1.25em;
11340
+ flex-shrink: 0;
11341
+ }
11342
+ .oc-btn > .oc-icon {
11343
+ inline-size: 1.1em;
11344
+ block-size: 1.1em;
11345
+ }
11346
+
11347
+ /* parts/site-brand-mark.css */
11348
+ /* File: packages/ui/src/parts/site-brand-mark.css */
11349
+ /* Map: brand mark (icon + text) -> word (accent part in the brand gradient) -> tagline -> sizes */
11350
+ /* A logo built from an icon and text: the mark (svg or img) with a soft glow in the brand color, the word mark with one
11351
+ part in the brand gradient, and a small tagline under it. Sizes --sm and --lg (a footer, a hero or a map). */
11352
+
11353
+ .oc-brand-mark {
11354
+ --oc-brand-mark-icon: 2.25rem;
11355
+ --oc-brand-mark-word: 1.35rem;
11356
+ --oc-brand-mark-tagline: 0.6rem;
11357
+ display: inline-flex;
11358
+ align-items: center;
11359
+ gap: 0.6rem;
11360
+ color: var(--oc-foreground);
11361
+ text-decoration: none;
11362
+ }
11363
+ .oc-brand-mark__icon {
11364
+ flex: none;
11365
+ inline-size: var(--oc-brand-mark-icon);
11366
+ block-size: var(--oc-brand-mark-icon);
11367
+ filter: drop-shadow(0 6px 14px oklch(from var(--oc-brand-1) l c h / 0.35));
11368
+ }
11369
+ .oc-brand-mark__text {
11370
+ display: grid;
11371
+ line-height: 1;
11372
+ }
11373
+ .oc-brand-mark__word {
11374
+ font-family: var(--oc-font-display, inherit);
11375
+ font-size: var(--oc-brand-mark-word);
11376
+ font-weight: 800;
11377
+ letter-spacing: -0.04em;
11378
+ }
11379
+ .oc-brand-mark__accent {
11380
+ background: linear-gradient(90deg, var(--oc-brand-1), var(--oc-brand-2));
11381
+ -webkit-background-clip: text;
11382
+ background-clip: text;
11383
+ color: transparent;
11384
+ }
11385
+ .oc-brand-mark__tagline {
11386
+ margin-block-start: 0.3rem;
11387
+ color: var(--oc-subtle-foreground, var(--oc-muted-foreground));
11388
+ font-family: var(--oc-font-mono);
11389
+ font-size: var(--oc-brand-mark-tagline);
11390
+ font-weight: 600;
11391
+ letter-spacing: 0.16em;
11392
+ text-transform: uppercase;
11393
+ }
11394
+ .oc-brand-mark--sm {
11395
+ --oc-brand-mark-icon: 1.75rem;
11396
+ --oc-brand-mark-word: 1.1rem;
11397
+ --oc-brand-mark-tagline: 0.55rem;
11398
+ }
11399
+ .oc-brand-mark--lg {
11400
+ --oc-brand-mark-icon: 2.6rem;
11401
+ --oc-brand-mark-word: 1.6rem;
11402
+ --oc-brand-mark-tagline: 0.66rem;
11403
+ }
11404
+
11405
+ /* parts/site-cards.css */
11406
+ /* File: packages/ui/src/parts/site-cards.css */
11407
+ /* Map: feature card -> link card -> icon list (+ points) -> accent list + note -> badge row + accent tagline
11408
+ -> screenshot grid -> browser frame glow + mark */
11409
+ /* Cards and lists for the pages of a company site: services and features (icon tile + title + text), cards that are
11410
+ links, a list of icon rows in frames (or as points with a title), a list with markers in the accent color, a small
11411
+ note, a row of badges, a tagline in the accent color, a grid of screenshots that open in the lightbox, and a browser
11412
+ frame with a glow in the accent color and a product mark on it. */
11413
+
11414
+ /* Feature / service card: tile, title, text; a quiet hover. In an .oc-card-grid (--oc-card-min sets the column width). */
11415
+ .oc-feature-card {
11416
+ padding: 1.6rem;
11417
+ border: 1px solid var(--oc-border);
11418
+ border-radius: var(--oc-radius-lg, 1.25rem);
11419
+ background: var(--oc-surface);
11420
+ transition: border-color 0.3s, background 0.3s;
11421
+ }
11422
+ .oc-feature-card:hover {
11423
+ border-color: var(--oc-border-strong);
11424
+ background: var(--oc-surface-strong);
11425
+ }
11426
+ .oc-feature-card > .oc-icon-tile { margin-block-end: 1.1rem; }
11427
+ .oc-feature-card__title {
11428
+ margin: 0 0 0.6rem;
11429
+ font-family: var(--oc-font-display, inherit);
11430
+ font-size: 1.2rem;
11431
+ line-height: 1.25;
11432
+ }
11433
+ .oc-feature-card__text {
11434
+ margin: 0;
11435
+ color: var(--oc-muted-foreground);
11436
+ font-size: 0.95rem;
11437
+ }
11438
+
11439
+ /* Card that is a link (an agent, a tool, a document): tile, title, text and a "more" line; hover in the accent color. */
11440
+ .oc-link-card {
11441
+ display: grid;
11442
+ align-content: start;
11443
+ gap: 0.4rem;
11444
+ padding: 1.25rem;
11445
+ border: 1px solid var(--oc-border);
11446
+ border-radius: var(--oc-radius-lg, 1.25rem);
11447
+ background: var(--oc-surface);
11448
+ color: var(--oc-foreground);
11449
+ text-decoration: none;
11450
+ transition: border-color 0.3s, background 0.3s;
11451
+ }
11452
+ /* --oc-link-card-hover: the hover color alone (without an accent preset, so the tile keeps its own color). */
11453
+ .oc-link-card:hover {
11454
+ border-color: oklch(from var(--oc-link-card-hover, var(--oc-accent-from)) l c h / 0.5);
11455
+ background: var(--oc-surface-strong);
11456
+ }
11457
+ .oc-link-card:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 2px; }
11458
+ .oc-link-card > .oc-icon-tile { margin-block-end: 0.4rem; }
11459
+ .oc-link-card__title { font-family: var(--oc-font-display, inherit); font-weight: 700; }
11460
+ .oc-link-card__text { color: var(--oc-muted-foreground); font-size: 0.93rem; }
11461
+ .oc-link-card__more {
11462
+ display: inline-flex;
11463
+ align-items: center;
11464
+ gap: 0.35rem;
11465
+ margin-block-start: 0.4rem;
11466
+ font-size: 0.9rem;
11467
+ font-weight: 600;
11468
+ }
11469
+ .oc-link-card__more::after { content: "→"; transition: translate 0.25s var(--oc-ease-out, ease); }
11470
+ .oc-link-card:hover .oc-link-card__more::after { translate: 4px 0; }
11471
+
11472
+ /* Icon list: rows with an icon in a frame. --points: an icon tile next to a title and a text, without frames. */
11473
+ .oc-icon-list {
11474
+ display: grid;
11475
+ gap: 0.75rem;
11476
+ margin: 0;
11477
+ padding: 0;
11478
+ list-style: none;
11479
+ }
11480
+ .oc-icon-list > li {
11481
+ display: flex;
11482
+ align-items: center;
11483
+ gap: 0.75rem;
11484
+ padding: 0.85rem 1rem;
11485
+ border: 1px solid var(--oc-border);
11486
+ border-radius: var(--oc-radius-md, 0.75rem);
11487
+ background: var(--oc-surface);
11488
+ }
11489
+ .oc-icon-list > li > :is(svg, iconify-icon, .oc-icon) {
11490
+ flex: none;
11491
+ inline-size: 1.25rem;
11492
+ block-size: 1.25rem;
11493
+ font-size: 1.25rem;
11494
+ color: var(--oc-primary);
11495
+ }
11496
+ .oc-icon-list--points { gap: 1rem; }
11497
+ .oc-icon-list--points > li {
11498
+ display: grid;
11499
+ grid-template-columns: auto 1fr;
11500
+ align-items: start;
11501
+ gap: 0.25rem 0.9rem;
11502
+ padding: 0;
11503
+ border: 0;
11504
+ background: none;
11505
+ }
11506
+ .oc-icon-list--points > li > .oc-icon-tile { grid-row: span 2; }
11507
+ .oc-icon-list__title { font-family: var(--oc-font-display, inherit); font-weight: 700; }
11508
+ .oc-icon-list__text { color: var(--oc-muted-foreground); font-size: 0.95rem; }
11509
+
11510
+ /* List with markers in the accent color, and a small note under a block. */
11511
+ .oc-list--accent {
11512
+ display: grid;
11513
+ gap: 0.6rem;
11514
+ margin: 0;
11515
+ padding-inline-start: 1.1rem;
11516
+ list-style: disc;
11517
+ color: var(--oc-muted-foreground);
11518
+ font-size: 0.95rem;
11519
+ }
11520
+ .oc-list--accent > li::marker { color: var(--oc-accent-from, var(--oc-primary)); }
11521
+ .oc-note {
11522
+ margin: 1.25rem 0 0;
11523
+ color: var(--oc-muted-foreground);
11524
+ font-size: 0.85rem;
11525
+ }
11526
+ .oc-note--md {
11527
+ max-inline-size: 60rem;
11528
+ font-size: 1rem;
11529
+ }
11530
+
11531
+ /* Row of badges or pills above a title, and a tagline in the accent color (lighter, so it reads on a dark page). */
11532
+ .oc-badge-row {
11533
+ display: flex;
11534
+ flex-wrap: wrap;
11535
+ gap: 0.5rem;
11536
+ margin-block-end: 1.25rem;
11537
+ }
11538
+ .oc-tagline--accent {
11539
+ margin: 0 0 1rem;
11540
+ color: light-dark(var(--oc-accent-from), color-mix(in oklch, var(--oc-accent-from) 55%, white));
11541
+ font-size: clamp(1.2rem, 2.5vw, 1.6rem);
11542
+ font-weight: 600;
11543
+ }
11544
+
11545
+ /* Screenshot grid: figures with the whole image visible (contain, 16:10 on the card color) and a caption;
11546
+ a link or a button around the image opens the lightbox
11547
+ (<button command="show-modal" commandfor="<lightbox id>">). A <picture> (animated WebP with a still image) works the same. */
11548
+ .oc-shot-grid {
11549
+ display: grid;
11550
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--oc-shot-min, 18rem)), 1fr));
11551
+ gap: 1.25rem;
11552
+ }
11553
+ .oc-shot {
11554
+ margin: 0;
11555
+ border: 1px solid var(--oc-border);
11556
+ border-radius: var(--oc-radius-lg, 1.25rem);
11557
+ overflow: hidden;
11558
+ background: var(--oc-surface);
11559
+ }
11560
+ .oc-shot > :is(a, button) { display: block; inline-size: 100%; padding: 0; border: 0; background: none; cursor: zoom-in; }
11561
+ .oc-shot > :is(a, button):focus-visible { outline: 2px solid var(--oc-ring); outline-offset: -2px; }
11562
+ .oc-shot :is(picture) { display: block; }
11563
+ .oc-shot img {
11564
+ display: block;
11565
+ inline-size: 100%;
11566
+ aspect-ratio: var(--oc-shot-ratio, 16 / 10);
11567
+ object-fit: contain;
11568
+ background: var(--oc-card);
11569
+ }
11570
+ .oc-shot > figcaption {
11571
+ padding: 0.8rem 1rem;
11572
+ color: var(--oc-muted-foreground);
11573
+ font-size: 0.88rem;
11574
+ }
11575
+
11576
+ /* Browser frame with a shadow in the accent color and a product mark on the bottom end corner. */
11577
+ .oc-browser-frame--glow {
11578
+ position: relative;
11579
+ box-shadow: 0 40px 90px -40px color-mix(in srgb, var(--oc-accent-from) 70%, transparent);
11580
+ }
11581
+ .oc-browser-frame--glow :is(.oc-browser-frame__view, .oc-browser-frame__body) img { transition: scale 0.6s var(--oc-ease-out, ease); }
11582
+ .oc-browser-frame--glow:hover :is(.oc-browser-frame__view, .oc-browser-frame__body) img { scale: 1.02; }
11583
+ .oc-browser-frame__mark {
11584
+ position: absolute;
11585
+ inset-block-end: 1rem;
11586
+ inset-inline-end: 1rem;
11587
+ filter: drop-shadow(0 10px 20px oklch(0 0 0 / 0.6));
11588
+ }
11589
+
9883
11590
  /* parts/site-fact-list.css */
9884
11591
  /* File: packages/ui/src/parts/site-fact-list.css
9885
11592
  Map: plain -> compact -> stacked -> accent values -> narrow screens
@@ -9907,6 +11614,191 @@ a.oc-message__quote:focus-visible { outline: 2px solid var(--oc-ring); outline-o
9907
11614
  /* Very narrow containers: wrap instead of squeezing label and value together. */
9908
11615
  .oc-fact-list > div { flex-wrap: wrap; }
9909
11616
 
11617
+ /* parts/site-fly.css */
11618
+ /* File: packages/ui/src/parts/site-fly.css */
11619
+ /* Map: fly in (only with fly.js) -> visible -> reduced motion -> product card badge -> code window tokens */
11620
+ /* Cards that fly in when they scroll into view: they start from the middle of their row (--oc-fly-x, set by fly.js),
11621
+ 70px lower, smaller and tilted, and land in their place. Without fly.js (html.oc-fly-ready is missing) or with
11622
+ "reduce motion" everything simply stands in its place. Options: --oc-fly-step (offset per column), --oc-fly-stagger
11623
+ (delay per column) and data-oc-fly-repeat (fly out again when leaving the view). The lift on hover uses translate, so it never fights the
11624
+ transform of the fly-in. Also: a "New" badge in a product card name and short syntax classes for a code window. */
11625
+
11626
+ .oc-fly-ready .oc-reveal--fly {
11627
+ /* --oc-fly-delay: set per card by fly.js from --oc-fly-stagger (delay per column). */
11628
+ transition:
11629
+ opacity 0.6s var(--oc-ease-out, ease) var(--oc-fly-delay, 0s),
11630
+ transform 0.9s cubic-bezier(0.16, 1, 0.3, 1) var(--oc-fly-delay, 0s),
11631
+ border-color 0.35s;
11632
+ }
11633
+ .oc-fly-ready .oc-reveal--fly:not(.is-visible) {
11634
+ opacity: 0;
11635
+ transform: perspective(900px) translate3d(var(--oc-fly-x, 0), 70px, 0) scale(0.9) rotateX(14deg);
11636
+ }
11637
+ @media (prefers-reduced-motion: reduce) {
11638
+ .oc-fly-ready .oc-reveal--fly:not(.is-visible) { opacity: 1; transform: none; }
11639
+ }
11640
+
11641
+ /* "New" after the name of a product card: <h3 class="oc-product-card__name">Product 1 <span class="oc-product-card__badge oc-pill">New</span></h3> */
11642
+ .oc-product-card__badge {
11643
+ margin-inline-start: 0.4rem;
11644
+ vertical-align: middle;
11645
+ font-family: var(--oc-font-sans);
11646
+ font-size: 0.7rem;
11647
+ letter-spacing: 0.04em;
11648
+ text-transform: uppercase;
11649
+ }
11650
+
11651
+ /* Short syntax classes for hand-written code in an .oc-code-window: keyword, string, constant, function, muted.
11652
+ They take the colors of the window (--oc-syntax-*), so they follow light and dark mode and a theme can change them. */
11653
+ .oc-code-window .oc-tok-k { color: var(--oc-syntax-keyword); }
11654
+ .oc-code-window .oc-tok-s { color: var(--oc-syntax-string); }
11655
+ .oc-code-window .oc-tok-c { color: var(--oc-syntax-constant); }
11656
+ .oc-code-window .oc-tok-f { color: var(--oc-syntax-function); }
11657
+ .oc-code-window .oc-tok-m { color: var(--oc-syntax-comment); }
11658
+
11659
+ /* parts/site-header-variants.css */
11660
+ /* File: packages/ui/src/parts/site-header-variants.css
11661
+ Map: actions slot -> centred menu -> dark -> floating pill -> glass pill -> footer: centred, dark, newsletter, social
11662
+ Variants of the marketing header (.oc-site-header) and footer (.oc-site-footer) in showcase.css. The sticky header
11663
+ turns to glass while scrolling; the coloured variants keep their own background (no scroll animation). */
11664
+
11665
+ .oc-site-header__actions { display: flex; align-items: center; gap: 0.5rem; flex: none; }
11666
+ .oc-site-header__brand { white-space: nowrap; flex: none; }
11667
+ .oc-site-header__nav > :is(a, button):not(.oc-btn) { white-space: nowrap; }
11668
+
11669
+ /* Centred menu: brand left, menu in the middle, actions right. */
11670
+ @media (min-width: 861px) {
11671
+ .oc-site-header--center .oc-site-header__inner { display: grid; grid-template-columns: 1fr auto 1fr; }
11672
+ .oc-site-header--center .oc-site-header__brand { justify-self: start; }
11673
+ .oc-site-header--center .oc-site-header__actions { justify-self: end; }
11674
+ }
11675
+
11676
+ /* Dark: a solid dark bar with light text. */
11677
+ .oc-site-header--dark {
11678
+ animation: none;
11679
+ color: var(--color-white-50);
11680
+ background: var(--oc-dark-band, var(--color-duskblue-95));
11681
+ }
11682
+ .oc-site-header--dark :is(.oc-site-header__brand, .oc-site-header__nav a[aria-current="page"], .oc-site-header__nav a:not(.oc-btn):hover) { color: var(--color-white-50); }
11683
+ .oc-site-header--dark .oc-site-header__nav a:not(.oc-btn) { color: oklch(from var(--color-white-50) l c h / 0.72); }
11684
+
11685
+ /* Floating: the bar as a rounded card with room around it. */
11686
+ .oc-site-header:is(.oc-site-header--floating, .oc-site-header--glass) { animation: none; padding: 0.75rem; }
11687
+ .oc-site-header--floating .oc-site-header__inner {
11688
+ block-size: 4rem;
11689
+ padding-inline: 1rem 0.75rem;
11690
+ border: 1px solid var(--oc-border);
11691
+ border-radius: 18px;
11692
+ background: var(--oc-background);
11693
+ box-shadow: 0 16px 36px -22px oklch(from var(--color-black-60) l c h / 0.45);
11694
+ }
11695
+
11696
+ /* Glass: a clear liquid-glass pill over a photo or a hero (look in media-glass-liquid.css). */
11697
+ .oc-site-header--glass .oc-site-header__inner { block-size: 3.75rem; padding-inline: 1.1rem 0.6rem; border-radius: 999px; }
11698
+ .oc-site-header--glass :is(.oc-site-header__brand, .oc-site-header__nav a:not(.oc-btn)) { color: light-dark(var(--color-black-95), var(--color-white-50)); text-shadow: 0 1px 2px light-dark(oklch(100% 0 0deg / 0.5), oklch(0% 0 0deg / 0.4)); }
11699
+
11700
+ /* ---- Footer variants ---- */
11701
+ /* Centred: brand, one row of links, social icons and the bottom line, all centred. */
11702
+ .oc-site-footer--center { text-align: center; }
11703
+ .oc-site-footer--center :is(.oc-site-footer__about, .oc-site-footer__bottom) { justify-items: center; justify-content: center; }
11704
+ .oc-site-footer--center .oc-site-footer__about { display: grid; gap: 1rem; }
11705
+ .oc-site-footer--center .oc-site-footer__about p { margin: 0; max-inline-size: 34rem; }
11706
+ .oc-site-footer__row { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem 1.75rem; margin: 2rem 0 0; padding: 0; list-style: none; }
11707
+ .oc-site-footer__social { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; padding: 0; list-style: none; }
11708
+ .oc-site-footer--center .oc-site-footer__social { justify-content: center; margin-block-start: 1.5rem; }
11709
+ .oc-site-footer__social a {
11710
+ display: grid;
11711
+ place-items: center;
11712
+ inline-size: 2.25rem;
11713
+ block-size: 2.25rem;
11714
+ border: 1px solid var(--oc-border);
11715
+ border-radius: 999px;
11716
+ transition: border-color 0.2s, color 0.2s;
11717
+ }
11718
+ .oc-site-footer__social a:hover { border-color: var(--oc-primary); color: var(--oc-primary); }
11719
+ .oc-site-footer__social :is(svg, iconify-icon) { inline-size: 1.1rem; block-size: 1.1rem; font-size: 1.1rem; }
11720
+
11721
+ /* Dark: light text on a dark band, the same deep blue in light and dark mode (--oc-dark-band), no top border. */
11722
+ .oc-site-footer--dark { border-block-start: 0; color: oklch(from var(--color-white-50) l c h / 0.7); background: var(--oc-dark-band, var(--color-duskblue-95)); }
11723
+ .oc-site-footer--dark :is(.oc-site-footer__title, .oc-site-header__brand, a:hover) { color: var(--color-white-50); }
11724
+ .oc-site-footer--dark :is(.oc-site-footer__bottom, .oc-site-footer__social a) { border-color: oklch(from var(--color-white-50) l c h / 0.16); }
11725
+
11726
+ /* Newsletter: a sign-up field in the brand column. */
11727
+ .oc-site-footer__signup { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-block-start: 1.25rem; max-inline-size: 24rem; }
11728
+ .oc-site-footer__signup .oc-input { flex: 1 1 12rem; min-inline-size: 0; }
11729
+ .oc-site-footer--dark .oc-site-footer__signup .oc-input { color: var(--color-white-50); background: oklch(from var(--color-white-50) l c h / 0.08); border-color: oklch(from var(--color-white-50) l c h / 0.2); }
11730
+
11731
+ /* parts/site-layout.css */
11732
+ /* File: packages/ui/src/parts/site-layout.css */
11733
+ /* Map: page hero -> section head / more -> action row -> group title -> band -> meta line */
11734
+ /* Page building blocks for a marketing or company site, on top of the showcase classes (.oc-section-pad, .oc-display):
11735
+ the larger header of an inner page, a section head and a "see more" row, a row of buttons, a group title with a count,
11736
+ a full-width band (for a stat strip) and a meta line (version · date). */
11737
+
11738
+ /* Inner page header: more room on top, less below, and a larger display title. */
11739
+ .oc-section-pad--page { padding-block: clamp(4rem, 10vw, 7rem) clamp(2rem, 5vw, 3rem); }
11740
+ .oc-display--page {
11741
+ margin-block-end: 1.25rem;
11742
+ font-size: clamp(2.5rem, 6.5vw, 4.75rem);
11743
+ }
11744
+
11745
+ /* Head of a section (eyebrow, title, lead) and the row with a "see all" link under the section. */
11746
+ .oc-section-head {
11747
+ max-inline-size: 46rem;
11748
+ margin-block-end: 3rem;
11749
+ }
11750
+ .oc-section-head--center {
11751
+ margin-inline: auto;
11752
+ text-align: center;
11753
+ }
11754
+ .oc-section-more { margin-block-start: 2.5rem; }
11755
+
11756
+ /* Row of buttons under a text: wraps on a narrow screen. */
11757
+ .oc-actions {
11758
+ display: flex;
11759
+ flex-wrap: wrap;
11760
+ gap: 0.75rem;
11761
+ margin-block-start: 2rem;
11762
+ }
11763
+ .oc-actions--center { justify-content: center; }
11764
+ .oc-actions--end { justify-content: flex-end; }
11765
+ .oc-actions--tight { margin-block-start: 1rem; }
11766
+
11767
+ /* Title of a group of cards with the number of items in a pill: <h3 class="oc-group-title">Apps <span class="oc-group-title__count">8</span></h3> */
11768
+ .oc-group-title {
11769
+ display: flex;
11770
+ align-items: center;
11771
+ gap: 0.75rem;
11772
+ margin: 0 0 1.5rem;
11773
+ font-size: 1.4rem;
11774
+ }
11775
+ .oc-group-title__count {
11776
+ padding: 0.1rem 0.6rem;
11777
+ border-radius: 999px;
11778
+ background: var(--oc-surface-strong);
11779
+ color: var(--oc-muted-foreground);
11780
+ font-size: 0.85rem;
11781
+ font-weight: 500;
11782
+ }
11783
+
11784
+ /* Band over the full width of the page with a line above and below; the content stays in its .oc-container.
11785
+ Put a .oc-stat-strip--plain in it for a row of key figures. */
11786
+ .oc-band {
11787
+ border-block: 1px solid var(--oc-border);
11788
+ background: var(--oc-band-bg, var(--oc-surface));
11789
+ }
11790
+
11791
+ /* Meta line under a page title: version · date, author. */
11792
+ .oc-meta {
11793
+ margin: 0 0 1rem;
11794
+ color: var(--oc-subtle-foreground, var(--oc-muted-foreground));
11795
+ font-size: 0.9rem;
11796
+ }
11797
+
11798
+ /* Muted and subtle text from the theme tokens, for running text inside a panel, a spotlight or a split. */
11799
+ .oc-text-muted { color: var(--oc-muted-foreground); }
11800
+ .oc-text-subtle { color: var(--oc-subtle-foreground, var(--oc-muted-foreground)); }
11801
+
9910
11802
  /* parts/site-link-arrow.css */
9911
11803
  /* File: packages/ui/src/parts/site-link-arrow.css
9912
11804
  Map: focus ring -> back arrow -> external arrow -> color modifiers -> sizes -> reduced motion
@@ -10112,6 +12004,37 @@ a.oc-message__quote:focus-visible { outline: 2px solid var(--oc-ring); outline-o
10112
12004
  .oc-next-teaser__tagline::before { transition: none; }
10113
12005
  }
10114
12006
 
12007
+ /* parts/site-panel.css */
12008
+ /* File: packages/ui/src/parts/site-panel.css */
12009
+ /* Map: panel -> glow (start / end / both) */
12010
+ /* A framed panel on the page (a form, a call to action, a feature with a list): card background, strong border, large
12011
+ radius, and room above it when a link jumps to it (#form). --glow adds a soft radial light in a corner:
12012
+ --oc-panel-glow-color (brand 1) and --oc-panel-glow-at (top left); --glow-end puts it top right, --glow-both adds a
12013
+ second light (brand 3) bottom right. */
12014
+
12015
+ .oc-panel {
12016
+ --oc-panel-glow-color: var(--oc-brand-1);
12017
+ --oc-panel-glow-at: 0% 0%;
12018
+ position: relative;
12019
+ padding: var(--oc-panel-padding, clamp(1.5rem, 4vw, 2.5rem));
12020
+ border: 1px solid var(--oc-border-strong);
12021
+ border-radius: calc(var(--oc-radius-lg, 1.25rem) * 1.2);
12022
+ background: var(--oc-card);
12023
+ scroll-margin-top: var(--oc-scroll-offset, 6rem);
12024
+ }
12025
+ .oc-panel--glow {
12026
+ background:
12027
+ radial-gradient(30rem circle at var(--oc-panel-glow-at), oklch(from var(--oc-panel-glow-color) l c h / 0.12), transparent 60%),
12028
+ var(--oc-card);
12029
+ }
12030
+ .oc-panel--glow-end { --oc-panel-glow-at: 100% 0%; }
12031
+ .oc-panel--glow-both {
12032
+ background:
12033
+ radial-gradient(40rem circle at 0% 0%, oklch(from var(--oc-brand-1) l c h / 0.18), transparent 55%),
12034
+ radial-gradient(40rem circle at 100% 100%, oklch(from var(--oc-brand-3) l c h / 0.2), transparent 55%),
12035
+ var(--oc-card);
12036
+ }
12037
+
10115
12038
  /* parts/site-price-card.css */
10116
12039
  /* File: packages/ui/src/parts/site-price-card.css
10117
12040
  Map: card -> head + discount badge -> period toggle -> monthly / yearly / lifetime price (:has) -> features -> featured
@@ -10400,7 +12323,7 @@ a.oc-message__quote:focus-visible { outline: 2px solid var(--oc-ring); outline-o
10400
12323
  -> sizes (--sm, --lg) -> full width
10401
12324
  Prose: typography for long-form text from a CMS, blog or help page, where the HTML has no classes of its own.
10402
12325
  Every rule is scoped with :where(), so a utility class on an element inside still wins. Spacing is in em, so --sm and --lg
10403
- scale the whole text. Based on the help and maintenance pages of the Korrekt app. */
12326
+ scale the whole text. */
10404
12327
 
10405
12328
  .oc-prose {
10406
12329
  max-inline-size: 68ch;
@@ -10531,6 +12454,18 @@ a.oc-message__quote:focus-visible { outline: 2px solid var(--oc-ring); outline-o
10531
12454
  @media (prefers-reduced-motion: reduce) {
10532
12455
  .oc-prose :where(a) { transition: none; }
10533
12456
  }
12457
+
12458
+ /* ---- Legal text (terms, privacy, cookies): narrower, muted body text in a slightly larger size, headings and strong
12459
+ in the foreground, disc lists with a gap, links in brand 1. A table in an .oc-table-wrap scrolls on a phone. ---- */
12460
+ .oc-prose--legal { max-inline-size: 46rem; color: var(--oc-muted-foreground); font-size: 1.0625rem; }
12461
+ .oc-prose--legal :where(h2) { margin-block-start: 2.5rem; font-size: 1.35rem; color: var(--oc-foreground); }
12462
+ .oc-prose--legal :where(h3) { margin-block-start: 1.5rem; font-size: 1.05rem; color: var(--oc-foreground); }
12463
+ .oc-prose--legal :where(strong, b) { color: var(--oc-foreground); }
12464
+ .oc-prose--legal :where(ul) { display: grid; gap: 0.4rem; padding-inline-start: 1.25rem; list-style: disc; }
12465
+ .oc-prose--legal :where(ul > li) { margin-block-start: 0; }
12466
+ .oc-prose--legal :where(a) { color: var(--oc-brand-1); font-weight: inherit; text-decoration-color: currentColor; text-underline-offset: 3px; }
12467
+ .oc-prose--legal :where(a:hover) { color: var(--oc-foreground); }
12468
+ .oc-prose--legal :where(table) { font-size: 0.93rem; }
10534
12469
 
10535
12470
  /* parts/site-stack-list.css */
10536
12471
  /* File: packages/ui/src/parts/site-stack-list.css
@@ -10638,6 +12573,52 @@ a.oc-message__quote:focus-visible { outline: 2px solid var(--oc-ring); outline-o
10638
12573
  /* Line in the accent color instead of the neutral border. */
10639
12574
  .oc-timeline--accent { border-inline-start-color: color-mix(in oklch, var(--oc-accent-from) 55%, transparent); }
10640
12575
 
12576
+ /* parts/utility-hover.css */
12577
+ /* File: packages/ui/src/parts/utility-hover.css */
12578
+ /* Map: smooth timing -> lift / grow / glow / none -> steady hover area -> reduced motion */
12579
+ /* Hover effect of a card, chosen with one class on the card (any card: .oc-card, product, feature, link, price, project …):
12580
+ .oc-hover-lift moves up (--oc-hover-lift, 6px) with a soft shadow; what a product card does by default
12581
+ .oc-hover-grow grows a little in place (--oc-hover-scale, 1.02)
12582
+ .oc-hover-glow stays in place, with a glow in the accent color
12583
+ .oc-hover-none no movement (also turns off the default lift of a product or price card)
12584
+ They share one smooth curve: --oc-hover-duration (0.45s) and --oc-hover-ease. This file loads after the cards, so the class wins. */
12585
+
12586
+ :is(.oc-hover-lift, .oc-hover-grow, .oc-hover-glow, .oc-product-card) {
12587
+ transition:
12588
+ translate var(--oc-hover-duration, 0.45s) var(--oc-hover-ease, cubic-bezier(0.22, 1, 0.36, 1)),
12589
+ scale var(--oc-hover-duration, 0.45s) var(--oc-hover-ease, cubic-bezier(0.22, 1, 0.36, 1)),
12590
+ box-shadow var(--oc-hover-duration, 0.45s) var(--oc-hover-ease, cubic-bezier(0.22, 1, 0.36, 1)),
12591
+ border-color 0.35s,
12592
+ background-color 0.35s,
12593
+ --oc-glow 0.35s;
12594
+ }
12595
+ .oc-product-card:hover { translate: 0 calc(-1 * var(--oc-hover-lift, 6px)); }
12596
+ .oc-hover-lift:hover {
12597
+ translate: 0 calc(-1 * var(--oc-hover-lift, 6px));
12598
+ box-shadow: 0 1.25rem 2.5rem -1.25rem oklch(from var(--color-black-60) l c h / 0.35);
12599
+ }
12600
+ .oc-hover-grow:hover { translate: none; scale: var(--oc-hover-scale, 1.02); }
12601
+ .oc-hover-glow:hover {
12602
+ translate: none;
12603
+ box-shadow:
12604
+ 0 0 0 1px oklch(from var(--oc-accent-from, var(--oc-primary)) l c h / 0.4),
12605
+ 0 0.75rem 2.5rem -0.75rem oklch(from var(--oc-accent-from, var(--oc-primary)) l c h / 0.5);
12606
+ }
12607
+ .oc-hover-none:hover { translate: none; scale: none; }
12608
+
12609
+ /* While a card moves up, the strip it leaves keeps the hover, so a mouse at the bottom edge does not make it bounce. */
12610
+ :where(.oc-hover-lift) { position: relative; }
12611
+ :is(.oc-hover-lift, .oc-product-card:not([class*="oc-hover-"])):hover::before {
12612
+ content: "";
12613
+ position: absolute;
12614
+ inset: 0 0 calc(-1 * var(--oc-hover-lift, 6px) - 2px);
12615
+ z-index: -1;
12616
+ }
12617
+
12618
+ @media (prefers-reduced-motion: reduce) {
12619
+ :is(.oc-hover-lift, .oc-hover-grow, .oc-product-card):hover { translate: none; scale: none; }
12620
+ }
12621
+
10641
12622
  /* File: packages/ui/src/code-block.css — Code-block component (line numbers + Omnia syntax theme) */
10642
12623
  /* Map: pre.code base -> CSS variables -> token spans (.hl-tag, .hl-attr, .hl-string, etc.) */
10643
12624
  /*