@birdycom/omniaui 0.12.4 → 0.12.6

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 (79) hide show
  1. package/README.md +1 -1
  2. package/bin/lib/copy-js.mjs +10 -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 +1496 -193
  8. package/dist/components.min.css +1 -1
  9. package/dist/data-table.js +135 -98
  10. package/dist/form-extras.d.ts +11 -0
  11. package/dist/form-extras.js +214 -0
  12. package/dist/lazy.d.ts +5 -0
  13. package/dist/lazy.js +37 -0
  14. package/dist/oc-theme.css +1 -1
  15. package/dist/parts/app-data-table-footer.css +38 -0
  16. package/dist/parts/app-data-table-rich.css +161 -0
  17. package/dist/parts/app-data-table.css +44 -74
  18. package/dist/parts/app-name-card.css +65 -0
  19. package/dist/parts/app-titlebar.css +16 -8
  20. package/dist/parts/data-meter-row.css +40 -0
  21. package/dist/parts/data-price.css +20 -0
  22. package/dist/parts/data-ribbon.css +76 -0
  23. package/dist/parts/data-spec-list.css +36 -0
  24. package/dist/parts/data-stock.css +41 -0
  25. package/dist/parts/forms-affix-input.css +1 -1
  26. package/dist/parts/forms-avatar-picker.css +43 -0
  27. package/dist/parts/forms-date-field.css +53 -0
  28. package/dist/parts/forms-density.css +13 -0
  29. package/dist/parts/forms-form-section.css +64 -0
  30. package/dist/parts/forms-quantity.css +55 -0
  31. package/dist/parts/forms-rating.css +81 -0
  32. package/dist/parts/media-attachment.css +1 -1
  33. package/dist/parts/media-barcode.css +23 -0
  34. package/dist/parts/media-chat-composer.css +3 -0
  35. package/dist/parts/media-emoji-picker.css +3 -1
  36. package/dist/parts/media-gallery.css +55 -0
  37. package/dist/parts/media-glass-liquid.css +99 -0
  38. package/dist/parts/media-glass-options.css +50 -0
  39. package/dist/parts/media-lazy.css +46 -0
  40. package/dist/parts/media-message-thread.css +1 -1
  41. package/dist/parts/media-message.css +1 -1
  42. package/dist/parts/nav-topbar-variants.css +96 -0
  43. package/dist/parts/site-header-variants.css +70 -0
  44. package/dist/parts/site-prose.css +1 -1
  45. package/dist/showcase.css +40 -9
  46. package/manifest.json +121 -9
  47. package/package.json +5 -2
  48. package/snippets/accordion-accent.html +1 -1
  49. package/snippets/attachment.html +4 -4
  50. package/snippets/avatar-picker.html +40 -0
  51. package/snippets/barcode.html +20 -0
  52. package/snippets/chat-composer.html +3 -3
  53. package/snippets/data-table.html +51 -76
  54. package/snippets/date-field.html +44 -0
  55. package/snippets/doc-viewer.html +1 -1
  56. package/snippets/emoji-picker.html +1 -1
  57. package/snippets/form-density.html +25 -0
  58. package/snippets/form-section.html +47 -0
  59. package/snippets/gallery.html +25 -0
  60. package/snippets/lazy.html +39 -0
  61. package/snippets/message-thread.html +12 -12
  62. package/snippets/message.html +10 -10
  63. package/snippets/meter-row.html +29 -0
  64. package/snippets/name-card.html +72 -0
  65. package/snippets/page-header.html +1 -1
  66. package/snippets/price.html +25 -0
  67. package/snippets/prose.html +3 -3
  68. package/snippets/quantity.html +12 -0
  69. package/snippets/rating.html +26 -0
  70. package/snippets/ribbon.html +48 -0
  71. package/snippets/sidebar.html +3 -3
  72. package/snippets/spec-list.html +34 -0
  73. package/snippets/stock.html +20 -0
  74. package/snippets/table-actions.html +5 -5
  75. package/snippets/table.html +2 -2
  76. package/snippets/titlebar.html +1 -1
  77. package/snippets/user.html +3 -3
  78. package/dist/parts/app-column-menu.css +0 -85
  79. 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.4. Do not edit. */
1
+ /* File: oc-theme.css — generated from tokens/semantic.json by @birdycom/omniacss 0.12.6. 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 {
@@ -486,14 +486,63 @@ flex-direction: column;
486
486
  border-radius: calc(var(--oc-radius) + 2px);
487
487
  }
488
488
 
489
+ /* Square, exactly as tall as a text button of the same size, so both line up in a toolbar. */
489
490
  .oc-btn--icon {
490
- padding: 0.5rem;
491
- aspect-ratio: 1;
491
+ --oc-btn-square: calc(2.25rem + 2px);
492
+ padding: 0;
493
+ inline-size: var(--oc-btn-square);
494
+ block-size: var(--oc-btn-square);
495
+ flex-shrink: 0;
496
+ }
497
+
498
+ .oc-btn--icon.oc-btn--xs { --oc-btn-square: calc(1.75rem + 2px); }
499
+ .oc-btn--icon.oc-btn--sm { --oc-btn-square: calc(2rem + 2px); }
500
+ .oc-btn--icon.oc-btn--lg { --oc-btn-square: calc(2.5rem + 2px); }
501
+
502
+ /* Webicons in a button: <iconify-icon> before (or after) the label, the same size as an inline SVG. */
503
+ .oc-btn > iconify-icon { font-size: 1.15em; flex-shrink: 0; }
504
+
505
+ /* Soft — tinted background in a tone, for row actions (edit, view, delete …) and quiet secondary actions.
506
+ --soft-primary | --soft-success | --soft-warning | --soft-destructive | --soft-info */
507
+ .oc-btn[class*="oc-btn--soft-"] {
508
+ 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));
509
+ color: var(--oc-btn-tone);
510
+ border-color: transparent;
511
+ }
512
+ .oc-btn[class*="oc-btn--soft-"]:hover {
513
+ 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));
514
+ opacity: 1;
515
+ }
516
+ .oc-btn--soft-primary { --oc-btn-tone: var(--oc-primary); }
517
+ .oc-btn--soft-success { --oc-btn-tone: var(--oc-success); }
518
+ .oc-btn--soft-warning { --oc-btn-tone: var(--oc-warning); }
519
+ .oc-btn--soft-destructive { --oc-btn-tone: var(--oc-destructive); }
520
+ .oc-btn--soft-info { --oc-btn-tone: var(--oc-info, light-dark(var(--color-deepskyblue-70), var(--color-skyblue-50))); }
521
+
522
+ /* Plain — only the icon: no surface, no padding; muted until hovered (menus, toolbars, inline actions). */
523
+ .oc-btn--plain {
524
+ padding: 0;
525
+ background: transparent;
526
+ border-color: transparent;
527
+ color: var(--oc-muted-foreground);
492
528
  }
529
+ .oc-btn--plain:hover,
530
+ .oc-btn--plain[aria-expanded="true"] { color: var(--oc-primary); opacity: 1; }
493
531
 
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; }
532
+ /* Loading — aria-busy="true": a spinner in front of the label (it replaces a leading icon); not clickable. */
533
+ .oc-btn[aria-busy="true"] { pointer-events: none; }
534
+ .oc-btn[aria-busy="true"]::before {
535
+ content: "";
536
+ inline-size: 1em;
537
+ block-size: 1em;
538
+ flex-shrink: 0;
539
+ border: 2px solid currentColor;
540
+ border-inline-end-color: transparent;
541
+ border-radius: 50%;
542
+ animation: oc-btn-spin 0.7s linear infinite;
543
+ }
544
+ .oc-btn[aria-busy="true"] > :is(svg, iconify-icon):first-child { display: none; }
545
+ @keyframes oc-btn-spin { to { rotate: 360deg; } }
497
546
 
498
547
  /* Button group */
499
548
  .oc-btn-group {
@@ -1633,6 +1682,8 @@ textarea.oc-input {
1633
1682
  /* Vertical tabs */
1634
1683
  .oc-tab-list--vertical {
1635
1684
  flex-direction: column;
1685
+ /* Every tab as wide as the list, so the active line on the right stays on one vertical line. */
1686
+ align-items: stretch;
1636
1687
  border-bottom: none;
1637
1688
  border-right: 1px solid var(--oc-border);
1638
1689
  }
@@ -1645,7 +1696,9 @@ textarea.oc-input {
1645
1696
  text-align: left;
1646
1697
  }
1647
1698
 
1648
- .oc-tab-list--vertical .oc-tab-trigger.is-active {
1699
+ .oc-tab-list--vertical .oc-tab-trigger.is-active,
1700
+ .oc-tab-list--vertical .oc-tab-trigger[aria-selected="true"] {
1701
+ border-bottom-color: transparent;
1649
1702
  border-right-color: var(--oc-foreground);
1650
1703
  }
1651
1704
 
@@ -1848,6 +1901,12 @@ textarea.oc-input {
1848
1901
  border-radius: calc(var(--oc-radius) + 2px);
1849
1902
  }
1850
1903
  .oc-table-wrap > .oc-table { min-inline-size: 32rem; }
1904
+ /* The rounded frame draws the outer border; the cells keep only the inner lines, so nothing is cut at the corners. */
1905
+ .oc-table-wrap:has(> .oc-table) { border: 1px solid var(--oc-border); }
1906
+ .oc-table-wrap > .oc-table :is(th, td):first-child { border-inline-start: 0; }
1907
+ .oc-table-wrap > .oc-table :is(th, td):last-child { border-inline-end: 0; }
1908
+ .oc-table-wrap > .oc-table > :first-child > tr:first-child > * { border-block-start: 0; }
1909
+ .oc-table-wrap > .oc-table > :last-child > tr:last-child > * { border-block-end: 0; }
1851
1910
 
1852
1911
  /* ===================================================================
1853
1912
  Shared docs utility table styles (dark theme)
@@ -1984,6 +2043,42 @@ a.oc-breadcrumb-item:hover {
1984
2043
  font-weight: 500;
1985
2044
  }
1986
2045
 
2046
+ /* Icons: a webicon or SVG before the label of any item (the link or the item itself). */
2047
+ .oc-breadcrumb-item,
2048
+ .oc-breadcrumb-item a { gap: 0.375rem; }
2049
+ .oc-breadcrumb-item a { display: inline-flex; align-items: center; }
2050
+ .oc-breadcrumb-item :is(svg, iconify-icon) { font-size: 1.1em; inline-size: 1.1em; block-size: 1.1em; flex-shrink: 0; }
2051
+
2052
+ /* --chevron: a › chevron between the items instead of "/". */
2053
+ .oc-breadcrumb--chevron .oc-breadcrumb-item + .oc-breadcrumb-item::before {
2054
+ content: "";
2055
+ inline-size: 1em;
2056
+ block-size: 1em;
2057
+ margin: 0 0.25rem;
2058
+ background: currentColor;
2059
+ color: var(--oc-muted-foreground);
2060
+ -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;
2061
+ 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;
2062
+ }
2063
+
2064
+ /* Collapsed middle of a long trail: a "…" button (opens a menu with the hidden levels). */
2065
+ .oc-breadcrumb-ellipsis {
2066
+ display: inline-flex;
2067
+ align-items: center;
2068
+ justify-content: center;
2069
+ inline-size: 1.75rem;
2070
+ block-size: 1.5rem;
2071
+ padding: 0;
2072
+ border: 0;
2073
+ border-radius: calc(var(--oc-radius) - 2px);
2074
+ background: transparent;
2075
+ color: var(--oc-muted-foreground);
2076
+ font: inherit;
2077
+ cursor: pointer;
2078
+ }
2079
+ .oc-breadcrumb-ellipsis:hover { background: var(--oc-accent); color: var(--oc-foreground); }
2080
+ .oc-breadcrumb-ellipsis:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
2081
+
1987
2082
  /* ===================================================================
1988
2083
  PROGRESS BAR
1989
2084
  =================================================================== */
@@ -3670,6 +3765,9 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3670
3765
  background: var(--oc-gradient-brand);
3671
3766
  -webkit-background-clip: text;
3672
3767
  background-clip: text;
3768
+ /* The gradient only paints the box: keep italic or tight last letters inside it without moving the text. */
3769
+ padding-inline-end: 0.08em;
3770
+ margin-inline-end: -0.08em;
3673
3771
  color: transparent;
3674
3772
  }
3675
3773
  .oc-text-accent { background-image: linear-gradient(100deg, var(--oc-accent-from), var(--oc-accent-to)); }
@@ -3769,17 +3867,44 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3769
3867
  .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
3868
  .oc-split { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr)); gap: 3rem; align-items: center; }
3771
3869
 
3772
- /* Code window: editor frame with traffic lights and a file name (always dark, like an editor) */
3870
+ /* Code window: editor frame with traffic lights and a file name. It follows the page theme: a light frame and light
3871
+ syntax colors in light mode, the Omnia Dark editor colors in dark mode. --dark keeps it dark on a light page (the
3872
+ hero blocks, where it reads as an editor screenshot). */
3773
3873
  .oc-code-window {
3774
3874
  position: relative;
3775
3875
  border: 1px solid var(--oc-border-strong);
3776
3876
  border-radius: var(--oc-radius-lg);
3777
- background: oklch(from var(--color-shallowblue-60) l c h / 0.9);
3877
+ background: light-dark(var(--oc-card), oklch(from var(--color-shallowblue-60) l c h / 0.9));
3778
3878
  box-shadow: 0 40px 80px -30px oklch(from var(--color-black-50) l c h / 0.6);
3779
3879
  backdrop-filter: blur(12px);
3780
3880
  overflow: hidden;
3781
- color-scheme: dark;
3782
- }
3881
+ /* Syntax colors (oc-code-syntax.css) for both themes; the code sits on the window surface. */
3882
+ --oc-syntax-bg: transparent;
3883
+ --oc-syntax-bg-preview: transparent;
3884
+ --oc-syntax-fg: light-dark(var(--color-shallowblue-60), var(--color-silver-60));
3885
+ --oc-syntax-comment: light-dark(var(--color-gray-60), var(--color-purple-2));
3886
+ --oc-syntax-string: light-dark(var(--color-green-80), var(--color-green-2));
3887
+ --oc-syntax-keyword: light-dark(var(--color-purple-70), var(--color-purple-2));
3888
+ --oc-syntax-variable: light-dark(var(--color-skycyan-90), var(--color-skycyan-30));
3889
+ --oc-syntax-function: light-dark(var(--color-skycyan-90), var(--color-skycyan-60));
3890
+ --oc-syntax-number: light-dark(var(--color-peru-80), var(--color-peru-40));
3891
+ --oc-syntax-operator: light-dark(var(--color-red-70), var(--color-red-50));
3892
+ --oc-syntax-constant: light-dark(var(--color-skycyan-90), var(--color-skycyan-70));
3893
+ --oc-syntax-storage: light-dark(var(--color-green-80), var(--color-green-2));
3894
+ --oc-syntax-tag: light-dark(var(--color-brown-70), var(--color-brown-10));
3895
+ --oc-syntax-attr: light-dark(var(--color-skycyan-90), var(--color-skycyan-70));
3896
+ --oc-syntax-prop: light-dark(var(--color-green-80), var(--color-green-2));
3897
+ --oc-syntax-value: light-dark(var(--color-teal-80), var(--color-teal-10));
3898
+ --oc-syntax-cls: light-dark(var(--color-darkgoldenrod-80), var(--color-wheat-70));
3899
+ --oc-syntax-id: light-dark(var(--color-skycyan-90), var(--color-skycyan-70));
3900
+ --oc-syntax-unit: light-dark(var(--color-peru-80), var(--color-peru-40));
3901
+ --oc-syntax-key: light-dark(var(--color-brown-70), var(--color-brown-10));
3902
+ --oc-code-block-line-number-color: light-dark(var(--color-gray-40), oklch(from var(--color-white-2) l c h / 0.25));
3903
+ }
3904
+ .oc-code-window--dark { color-scheme: dark; box-shadow: 0 40px 80px -30px oklch(from var(--color-black-50) l c h / 0.6); }
3905
+ .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); }
3906
+ /* The code block inside follows the window instead of its own always-dark theme. */
3907
+ .oc-code-window :is(.oc-code-block, pre.code-preview) { color-scheme: inherit; }
3783
3908
  .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
3909
  .oc-code-window__bar > i { inline-size: 0.7rem; aspect-ratio: 1; border-radius: 50%; background: var(--color-red-50); }
3785
3910
  .oc-code-window__bar > i:nth-child(2) { background: var(--color-orange-60); }
@@ -3880,19 +4005,20 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3880
4005
  background: oklch(from var(--oc-accent-from) l c h / 0.14);
3881
4006
  color: light-dark(var(--oc-accent-from), oklch(from var(--oc-accent-from) max(l, 0.72) c h));
3882
4007
  }
3883
- /* Pulse: the ring starts at --oc-status-dot-pulse-alpha of the accent and grows to --oc-status-dot-pulse-size.
4008
+ /* Live dot in a pill (.oc-pill > .oc-status-dot): the ring starts at --oc-status-dot-pulse-alpha of the accent and grows
4009
+ to --oc-status-dot-pulse-size. Scoped to the pill: a plain .oc-status-dot is the status light (parts/app-status-dot.css).
3884
4010
  --strong starts at the full accent with a wider ring. */
3885
- .oc-status-dot {
4011
+ .oc-pill .oc-status-dot {
3886
4012
  --oc-status-dot-pulse-alpha: 0.6;
3887
4013
  --oc-status-dot-pulse-size: 8px;
3888
4014
  inline-size: 0.5rem;
3889
4015
  aspect-ratio: 1;
3890
4016
  border-radius: 50%;
3891
4017
  background: var(--oc-accent-from);
3892
- animation: oc-pulse 2s infinite;
4018
+ animation: oc-status-dot-ring 2s infinite;
3893
4019
  }
3894
- .oc-status-dot--strong { --oc-status-dot-pulse-alpha: 1; --oc-status-dot-pulse-size: 10px; }
3895
- @keyframes oc-pulse {
4020
+ .oc-pill .oc-status-dot--strong { --oc-status-dot-pulse-alpha: 1; --oc-status-dot-pulse-size: 10px; }
4021
+ @keyframes oc-status-dot-ring {
3896
4022
  0% { box-shadow: 0 0 0 0 oklch(from var(--oc-accent-from) l c h / var(--oc-status-dot-pulse-alpha)); }
3897
4023
  70%, 100% { box-shadow: 0 0 0 var(--oc-status-dot-pulse-size) transparent; }
3898
4024
  }
@@ -4053,7 +4179,7 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
4053
4179
 
4054
4180
  @media (prefers-reduced-motion: reduce) {
4055
4181
  .oc-backdrop::before, .oc-backdrop::after, .oc-backdrop > .oc-backdrop__blob,
4056
- .oc-icon-tile--float, .oc-marquee__track, .oc-status-dot, .oc-reveal, .oc-site-header { animation: none; }
4182
+ .oc-icon-tile--float, .oc-marquee__track, .oc-pill .oc-status-dot, .oc-reveal, .oc-site-header { animation: none; }
4057
4183
  }
4058
4184
 
4059
4185
  /* ===================================================================
@@ -4436,7 +4562,21 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
4436
4562
  .oc-slider:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 4px oklch(from var(--oc-ring) l c h / 0.28); }
4437
4563
  .oc-slider:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 4px oklch(from var(--oc-ring) l c h / 0.28); }
4438
4564
  .oc-slider:disabled { opacity: 0.5; cursor: not-allowed; }
4439
- .oc-slider--vertical { writing-mode: vertical-lr; direction: rtl; inline-size: var(--oc-slider-thumb); block-size: 10rem; }
4565
+ /* In a vertical writing mode the inline axis is vertical: inline-size is the height of the slider. */
4566
+ .oc-slider--vertical { writing-mode: vertical-lr; direction: rtl; inline-size: 10rem; block-size: var(--oc-slider-thumb); }
4567
+ /* Scale under a stepped slider: one mark per step (or per labelled value), centered under the thumb positions.
4568
+ The padding keeps the first and last mark under the thumb center at min and max. */
4569
+ .oc-slider-scale {
4570
+ display: flex;
4571
+ justify-content: space-between;
4572
+ padding-inline: calc(var(--oc-slider-thumb, 1.125rem) / 2);
4573
+ margin-block-start: 0.25rem;
4574
+ font-size: 0.6875rem;
4575
+ font-variant-numeric: tabular-nums;
4576
+ color: var(--oc-muted-foreground);
4577
+ }
4578
+ .oc-slider-scale > * { display: flex; flex-direction: column; align-items: center; gap: 0.125rem; inline-size: 0; }
4579
+ .oc-slider-scale > *::before { content: ""; inline-size: 1px; block-size: 0.375rem; background: var(--oc-border); }
4440
4580
  .oc-slider--vertical::-webkit-slider-runnable-track {
4441
4581
  background: linear-gradient(to top, var(--oc-primary) var(--oc-slider-progress), var(--oc-muted) var(--oc-slider-progress));
4442
4582
  }
@@ -5439,93 +5579,6 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5439
5579
  @keyframes oc-busy-in { from { opacity: 0; } }
5440
5580
  @media (prefers-reduced-motion: reduce) { .oc-busy-overlay[open], .oc-busy-overlay[open]::backdrop { animation-duration: 0s; } }
5441
5581
 
5442
- /* parts/app-column-menu.css */
5443
- /* File: packages/ui/src/parts/app-column-menu.css | Map: menu panel -> title + list -> item states -> footer -> CSS-only column hiding (scope) */
5444
- /* Column menu: a popover with one checkbox per table column (column visibility).
5445
- CSS-only hiding: wrap the menu and the table in .oc-column-menu-scope and give each checkbox
5446
- data-col="<column number>". An unchecked box hides that column (th and td) through :has(). */
5447
-
5448
- /* Panel look. Without the popover attribute it is a normal block (e.g. inside a sheet or sidebar). */
5449
- .oc-column-menu {
5450
- inline-size: 14rem;
5451
- max-inline-size: calc(100vw - 1rem);
5452
- padding: 0.375rem;
5453
- font-size: 0.875rem;
5454
- color: var(--oc-popover-foreground);
5455
- background: var(--oc-popover);
5456
- border: 1px solid var(--oc-border);
5457
- border-radius: var(--oc-radius);
5458
- box-shadow: 0 8px 24px oklch(from var(--color-black-60) l c h / 0.15);
5459
- }
5460
- /* As a popover: in the top layer, tethered to its trigger like the dropdown. */
5461
- .oc-column-menu[popover] {
5462
- position: fixed;
5463
- inset: auto;
5464
- margin: 4px 0 0;
5465
- position-area: block-end span-inline-start;
5466
- position-try-fallbacks: flip-block, flip-inline;
5467
- transition: opacity var(--oc-transition-fast), display var(--oc-transition-fast) allow-discrete, overlay var(--oc-transition-fast) allow-discrete;
5468
- }
5469
- .oc-column-menu[popover]:popover-open {
5470
- display: block;
5471
- @starting-style { opacity: 0; }
5472
- }
5473
-
5474
- /* Small heading above the list ("Columns"). */
5475
- .oc-column-menu-title {
5476
- margin: 0;
5477
- padding: 0.375rem 0.5rem 0.25rem;
5478
- font-size: 0.75rem;
5479
- font-weight: 600;
5480
- color: var(--oc-muted-foreground);
5481
- }
5482
-
5483
- /* The list is a <form>, so a native <button type="reset"> restores the default columns. */
5484
- .oc-column-menu-list { display: grid; gap: 1px; margin: 0; max-block-size: 18rem; overflow-y: auto; }
5485
-
5486
- /* One row: label around a checkbox and the column name. */
5487
- .oc-column-menu-item {
5488
- display: flex;
5489
- align-items: center;
5490
- gap: 0.5rem;
5491
- padding: 0.375rem 0.5rem;
5492
- border-radius: calc(var(--oc-radius) - 2px);
5493
- cursor: pointer;
5494
- user-select: none;
5495
- }
5496
- .oc-column-menu-item:hover { background: var(--oc-accent); }
5497
- .oc-column-menu-item:has(:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: -2px; }
5498
- .oc-column-menu-item input { margin: 0; accent-color: var(--oc-primary); }
5499
- /* Locked column (always visible): <input disabled checked>. */
5500
- .oc-column-menu-item:has(input:disabled) { color: var(--oc-muted-foreground); cursor: default; }
5501
- .oc-column-menu-item:has(input:disabled):hover { background: none; }
5502
-
5503
- /* Footer row with "Reset" (type="reset") or other small buttons. */
5504
- .oc-column-menu-footer {
5505
- display: flex;
5506
- justify-content: flex-end;
5507
- gap: 0.25rem;
5508
- margin-block-start: 0.375rem;
5509
- padding-block-start: 0.375rem;
5510
- border-block-start: 1px solid var(--oc-border);
5511
- }
5512
-
5513
- /* ---- CSS-only column hiding (columns 1 to 10) ---- */
5514
- .oc-column-menu-scope:has([data-col="1"]:not(:checked)) .oc-table :is(th, td):nth-child(1),
5515
- .oc-column-menu-scope:has([data-col="2"]:not(:checked)) .oc-table :is(th, td):nth-child(2),
5516
- .oc-column-menu-scope:has([data-col="3"]:not(:checked)) .oc-table :is(th, td):nth-child(3),
5517
- .oc-column-menu-scope:has([data-col="4"]:not(:checked)) .oc-table :is(th, td):nth-child(4),
5518
- .oc-column-menu-scope:has([data-col="5"]:not(:checked)) .oc-table :is(th, td):nth-child(5),
5519
- .oc-column-menu-scope:has([data-col="6"]:not(:checked)) .oc-table :is(th, td):nth-child(6),
5520
- .oc-column-menu-scope:has([data-col="7"]:not(:checked)) .oc-table :is(th, td):nth-child(7),
5521
- .oc-column-menu-scope:has([data-col="8"]:not(:checked)) .oc-table :is(th, td):nth-child(8),
5522
- .oc-column-menu-scope:has([data-col="9"]:not(:checked)) .oc-table :is(th, td):nth-child(9),
5523
- .oc-column-menu-scope:has([data-col="10"]:not(:checked)) .oc-table :is(th, td):nth-child(10) { display: none; }
5524
-
5525
- @media (prefers-reduced-motion: reduce) {
5526
- .oc-column-menu[popover] { transition: none; }
5527
- }
5528
-
5529
5582
  /* parts/app-confirm-dialog.css */
5530
5583
  /* 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 */
5531
5584
  /* Confirm dialog: a variant of the alert dialog (dialog.oc-modal + .oc-alert-dialog).
@@ -5663,9 +5716,213 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5663
5716
  }
5664
5717
  @media (prefers-reduced-motion: reduce) { .oc-cookie-consent { transition: none; } }
5665
5718
 
5719
+ /* parts/app-data-table-footer.css */
5720
+ /* File: packages/ui/src/parts/app-data-table-footer.css | Map: footer grid (count, rows per page, pager) -> page buttons -> narrow screens */
5721
+ /* 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. */
5722
+
5723
+ /* Footer: count on the left, rows-per-page in the middle, pager on the right. */
5724
+ .oc-data-table-footer {
5725
+ display: grid;
5726
+ grid-template-columns: 1fr auto 1fr;
5727
+ align-items: center;
5728
+ gap: 0.75rem;
5729
+ }
5730
+ .oc-data-table-summary { margin: 0; font-size: 0.8125rem; color: var(--oc-muted-foreground); }
5731
+ .oc-data-table-per-page { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem; }
5732
+ .oc-data-table-per-page .oc-select { inline-size: auto; }
5733
+ .oc-data-table-pager { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.25rem; }
5734
+ .oc-data-table-page {
5735
+ min-inline-size: 2rem;
5736
+ padding: 0.25rem 0.45rem;
5737
+ font: inherit;
5738
+ font-size: 0.8125rem;
5739
+ color: var(--oc-foreground);
5740
+ background: var(--oc-card);
5741
+ border: 1px solid var(--oc-border);
5742
+ border-radius: calc(var(--oc-radius) - 2px);
5743
+ cursor: pointer;
5744
+ }
5745
+ .oc-data-table-page[aria-current="page"] {
5746
+ color: var(--oc-primary);
5747
+ background: oklch(from var(--oc-primary) l c h / 0.1);
5748
+ border-color: oklch(from var(--oc-primary) l c h / 0.35);
5749
+ }
5750
+ .oc-data-table-page:disabled { opacity: 0.4; cursor: default; }
5751
+ .oc-data-table-page:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
5752
+ .oc-data-table-ellipsis { padding-inline: 0.25rem; color: var(--oc-muted-foreground); }
5753
+
5754
+ @media (max-width: 40rem) {
5755
+ .oc-data-table-footer { grid-template-columns: 1fr; }
5756
+ .oc-data-table-pager { justify-content: flex-start; }
5757
+ }
5758
+
5759
+ /* parts/app-data-table-rich.css */
5760
+ /* 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 */
5761
+ /* The rich relations list on top of app-data-table.css: tabs that filter a column, filter selects with an icon,
5762
+ rich cells and the label/value lines of the card view. Behaviour: data-oc-col-filter in src/data-table.js. */
5763
+
5764
+ /* Every control is 2rem high: search, filter selects, trash pills, icon buttons, rows per page and the pager. */
5765
+ .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; }
5766
+ .oc-data-table .oc-data-table-page { min-block-size: 2rem; }
5767
+ .oc-data-table :is(.oc-data-table-toolbar, .oc-data-table-filters, thead) .oc-btn--icon { --oc-btn-square: 2rem; }
5768
+ .oc-data-table-toolbar .oc-filter-pill {
5769
+ display: inline-flex;
5770
+ align-items: center;
5771
+ block-size: 2rem;
5772
+ padding-inline: 0.625rem;
5773
+ border-radius: calc(var(--oc-radius) - 2px);
5774
+ }
5775
+
5776
+ /* Header: the sort mark only on the sorted column (and on hover); the actions header holds the column button. */
5777
+ /* The sort mark always keeps its 1rem, so sorting another column never moves the header: empty while unsorted,
5778
+ faint chevrons on hover, the accent sort icon on the sorted column. */
5779
+ :is(.oc-data-table, .oc-block-datatable) th:has(.oc-table-sort) { cursor: pointer; }
5780
+ :is(.oc-data-table, .oc-block-datatable) th .oc-table-sort::after { content: ""; flex-shrink: 0; inline-size: 1rem; block-size: 1rem; opacity: 1; }
5781
+ :is(.oc-data-table, .oc-block-datatable) th:not([aria-sort]):hover .oc-table-sort::after {
5782
+ background: var(--oc-muted-foreground);
5783
+ opacity: 0.6;
5784
+ -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;
5785
+ 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;
5786
+ }
5787
+ /* Sorted column: a bold label and a sort icon in the accent color. aria-sort on a header in the HTML
5788
+ is the column sorted when the page opens. */
5789
+ :is(.oc-data-table, .oc-block-datatable) th[aria-sort] .oc-table-sort { color: var(--oc-foreground); }
5790
+ :is(.oc-data-table, .oc-block-datatable) th[aria-sort] .oc-table-sort::after {
5791
+ content: "";
5792
+ inline-size: 1rem;
5793
+ block-size: 1rem;
5794
+ opacity: 1;
5795
+ background: var(--oc-primary);
5796
+ -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;
5797
+ 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;
5798
+ }
5799
+ :is(.oc-data-table, .oc-block-datatable) th[aria-sort="descending"] .oc-table-sort::after {
5800
+ -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");
5801
+ 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");
5802
+ }
5803
+ .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); }
5804
+ .oc-data-table td[data-oc-col="actions"] { text-align: end; }
5805
+ /* Row actions per status: data-oc-when="active" (edit, view) only on live rows, data-oc-when="deleted" (restore,
5806
+ delete permanently) only on rows in the trash. */
5807
+ .oc-data-table tr[data-oc-status="deleted"] [data-oc-when="active"],
5808
+ .oc-data-table tr:not([data-oc-status="deleted"]) [data-oc-when="deleted"] { display: none; }
5809
+ /* The column chooser in the actions header: a large plain icon (1.75rem), no button frame. */
5810
+ .oc-data-table-columns-btn { font-size: 1.75rem; }
5811
+ .oc-data-table-columns-btn > :is(svg, iconify-icon) { font-size: 1em; inline-size: 1em; block-size: 1em; }
5812
+
5813
+ /* Tabs above the toolbar (All / Customers / Companies …): icon, label and a count; they filter one column. */
5814
+ .oc-data-table-tabs { display: flex; inline-size: 100%; overflow-x: auto; scrollbar-width: none; }
5815
+ .oc-data-table-tabs .oc-tab-trigger { flex-shrink: 0; }
5816
+ .oc-data-table-tabs .oc-tab-trigger { display: inline-flex; align-items: center; gap: 0.4rem; }
5817
+ .oc-data-table-tabs .oc-tab-trigger :is(svg, iconify-icon) { font-size: 1.1em; inline-size: 1.1em; block-size: 1.1em; }
5818
+ .oc-data-table-tabs .oc-tab-trigger[aria-selected="true"] { color: var(--oc-primary); border-bottom-color: var(--oc-primary); }
5819
+ .oc-data-table-tab-count {
5820
+ min-inline-size: 1.25rem;
5821
+ padding: 0 0.375rem;
5822
+ font-size: 0.6875rem;
5823
+ line-height: 1.125rem;
5824
+ text-align: center;
5825
+ color: var(--oc-muted-foreground);
5826
+ background: var(--oc-muted);
5827
+ border-radius: 999px;
5828
+ }
5829
+ .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); }
5830
+ /* A count in the color of its type (the same tone as the type badge in the table). */
5831
+ .oc-data-table-tab-count:is(.oc-data-table-tab-count--success, .oc-data-table-tab-count--info, .oc-data-table-tab-count--warning) {
5832
+ color: var(--oc-tab-tone);
5833
+ background: oklch(from var(--oc-tab-tone) l c h / 0.12);
5834
+ box-shadow: inset 0 0 0 1px oklch(from var(--oc-tab-tone) l c h / 0.35);
5835
+ }
5836
+ .oc-data-table-tab-count--success { --oc-tab-tone: var(--oc-success); }
5837
+ .oc-data-table-tab-count--info { --oc-tab-tone: var(--oc-info, light-dark(var(--color-deepskyblue-70), var(--color-skyblue-50))); }
5838
+ .oc-data-table-tab-count--warning { --oc-tab-tone: var(--oc-warning); }
5839
+
5840
+ /* Filter row: selects with a leading icon (status, type …) and the column button on the right. */
5841
+ .oc-data-table-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
5842
+ .oc-data-table-filters[hidden] { display: none; }
5843
+ .oc-data-table-filters > :last-child:is(.oc-btn) { margin-inline-start: auto; }
5844
+ .oc-data-table-filter { position: relative; display: inline-flex; align-items: center; }
5845
+ .oc-data-table-filter > :is(svg, iconify-icon) {
5846
+ position: absolute;
5847
+ inset-inline-start: 0.625rem;
5848
+ font-size: 1rem;
5849
+ inline-size: 1rem;
5850
+ block-size: 1rem;
5851
+ color: var(--oc-muted-foreground);
5852
+ pointer-events: none;
5853
+ }
5854
+ .oc-data-table-filter > .oc-select { inline-size: auto; min-inline-size: 11rem; padding-inline-start: 2.125rem; }
5855
+
5856
+ /* Cells. Name: avatar, name with a badge, and a muted second line (VAT number, e-mail …). */
5857
+ .oc-data-table-name { display: flex; align-items: center; gap: 0.625rem; min-inline-size: 0; }
5858
+ .oc-data-table-name .oc-avatar { inline-size: 2.25rem; block-size: 2.25rem; font-size: 0.75rem; }
5859
+ .oc-data-table-name-text { display: grid; min-inline-size: 0; }
5860
+ .oc-data-table-name-line { display: flex; align-items: center; gap: 0.375rem; font-weight: 600; }
5861
+ .oc-data-table-name-sub { font-size: 0.75rem; color: var(--oc-muted-foreground); }
5862
+ /* One line with an ellipsis; put the full text in title="". */
5863
+ .oc-data-table-truncate { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-inline-size: var(--oc-data-table-truncate, 18rem); }
5864
+ /* A count that opens the related list (projects, invoices …); muted while it is 0. */
5865
+ .oc-data-table-count { font-variant-numeric: tabular-nums; }
5866
+ .oc-data-table-count[data-count="0"] { color: var(--oc-muted-foreground); opacity: 0.75; }
5867
+ /* The sort is visible in the header (icon, bold label); the summary line is for screen readers only. */
5868
+ .oc-data-table-sort-summary {
5869
+ position: absolute;
5870
+ inline-size: 1px;
5871
+ block-size: 1px;
5872
+ margin: 0;
5873
+ overflow: hidden;
5874
+ clip-path: inset(50%);
5875
+ white-space: nowrap;
5876
+ }
5877
+ .oc-data-table-file { display: none; }
5878
+
5879
+ /* The other visible columns as label / value lines, and the row actions at the bottom. */
5880
+ .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; }
5881
+ .oc-data-table-card-fields dt { color: var(--oc-muted-foreground); }
5882
+ .oc-data-table-card-fields dd { margin: 0; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
5883
+ .oc-data-table-card-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.375rem; margin-block-start: auto; }
5884
+
5885
+ /* Code cells (article numbers, SKUs): monospace and a step smaller. */
5886
+ .oc-data-table-code { font-family: var(--oc-font-mono); font-size: 0.75rem; }
5887
+
5888
+ /* Version 2, ".oc-data-table--bordered": a tinted header row, a line between the columns and the
5889
+ sort chevrons always visible; the filter select has its icon in a zone of its own. The rest is the same table. */
5890
+ .oc-data-table.oc-data-table--bordered .oc-table :is(th, td) { border-inline-end: 1px solid var(--oc-border); }
5891
+ .oc-data-table.oc-data-table--bordered .oc-table :is(th, td):last-child { border-inline-end: 0; }
5892
+ .oc-data-table.oc-data-table--bordered .oc-table thead th {
5893
+ font-weight: 500;
5894
+ background: color-mix(in oklch, var(--oc-card), var(--oc-foreground) 4%);
5895
+ }
5896
+ .oc-data-table.oc-data-table--bordered .oc-table thead th[data-oc-col="actions"] {
5897
+ background: color-mix(in oklch, var(--oc-card), var(--oc-foreground) 4%);
5898
+ }
5899
+ .oc-data-table--bordered td[data-oc-col="actions"] { text-align: center; }
5900
+ .oc-data-table--bordered th[data-oc-col="actions"] .oc-data-table-th { justify-content: center; }
5901
+ .oc-data-table--bordered th:not([aria-sort]) .oc-table-sort::after,
5902
+ .oc-data-table--bordered th:not([aria-sort]):hover .oc-table-sort::after {
5903
+ content: "";
5904
+ inline-size: 0.875rem;
5905
+ block-size: 0.875rem;
5906
+ opacity: 0.5;
5907
+ background: currentColor;
5908
+ -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;
5909
+ 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;
5910
+ }
5911
+ .oc-data-table--bordered .oc-data-table-filter::after {
5912
+ content: "";
5913
+ position: absolute;
5914
+ inset-block: 0;
5915
+ inset-inline-start: 2rem;
5916
+ inline-size: 1px;
5917
+ background: var(--oc-border);
5918
+ pointer-events: none;
5919
+ }
5920
+ .oc-data-table--bordered .oc-data-table-filter > .oc-select { padding-inline-start: 2.75rem; }
5921
+
5666
5922
  /* parts/app-data-table.css */
5667
- /* 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 */
5668
- /* List table from Korrekt and OmniaDesk. The frame, toolbar and footer are styling.
5923
+ /* 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 */
5924
+ /* List table for business apps. The frame, toolbar and footer are styling. Tabs, filter row, rich cells and
5925
+ card fields: app-data-table-rich.css; footer and pager: app-data-table-footer.css.
5669
5926
  Behaviour (search, sort, pages, resize, drag, trash, bulk) lives in src/data-table.js.
5670
5927
  Set --oc-data-table-max on the shell when the body should scroll and the header should stick. */
5671
5928
 
@@ -5678,13 +5935,16 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5678
5935
  background: var(--oc-card);
5679
5936
  border: 1px solid var(--oc-border);
5680
5937
  border-radius: var(--oc-radius);
5938
+ /* Full width, and in a grid or flex parent the table scrolls inside the frame instead of stretching the card. */
5939
+ inline-size: 100%;
5940
+ min-inline-size: 0;
5681
5941
  }
5682
5942
 
5683
- /* Toolbar: title + gear, search, trash pills, table/cards toggle, page actions. */
5943
+ /* Toolbar: title + gear/filter button, search (takes the free width), trash pills, table/cards toggle, page actions. */
5684
5944
  .oc-data-table-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
5685
5945
  .oc-data-table-heading { display: flex; align-items: center; gap: 0.5rem; min-inline-size: 0; }
5686
5946
  .oc-data-table-title { font-size: 0.875rem; font-weight: 600; }
5687
- .oc-data-table-search { flex: 1 1 12rem; min-inline-size: 10rem; max-inline-size: 22rem; }
5947
+ .oc-data-table-search { flex: 1 1 10rem; min-inline-size: 9rem; }
5688
5948
  .oc-data-table-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-inline-start: auto; }
5689
5949
  .oc-data-table-view { display: inline-flex; gap: 0.25rem; }
5690
5950
  .oc-data-table-view [aria-pressed="true"] {
@@ -5692,55 +5952,52 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5692
5952
  background: oklch(from var(--oc-primary) l c h / 0.1);
5693
5953
  border-color: oklch(from var(--oc-primary) l c h / 0.35);
5694
5954
  }
5695
- .oc-data-table-sort-summary { flex-basis: 100%; margin: 0; font-size: 0.75rem; color: var(--oc-muted-foreground); }
5696
- .oc-data-table-sort-summary:empty { display: none; }
5697
- .oc-data-table-file { display: none; }
5955
+ /* In the toolbar the trash pills read as one switch: the chosen one is filled. */
5956
+ .oc-data-table-toolbar .oc-filter-pills { border-inline-start: 0; padding-inline-start: 0; }
5957
+ .oc-data-table-toolbar .oc-filter-pill[aria-current="true"] {
5958
+ color: var(--oc-primary-foreground);
5959
+ background: var(--oc-primary);
5960
+ border-color: var(--oc-primary);
5961
+ }
5698
5962
 
5699
5963
  /* The table sits in its own rounded frame. The scroll box keeps the header sticky. */
5700
5964
  .oc-data-table-frame {
5965
+ min-inline-size: 0;
5701
5966
  overflow: hidden;
5702
- background: var(--oc-background);
5967
+ background: var(--oc-card);
5703
5968
  border: 1px solid var(--oc-border);
5704
5969
  border-radius: calc(var(--oc-radius) - 2px);
5705
5970
  }
5706
5971
  .oc-data-table-scroll { overflow: auto; max-block-size: var(--oc-data-table-max, none); }
5707
5972
  .oc-data-table .oc-table :is(th, td) { border-inline: 0; border-block-start: 0; }
5973
+ /* Columns size to their content and cells stay on one line: a narrow screen scrolls the frame sideways instead of
5974
+ squeezing names and phone numbers onto two lines. .oc-data-table-wrap on a cell lets long text wrap again. */
5975
+ .oc-data-table .oc-data-table-table { table-layout: auto; }
5976
+ .oc-data-table .oc-data-table-table :is(th, td) { white-space: nowrap; vertical-align: middle; padding-inline: 0.625rem; }
5977
+ .oc-data-table .oc-data-table-table td.oc-data-table-wrap { white-space: normal; }
5708
5978
  .oc-data-table .oc-table thead th {
5709
5979
  position: sticky;
5710
5980
  inset-block-start: 0;
5711
5981
  z-index: 1;
5712
- block-size: 2.5rem;
5713
- font-size: 0.75rem;
5714
- font-weight: 500;
5982
+ block-size: 2.75rem;
5983
+ font-size: 0.8125rem;
5984
+ font-weight: 600;
5985
+ color: var(--oc-foreground);
5715
5986
  background: var(--oc-card);
5716
5987
  }
5717
- .oc-data-table .oc-table tbody tr:nth-child(even) { background: var(--oc-surface); }
5988
+ /* Striped rows (every second visible row), the row under the pointer a step stronger. Solid colors, so the sticky
5989
+ actions cell can take the same background (inherit) and nothing scrolls through it. */
5990
+ .oc-data-table {
5991
+ --oc-data-table-stripe: color-mix(in oklch, var(--oc-card), var(--oc-foreground) 3.5%);
5992
+ --oc-data-table-hover: color-mix(in oklch, var(--oc-card), var(--oc-primary) 8%);
5993
+ }
5994
+ .oc-data-table .oc-table tbody tr:nth-child(even of :not([hidden])) { background: var(--oc-data-table-stripe); }
5995
+ .oc-data-table .oc-table tbody tr:hover { background: var(--oc-data-table-hover); }
5996
+ .oc-data-table--plain .oc-table tbody tr:nth-child(even of :not([hidden])):not(:hover) { background: none; }
5718
5997
  .oc-data-table .oc-table tbody tr:last-child > td { border-block-end: 0; }
5719
5998
 
5720
- /* Header label and the drag handle that sets the column width. */
5721
- .oc-data-table-th { display: flex; align-items: center; gap: 0.25rem; position: relative; min-inline-size: 0; }
5722
- .oc-data-table-resize {
5723
- position: absolute;
5724
- inset-block: 0;
5725
- inset-inline-end: -0.45rem;
5726
- inline-size: 0.55rem;
5727
- padding: 0;
5728
- background: none;
5729
- border: 0;
5730
- cursor: col-resize;
5731
- touch-action: none;
5732
- }
5733
- .oc-data-table-resize::after {
5734
- content: "";
5735
- position: absolute;
5736
- inset-block: 0.4rem;
5737
- inset-inline-start: 0.25rem;
5738
- inline-size: 1px;
5739
- background: var(--oc-border);
5740
- }
5741
- .oc-data-table.is-resizing { cursor: col-resize; user-select: none; }
5742
- /* After a resize the table grows with its columns and the frame scrolls sideways. */
5743
- .oc-data-table-table--sized { inline-size: max-content; min-inline-size: 100%; }
5999
+ /* Header label (column width by dragging is OmniaUI Pro: app-data-table-pro.css). */
6000
+ .oc-data-table-th { display: flex; align-items: center; gap: 0.25rem; min-inline-size: 0; }
5744
6001
 
5745
6002
  /* Narrow control columns. The actions column stays on the right while the rest scrolls. */
5746
6003
  .oc-data-table .oc-table :is(th, td)[data-oc-col="select"],
@@ -5756,8 +6013,8 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5756
6013
  background: var(--oc-card);
5757
6014
  box-shadow: -1px 0 0 var(--oc-border);
5758
6015
  }
5759
- .oc-data-table .oc-table thead th[data-oc-col="actions"] { z-index: 3; }
5760
- .oc-data-table .oc-table tbody tr:nth-child(even) > td[data-oc-col="actions"] { background: var(--oc-surface); }
6016
+ .oc-data-table .oc-table thead th[data-oc-col="actions"] { z-index: 3; background: var(--oc-card); }
6017
+ .oc-data-table .oc-table tbody tr:is(:nth-child(even of :not([hidden])), :hover) > td[data-oc-col="actions"] { background: inherit; }
5761
6018
 
5762
6019
  /* Trash and archive rows stay readable but quieter than active rows. */
5763
6020
  .oc-data-table tr[data-oc-status="deleted"],
@@ -5786,55 +6043,24 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5786
6043
  padding: 0.75rem;
5787
6044
  }
5788
6045
  .oc-data-table-card {
5789
- padding: 0.75rem;
6046
+ display: flex;
6047
+ flex-direction: column;
6048
+ gap: 0.75rem;
6049
+ padding: 0.875rem;
5790
6050
  background: var(--oc-card);
5791
6051
  border: 1px solid var(--oc-border);
5792
6052
  border-radius: calc(var(--oc-radius) - 2px);
5793
6053
  }
5794
6054
  .oc-data-table-card-title { margin: 0; font-size: 0.875rem; font-weight: 600; }
5795
-
6055
+ .oc-data-table-card-title .oc-data-table-name-sub { font-weight: 400; }
5796
6056
  .oc-data-table-empty { margin: 0; padding: 2rem 1rem; text-align: center; color: var(--oc-muted-foreground); }
5797
6057
  .oc-data-table-empty[hidden] { display: none; }
5798
6058
 
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
6059
 
5830
6060
  /* Design choice from the gear menu: tighter rows, like the compact article lists. */
5831
6061
  .oc-data-table--compact .oc-table :is(th, td) { padding: 0.25rem 0.5rem; font-size: 0.8125rem; }
5832
6062
  .oc-data-table--compact .oc-table thead th { block-size: 2rem; }
5833
6063
 
5834
- @media (max-width: 40rem) {
5835
- .oc-data-table-footer { grid-template-columns: 1fr; }
5836
- .oc-data-table-pager { justify-content: flex-start; }
5837
- }
5838
6064
  @media (prefers-reduced-motion: reduce) {
5839
6065
  .oc-data-table .oc-table tbody tr { transition: none; }
5840
6066
  }
@@ -5971,6 +6197,73 @@ input.oc-input[type="month"] { inline-size: auto; min-inline-size: 11rem; }
5971
6197
  .oc-list-toolbar-view { transition: none; }
5972
6198
  }
5973
6199
 
6200
+ /* parts/app-name-card.css */
6201
+ /* File: packages/ui/src/parts/app-name-card.css | Map: card (details) -> head (chevron, name, rename, amount, actions) -> rename input -> body -> nested cards */
6202
+ /* Name card: a collapsible card for one part of a quote or plan,
6203
+ with its name, a rename pencil, an optional amount and a delete button. Native <details>: it opens without script;
6204
+ packages/ui/src/name-card.js adds the rename (pencil -> input, Enter saves, Escape cancels). */
6205
+
6206
+ .oc-name-card {
6207
+ background: var(--oc-card);
6208
+ border: 1px solid var(--oc-border);
6209
+ border-radius: var(--oc-radius);
6210
+ }
6211
+ .oc-name-card + .oc-name-card { margin-block-start: 0.5rem; }
6212
+ .oc-name-card__head {
6213
+ display: flex;
6214
+ align-items: center;
6215
+ gap: 0.5rem;
6216
+ min-block-size: 2.75rem;
6217
+ padding: 0.375rem 0.5rem 0.375rem 0.75rem;
6218
+ list-style: none;
6219
+ cursor: pointer;
6220
+ }
6221
+ .oc-name-card__head::-webkit-details-marker { display: none; }
6222
+ .oc-name-card__head::before {
6223
+ content: "";
6224
+ flex-shrink: 0;
6225
+ inline-size: 1rem;
6226
+ block-size: 1rem;
6227
+ background: var(--oc-muted-foreground);
6228
+ -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;
6229
+ 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;
6230
+ transition: rotate var(--oc-transition-fast);
6231
+ }
6232
+ .oc-name-card[open] > .oc-name-card__head::before { rotate: 90deg; }
6233
+ .oc-name-card__head:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: -2px; border-radius: var(--oc-radius); }
6234
+ .oc-name-card__title { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
6235
+ .oc-name-card__meta { font-size: 0.75rem; color: var(--oc-muted-foreground); white-space: nowrap; }
6236
+ .oc-name-card__head .oc-btn { flex-shrink: 0; }
6237
+ /* Amount and actions sit on the right; the amount is tabular so a column of cards lines up. */
6238
+ .oc-name-card__amount { margin-inline-start: auto; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
6239
+ .oc-name-card__actions { display: inline-flex; flex-shrink: 0; gap: 0.25rem; margin-inline-start: auto; }
6240
+ .oc-name-card__amount + .oc-name-card__actions { margin-inline-start: 0.5rem; }
6241
+
6242
+ /* Rename: the title becomes an input of the same size (hidden toggles between the two). */
6243
+ .oc-name-card__input {
6244
+ flex: 1;
6245
+ min-inline-size: 0;
6246
+ block-size: 2rem;
6247
+ padding: 0 0.5rem;
6248
+ font: inherit;
6249
+ font-weight: 600;
6250
+ color: var(--oc-foreground);
6251
+ background: var(--oc-background);
6252
+ border: 1px solid var(--oc-ring);
6253
+ border-radius: calc(var(--oc-radius) - 2px);
6254
+ }
6255
+ .oc-name-card__input[hidden],
6256
+ .oc-name-card__title[hidden] { display: none; }
6257
+
6258
+ .oc-name-card__body { padding: 0.75rem 0.75rem 0.875rem 2.25rem; border-block-start: 1px solid var(--oc-border); }
6259
+ /* Nested cards (the parts of a title) use a quieter surface; --total colors the amount of the main card. */
6260
+ .oc-name-card__body > .oc-name-card { background: var(--oc-surface); }
6261
+ .oc-name-card--total > .oc-name-card__head .oc-name-card__amount { color: var(--oc-primary); }
6262
+
6263
+ @media (prefers-reduced-motion: reduce) {
6264
+ .oc-name-card__head::before { transition: none; }
6265
+ }
6266
+
5974
6267
  /* parts/app-page-header.css */
5975
6268
  /* 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) */
5976
6269
  /* Page header: back link + title + subtitle + actions at the top of an app page.
@@ -6455,6 +6748,11 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6455
6748
  min-block-size: 0;
6456
6749
  timeline-scope: --oc-titlebar-scroll;
6457
6750
  }
6751
+ /* A card with a title bar on top: the bar runs edge to edge, its last button follows the card corner. */
6752
+ .oc-card:has(> .oc-titlebar:first-child) { padding: 0; --oc-titlebar-radius: calc(var(--oc-radius) + 3px); }
6753
+ /* As the heading of a card section (bar + .oc-card-content): the title lines up with the content below. */
6754
+ .oc-card > .oc-titlebar:first-child:has(+ .oc-card-content) { padding-inline-start: 24px; }
6755
+ .oc-card > .oc-titlebar:first-child:has(+ .oc-card-content) .oc-titlebar__title { font-size: 0.9375rem; }
6458
6756
 
6459
6757
  .oc-titlebar {
6460
6758
  display: flex;
@@ -6462,7 +6760,7 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6462
6760
  align-items: center;
6463
6761
  gap: 2px;
6464
6762
  min-block-size: 46px;
6465
- padding: 0 8px 0 14px;
6763
+ padding: 0 0 0 14px;
6466
6764
  border-block-end: 1px solid transparent;
6467
6765
  -webkit-user-select: none;
6468
6766
  user-select: none;
@@ -6479,7 +6777,7 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6479
6777
  /* Title area: grows, cuts the subtitle off with …. */
6480
6778
  .oc-titlebar__main { display: flex; flex: 1; align-items: center; gap: 8px; min-inline-size: 0; }
6481
6779
  .oc-titlebar__icon { flex: none; inline-size: 18px; block-size: 18px; }
6482
- .oc-titlebar__title { flex: none; font-size: 0.8667rem; font-weight: 650; letter-spacing: -0.01em; }
6780
+ .oc-titlebar__title { flex: none; margin: 0; font-size: 0.8667rem; font-weight: 650; letter-spacing: -0.01em; }
6483
6781
  .oc-titlebar__subtitle {
6484
6782
  min-inline-size: 0;
6485
6783
  overflow: hidden;
@@ -6489,17 +6787,19 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6489
6787
  white-space: nowrap;
6490
6788
  }
6491
6789
  .oc-titlebar__subtitle::before { content: "·"; margin-inline-end: 8px; }
6492
- .oc-titlebar__actions { display: flex; flex: none; align-items: center; gap: 2px; }
6790
+ /* Window buttons sit in the top right corner (as on a desktop window), not centred on the title line. */
6791
+ .oc-titlebar__actions { display: flex; flex: none; align-self: flex-start; align-items: flex-start; margin-inline-start: auto; }
6493
6792
 
6494
- /* Compact icon buttons: muted, a light surface on hover, the close button red. */
6793
+ /* Window buttons: muted, a light surface on hover, the close button red. The last one follows the
6794
+ rounded corner of the card or window around the bar (--oc-titlebar-radius). */
6495
6795
  .oc-titlebar__btn {
6496
6796
  display: inline-grid;
6497
6797
  place-items: center;
6498
- inline-size: 30px;
6499
- block-size: 30px;
6798
+ inline-size: 40px;
6799
+ block-size: 34px;
6500
6800
  padding: 0;
6501
6801
  border: 0;
6502
- border-radius: 8px;
6802
+ border-radius: 0;
6503
6803
  background: transparent;
6504
6804
  color: var(--oc-muted-foreground);
6505
6805
  font: inherit;
@@ -6510,6 +6810,7 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6510
6810
  .oc-titlebar__btn:hover { background: var(--oc-glass-panel-strong, var(--oc-accent)); color: var(--oc-foreground); }
6511
6811
  .oc-titlebar__btn:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: -2px; }
6512
6812
  .oc-titlebar__btn--close:hover { background: var(--oc-destructive); color: var(--oc-destructive-foreground); }
6813
+ .oc-titlebar__actions > .oc-titlebar__btn:last-child { border-start-end-radius: var(--oc-titlebar-radius, calc(var(--oc-radius) - 1px)); }
6513
6814
 
6514
6815
  /* Body: the part that scrolls under the bar. */
6515
6816
  .oc-titlebar-body {
@@ -6534,7 +6835,7 @@ button.oc-stat-card:focus-visible { outline: 2px solid var(--oc-ring); outline-o
6534
6835
 
6535
6836
  /* Sizes */
6536
6837
  .oc-titlebar--sm { min-block-size: 36px; padding-inline-start: 10px; }
6537
- .oc-titlebar--sm .oc-titlebar__btn { inline-size: 26px; block-size: 26px; border-radius: 6px; }
6838
+ .oc-titlebar--sm .oc-titlebar__btn { inline-size: 34px; block-size: 28px; }
6538
6839
 
6539
6840
  @media (prefers-reduced-motion: reduce) {
6540
6841
  .oc-titlebar-window > .oc-titlebar { animation: none; }
@@ -7059,6 +7360,229 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7059
7360
  .oc-chart-line__dot, .oc-chart-line__tip { transition: none; }
7060
7361
  }
7061
7362
 
7363
+ /* parts/data-meter-row.css */
7364
+ /* File: packages/ui/src/parts/data-meter-row.css | Map: row grid -> label and value -> bar and fill -> levels -> reset line -> list */
7365
+ /* Usage meter row: a label, the usage as text, a bar and when it resets. The fill comes from
7366
+ --oc-meter (0–100%); data-level="warn" or "critical" changes the color. */
7367
+
7368
+ .oc-meter-row {
7369
+ display: grid;
7370
+ grid-template-columns: minmax(0, 1fr) auto;
7371
+ align-items: baseline;
7372
+ gap: 0.375rem 0.75rem;
7373
+ }
7374
+ .oc-meter-row__label { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.875rem; font-weight: 600; }
7375
+ .oc-meter-row__value { font-size: 0.8125rem; font-variant-numeric: tabular-nums; color: var(--oc-muted-foreground); white-space: nowrap; }
7376
+ .oc-meter-row__value strong { color: var(--oc-foreground); }
7377
+ .oc-meter-row__bar {
7378
+ grid-column: 1 / -1;
7379
+ block-size: 0.5rem;
7380
+ overflow: hidden;
7381
+ background: var(--oc-muted);
7382
+ border-radius: 999px;
7383
+ }
7384
+ .oc-meter-row__bar::before {
7385
+ content: "";
7386
+ display: block;
7387
+ inline-size: clamp(0%, var(--oc-meter, 0%), 100%);
7388
+ block-size: 100%;
7389
+ background: var(--oc-meter-color, var(--oc-success));
7390
+ border-radius: inherit;
7391
+ transition: inline-size 0.4s ease;
7392
+ }
7393
+ .oc-meter-row[data-level="warn"] { --oc-meter-color: var(--oc-warning); }
7394
+ .oc-meter-row[data-level="critical"] { --oc-meter-color: var(--oc-destructive); }
7395
+ .oc-meter-row[data-level="critical"] .oc-meter-row__value strong { color: var(--oc-destructive); }
7396
+ .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); }
7397
+ .oc-meter-list { display: grid; gap: 1rem; }
7398
+ .oc-meter-list--compact { gap: 0.625rem; }
7399
+ .oc-meter-list--compact .oc-meter-row__bar { block-size: 0.375rem; }
7400
+
7401
+ @media (prefers-reduced-motion: reduce) {
7402
+ .oc-meter-row__bar::before { transition: none; }
7403
+ }
7404
+
7405
+ /* parts/data-price.css */
7406
+ /* File: packages/ui/src/parts/data-price.css | Map: price block -> main line (amount, old price, discount) -> VAT note and alternative -> sizes and sale */
7407
+ /* A price as shops and quotes show it: the amount, an optional old price and discount badge, a VAT note
7408
+ ("excl. VAT") and the other amount ("€ 121,00 incl. VAT"). Numbers use tabular digits. */
7409
+
7410
+ .oc-price { display: inline-grid; gap: 0.125rem; min-inline-size: 0; }
7411
+ .oc-price__main { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.5rem; }
7412
+ .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; }
7413
+ .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; }
7414
+ .oc-price__main > .oc-badge { align-self: center; }
7415
+ .oc-price__note { font-size: 0.75rem; font-weight: 500; color: var(--oc-muted-foreground); }
7416
+ .oc-price__alt { font-size: 0.8125rem; color: var(--oc-muted-foreground); font-variant-numeric: tabular-nums; }
7417
+
7418
+ .oc-price--sm .oc-price__amount { font-size: 1rem; }
7419
+ .oc-price--lg .oc-price__amount { font-size: 1.75rem; }
7420
+ .oc-price--sale .oc-price__amount { color: var(--oc-destructive); }
7421
+ /* Inline: amount and note on one line (table cells, lists). */
7422
+ .oc-price--inline { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.5rem; }
7423
+ .oc-price--inline .oc-price__amount { font-size: inherit; font-weight: 600; }
7424
+ td[data-numeric] > .oc-price, .oc-price--end { justify-items: end; text-align: end; }
7425
+ td[data-numeric] > .oc-price .oc-price__main, .oc-price--end .oc-price__main { justify-content: flex-end; }
7426
+
7427
+ /* parts/data-ribbon.css */
7428
+ /* File: packages/ui/src/parts/data-ribbon.css | Map: host (positioned, clipped) -> ribbon band -> four corners -> tones -> size -> RTL */
7429
+ /* Corner ribbon: a diagonal band across one corner of a card, an image or a document ("Paid", "New", "Sale").
7430
+ Put .oc-ribbon as a child of the box; the box gets position: relative and clips the band (zero specificity,
7431
+ so your own position or overflow wins). The text is real text, so screen readers read it. */
7432
+
7433
+ :where(:has(> .oc-ribbon)) { position: relative; overflow: clip; }
7434
+
7435
+ .oc-ribbon {
7436
+ /* Distance from the corner to the middle of the band, along both edges. */
7437
+ --oc-ribbon-inset: 2.25rem;
7438
+ --oc-ribbon-x: 1;
7439
+ --oc-ribbon-y: -1;
7440
+ --oc-ribbon-turn: 45deg;
7441
+ --oc-ribbon-bg: var(--oc-primary);
7442
+ --oc-ribbon-fg: var(--oc-primary-foreground);
7443
+ position: absolute;
7444
+ z-index: 2;
7445
+ inset-block-start: var(--oc-ribbon-inset);
7446
+ inset-inline-end: var(--oc-ribbon-inset);
7447
+ inline-size: calc(var(--oc-ribbon-inset) * 2.83 + 3rem);
7448
+ padding: 0.3em 0;
7449
+ overflow: hidden;
7450
+ color: var(--oc-ribbon-fg);
7451
+ background: var(--oc-ribbon-bg);
7452
+ box-shadow: 0 1px 3px oklch(0 0 0 / 0.18);
7453
+ font-size: 0.75rem;
7454
+ font-weight: 700;
7455
+ line-height: 1.25;
7456
+ letter-spacing: 0.08em;
7457
+ text-align: center;
7458
+ text-overflow: ellipsis;
7459
+ text-transform: uppercase;
7460
+ white-space: nowrap;
7461
+ pointer-events: none;
7462
+ translate: calc(50% * var(--oc-ribbon-x)) calc(50% * var(--oc-ribbon-y));
7463
+ rotate: var(--oc-ribbon-turn);
7464
+ }
7465
+
7466
+ /* Corners: top end is the default. */
7467
+ .oc-ribbon--top-start {
7468
+ inset-inline-end: auto;
7469
+ inset-inline-start: var(--oc-ribbon-inset);
7470
+ --oc-ribbon-x: -1;
7471
+ --oc-ribbon-turn: -45deg;
7472
+ }
7473
+ .oc-ribbon--bottom-end {
7474
+ inset-block-start: auto;
7475
+ inset-block-end: var(--oc-ribbon-inset);
7476
+ --oc-ribbon-y: 1;
7477
+ --oc-ribbon-turn: -45deg;
7478
+ }
7479
+ .oc-ribbon--bottom-start {
7480
+ inset-block-start: auto;
7481
+ inset-block-end: var(--oc-ribbon-inset);
7482
+ inset-inline-end: auto;
7483
+ inset-inline-start: var(--oc-ribbon-inset);
7484
+ --oc-ribbon-x: -1;
7485
+ --oc-ribbon-y: 1;
7486
+ }
7487
+
7488
+ /* Tones. */
7489
+ .oc-ribbon--success { --oc-ribbon-bg: var(--oc-success); --oc-ribbon-fg: var(--oc-success-foreground); }
7490
+ .oc-ribbon--warning { --oc-ribbon-bg: var(--oc-warning); --oc-ribbon-fg: var(--oc-warning-foreground); }
7491
+ .oc-ribbon--destructive { --oc-ribbon-bg: var(--oc-destructive); --oc-ribbon-fg: var(--oc-destructive-foreground); }
7492
+ .oc-ribbon--muted { --oc-ribbon-bg: var(--oc-muted); --oc-ribbon-fg: var(--oc-muted-foreground); }
7493
+
7494
+ /* Sizes: a smaller band for a product card, a larger one for a document or a hero image. */
7495
+ .oc-ribbon--sm { --oc-ribbon-inset: 1.6rem; font-size: 0.625rem; }
7496
+ .oc-ribbon--lg { --oc-ribbon-inset: 2.9rem; font-size: 0.875rem; }
7497
+
7498
+ /* Right to left: start and end swap sides, so the band leans the other way. */
7499
+ .oc-ribbon:dir(rtl) { translate: calc(-50% * var(--oc-ribbon-x)) calc(50% * var(--oc-ribbon-y)); rotate: calc(var(--oc-ribbon-turn) * -1); }
7500
+
7501
+ @media (forced-colors: active) {
7502
+ .oc-ribbon { forced-color-adjust: none; color: Canvas; background: CanvasText; }
7503
+ }
7504
+
7505
+ /* parts/data-spec-list.css */
7506
+ /* File: packages/ui/src/parts/data-spec-list.css | Map: list (container) -> groups in columns -> group title -> rows (term / value) -> stack modifier */
7507
+ /* Technical specifications in named groups (Dimensions, Electrical, Package …): label / value rows with tabular
7508
+ digits. Two columns of groups once the list is wide enough (container query), one column with --stack. */
7509
+
7510
+ .oc-spec-list { container-type: inline-size; inline-size: 100%; display: grid; gap: 1.25rem; min-inline-size: 0; }
7511
+ @container (min-width: 40rem) {
7512
+ .oc-spec-list:not(.oc-spec-list--stack) { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 1.25rem 2rem; }
7513
+ }
7514
+ .oc-spec-group { min-inline-size: 0; }
7515
+ .oc-spec-group__title {
7516
+ display: flex;
7517
+ align-items: center;
7518
+ gap: 0.5rem;
7519
+ margin: 0 0 0.25rem;
7520
+ padding-block-end: 0.375rem;
7521
+ font-size: 0.75rem;
7522
+ font-weight: 600;
7523
+ letter-spacing: 0.04em;
7524
+ text-transform: uppercase;
7525
+ color: var(--oc-muted-foreground);
7526
+ border-block-end: 1px solid var(--oc-border);
7527
+ }
7528
+ .oc-spec-group__title > :is(svg, iconify-icon) { inline-size: 1rem; block-size: 1rem; font-size: 1rem; }
7529
+ .oc-spec-group__rows { margin: 0; }
7530
+ .oc-spec-row {
7531
+ display: grid;
7532
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
7533
+ gap: 1rem;
7534
+ padding: 0.4rem 0;
7535
+ font-size: 0.875rem;
7536
+ border-block-end: 1px dashed var(--oc-border);
7537
+ }
7538
+ .oc-spec-row:last-child { border-block-end: 0; }
7539
+ .oc-spec-row > dt { margin: 0; color: var(--oc-muted-foreground); }
7540
+ .oc-spec-row > dd { margin: 0; min-inline-size: 0; font-weight: 500; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
7541
+ .oc-spec-row > dd > small { margin-inline-start: 0.25rem; font-weight: 400; color: var(--oc-muted-foreground); }
7542
+
7543
+ /* parts/data-stock.css */
7544
+ /* File: packages/ui/src/parts/data-stock.css | Map: indicator (dot + text) -> levels (ok, low, out, order) -> badge variant -> count */
7545
+ /* Stock status: a colored dot with a short text ("In stock", "Only 3 left", "Out of stock", "On order").
7546
+ data-level picks the color; the text always says it too, so color is never the only signal. */
7547
+
7548
+ .oc-stock {
7549
+ --oc-stock-color: var(--oc-success);
7550
+ /* Inline text (not flex), so "Only <b>3</b> left" keeps normal word spacing. */
7551
+ display: inline-block;
7552
+ font-size: 0.8125rem;
7553
+ font-weight: 500;
7554
+ line-height: 1.3;
7555
+ color: var(--oc-foreground);
7556
+ white-space: nowrap;
7557
+ }
7558
+ .oc-stock::before {
7559
+ content: "";
7560
+ display: inline-block;
7561
+ margin-inline-end: 0.4rem;
7562
+ vertical-align: 0.05em;
7563
+ inline-size: 0.5rem;
7564
+ block-size: 0.5rem;
7565
+ background: var(--oc-stock-color);
7566
+ border-radius: 50%;
7567
+ box-shadow: 0 0 0 3px oklch(from var(--oc-stock-color) l c h / 0.18);
7568
+ }
7569
+ .oc-stock[data-level="low"] { --oc-stock-color: var(--oc-warning); }
7570
+ .oc-stock[data-level="out"] { --oc-stock-color: var(--oc-destructive); }
7571
+ .oc-stock[data-level="order"] { --oc-stock-color: var(--oc-info, light-dark(var(--color-deepskyblue-70), var(--color-skyblue-50))); }
7572
+ .oc-stock__count { font-weight: 700; font-variant-numeric: tabular-nums; }
7573
+ .oc-stock__note { font-weight: 400; color: var(--oc-muted-foreground); }
7574
+
7575
+ /* Badge: a tinted pill (table cells, product cards). */
7576
+ .oc-stock--badge {
7577
+ padding: 0.125rem 0.5rem 0.125rem 0.45rem;
7578
+ font-size: 0.75rem;
7579
+ color: color-mix(in oklch, var(--oc-stock-color) 75%, var(--oc-foreground));
7580
+ background: oklch(from var(--oc-stock-color) l c h / 0.12);
7581
+ border: 1px solid oklch(from var(--oc-stock-color) l c h / 0.3);
7582
+ border-radius: 999px;
7583
+ }
7584
+ .oc-stock--badge::before { inline-size: 0.4rem; block-size: 0.4rem; margin-inline-end: 0.3rem; vertical-align: 0.1em; box-shadow: none; }
7585
+
7062
7586
  /* parts/forms-affix-input.css */
7063
7587
  /* File: packages/ui/src/parts/forms-affix-input.css
7064
7588
  Map: --affix group (frame on the wrapper) -> inner input -> inline affix (€, %, @) -> states -> --end alignment
@@ -7087,7 +7611,7 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7087
7611
  min-inline-size: 0;
7088
7612
  }
7089
7613
 
7090
- /* Symbol column: same chrome as the Korrekt / OmniaDesk fields (€, %, @, barcode icon).
7614
+ /* Symbol column: the same chrome as the other fields (€, %, @, barcode icon).
7091
7615
  A divider separates the symbol from the value. */
7092
7616
  .oc-input-group.oc-input-group--affix .oc-input-addon {
7093
7617
  align-self: stretch;
@@ -7136,6 +7660,51 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7136
7660
  }
7137
7661
  .oc-input-group--affix:has(.oc-input:disabled) .oc-input { opacity: 1; }
7138
7662
 
7663
+ /* parts/forms-avatar-picker.css */
7664
+ /* 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 */
7665
+ /* Logo or avatar picker: a preview, "Upload" and "Remove", a hint, and dropping a file
7666
+ on the preview. The file input is a real <input type="file">; packages/ui/src/avatar-picker.js shows the preview. */
7667
+
7668
+ .oc-avatar-picker { display: flex; align-items: center; gap: 1rem; }
7669
+ .oc-avatar-picker__preview {
7670
+ position: relative;
7671
+ display: inline-flex;
7672
+ align-items: center;
7673
+ justify-content: center;
7674
+ flex-shrink: 0;
7675
+ inline-size: var(--oc-avatar-picker-size, 4.5rem);
7676
+ block-size: var(--oc-avatar-picker-size, 4.5rem);
7677
+ overflow: hidden;
7678
+ font-size: 1.25rem;
7679
+ font-weight: 600;
7680
+ color: var(--oc-muted-foreground);
7681
+ background: var(--oc-muted);
7682
+ border: 1px dashed var(--oc-border-strong, var(--oc-border));
7683
+ border-radius: 50%;
7684
+ cursor: pointer;
7685
+ }
7686
+ .oc-avatar-picker--square .oc-avatar-picker__preview { border-radius: var(--oc-radius); }
7687
+ .oc-avatar-picker__preview img { inline-size: 100%; block-size: 100%; object-fit: cover; }
7688
+ .oc-avatar-picker--logo .oc-avatar-picker__preview img { object-fit: contain; padding: 0.375rem; background: var(--color-white-50); }
7689
+ .oc-avatar-picker__preview:has(img) { border-style: solid; }
7690
+ .oc-avatar-picker__preview:hover,
7691
+ .oc-avatar-picker.is-dragover .oc-avatar-picker__preview {
7692
+ color: var(--oc-primary);
7693
+ background: oklch(from var(--oc-primary) l c h / 0.08);
7694
+ border-color: var(--oc-primary);
7695
+ }
7696
+ .oc-avatar-picker__preview:has(:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: 2px; }
7697
+ .oc-avatar-picker__file { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
7698
+ .oc-avatar-picker__text { display: grid; gap: 0.375rem; min-inline-size: 0; }
7699
+ .oc-avatar-picker__label { font-size: 0.875rem; font-weight: 600; }
7700
+ .oc-avatar-picker__hint { font-size: 0.75rem; color: var(--oc-muted-foreground); }
7701
+ .oc-avatar-picker__actions { display: flex; flex-wrap: wrap; gap: 0.375rem; }
7702
+ /* "Remove" only while there is an image. */
7703
+ .oc-avatar-picker:not(:has(.oc-avatar-picker__preview img)) [data-oc-avatar-remove] { display: none; }
7704
+ /* A refused file (wrong type or too large): red outline and the reason under the buttons. */
7705
+ .oc-avatar-picker.is-invalid .oc-avatar-picker__preview { border-color: var(--oc-destructive); }
7706
+ .oc-avatar-picker__error { font-size: 0.75rem; font-weight: 500; color: var(--oc-destructive); }
7707
+
7139
7708
  /* parts/forms-barcode-input.css */
7140
7709
  /* File: packages/ui/src/parts/forms-barcode-input.css
7141
7710
  Map: wrapper (field + preview stacked) -> monospace field -> preview area -> placeholder bars -> code caption -> empty / invalid states
@@ -7405,6 +7974,61 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7405
7974
  /* Small swatches. */
7406
7975
  .oc-color-select--sm .oc-color-select-swatch { inline-size: 1.25rem; block-size: 1.25rem; }
7407
7976
 
7977
+ /* parts/forms-date-field.css */
7978
+ /* File: packages/ui/src/parts/forms-date-field.css | Map: field frame -> icon zone -> native date input -> ISO week label -> sizes and states */
7979
+ /* Date field: calendar icon | line | date, with the ISO week at the end when wanted.
7980
+ The date is a native <input type="date"> (keyboard, picker and locale from the browser). The week label is filled
7981
+ by packages/ui/src/date-field.js (or by your own code). */
7982
+
7983
+ .oc-date-field {
7984
+ display: inline-flex;
7985
+ align-items: stretch;
7986
+ block-size: 2.5rem;
7987
+ background: var(--oc-background);
7988
+ border: 1px solid var(--oc-input, var(--oc-border));
7989
+ border-radius: var(--oc-radius);
7990
+ overflow: hidden;
7991
+ }
7992
+ .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); }
7993
+ .oc-date-field__icon {
7994
+ display: inline-flex;
7995
+ align-items: center;
7996
+ justify-content: center;
7997
+ inline-size: 2.5rem;
7998
+ font-size: 1.125rem;
7999
+ color: var(--oc-muted-foreground);
8000
+ border-inline-end: 1px solid var(--oc-border);
8001
+ }
8002
+ .oc-date-field__input {
8003
+ min-inline-size: 0;
8004
+ padding: 0 0.75rem;
8005
+ font: inherit;
8006
+ font-size: 0.875rem;
8007
+ color: var(--oc-foreground);
8008
+ background: transparent;
8009
+ border: 0;
8010
+ outline: none;
8011
+ }
8012
+ .oc-date-field__input::-webkit-calendar-picker-indicator { opacity: 0.55; cursor: pointer; }
8013
+ /* ISO week: a quiet label at the end ("Week 45"); hidden while there is no date. */
8014
+ .oc-date-field__week {
8015
+ display: inline-flex;
8016
+ align-items: center;
8017
+ padding-inline: 0.75rem;
8018
+ font-size: 0.75rem;
8019
+ font-weight: 600;
8020
+ font-variant-numeric: tabular-nums;
8021
+ color: var(--oc-primary);
8022
+ background: oklch(from var(--oc-primary) l c h / 0.08);
8023
+ border-inline-start: 1px solid var(--oc-border);
8024
+ white-space: nowrap;
8025
+ }
8026
+ .oc-date-field__week:empty { display: none; }
8027
+ .oc-date-field--sm { block-size: 2rem; }
8028
+ .oc-date-field--sm .oc-date-field__icon { inline-size: 2rem; font-size: 1rem; }
8029
+ .oc-date-field:has(.oc-date-field__input:disabled) { opacity: 0.55; }
8030
+ .oc-date-field:has(.oc-date-field__input[aria-invalid="true"]) { border-color: var(--oc-destructive); }
8031
+
7408
8032
  /* parts/forms-decimal-input.css */
7409
8033
  /* File: packages/ui/src/parts/forms-decimal-input.css
7410
8034
  Map: field (right-aligned tabular digits) -> unit suffix wrapper -> states -> --start alignment
@@ -7458,6 +8082,21 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7458
8082
  /* Disabled wrapper. */
7459
8083
  .oc-decimal-field:has(.oc-decimal-input:disabled) { opacity: 0.5; cursor: not-allowed; }
7460
8084
 
8085
+ /* parts/forms-density.css */
8086
+ /* File: packages/ui/src/parts/forms-density.css | Map: compact scope -> inputs, selects, date fields, textareas -> buttons -> field gaps */
8087
+ /* Form density: .oc-density-compact on a form or any wrapper makes its controls
8088
+ 2rem high instead of 2.5rem, for dense editors (invoice templates, inline tables). The default needs no class. */
8089
+
8090
+ .oc-density-compact :is(.oc-input, .oc-select, .oc-date-field) { block-size: 2rem; font-size: 0.8125rem; }
8091
+ .oc-density-compact :is(.oc-input, .oc-select) { padding-block: 0; }
8092
+ .oc-density-compact .oc-textarea { padding-block: 0.375rem; font-size: 0.8125rem; }
8093
+ .oc-density-compact .oc-date-field__icon { inline-size: 2rem; font-size: 1rem; }
8094
+ .oc-density-compact .oc-date-field__input { font-size: 0.8125rem; }
8095
+ .oc-density-compact .oc-btn:not(.oc-btn--icon, .oc-btn--plain, .oc-btn--inline) { padding-block: 0.3125rem; font-size: 0.8125rem; }
8096
+ .oc-density-compact .oc-btn--icon { --oc-btn-square: 2rem; }
8097
+ .oc-density-compact .oc-field { gap: 0.25rem; }
8098
+ .oc-density-compact .oc-field-label { font-size: 0.75rem; }
8099
+
7461
8100
  /* parts/forms-filter-counts.css */
7462
8101
  /* File: packages/ui/src/parts/forms-filter-counts.css
7463
8102
  Map: group (grid of tiles) -> tile (label + hidden radio) -> value / label / hint -> tones -> states -> --compact
@@ -7554,6 +8193,72 @@ a.oc-tree-label:focus-visible { outline: 2px solid var(--oc-ring); outline-offse
7554
8193
  .oc-filter-counts--compact .oc-filter-counts-label { font-size: 0.8rem; font-weight: 500; }
7555
8194
  .oc-filter-counts--compact .oc-filter-counts-hint { display: none; }
7556
8195
 
8196
+ /* parts/forms-form-section.css */
8197
+ /* File: packages/ui/src/parts/forms-form-section.css | Map: section (details) -> summary (icon, title, hint, status, chevron) -> body grid -> invalid state */
8198
+ /* Collapsible form section: a long form in named parts that open and close.
8199
+ Native <details>; sections with the same name="" keep one open at a time. */
8200
+
8201
+ .oc-form-section {
8202
+ background: var(--oc-card);
8203
+ border: 1px solid var(--oc-border);
8204
+ border-radius: var(--oc-radius);
8205
+ }
8206
+ .oc-form-section + .oc-form-section { margin-block-start: 0.75rem; }
8207
+ .oc-form-section__summary {
8208
+ display: grid;
8209
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
8210
+ align-items: center;
8211
+ gap: 0.25rem 0.75rem;
8212
+ padding: 0.875rem 1rem;
8213
+ list-style: none;
8214
+ cursor: pointer;
8215
+ }
8216
+ .oc-form-section__summary::-webkit-details-marker { display: none; }
8217
+ .oc-form-section__summary:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: -2px; border-radius: var(--oc-radius); }
8218
+ .oc-form-section__icon {
8219
+ display: inline-flex;
8220
+ align-items: center;
8221
+ justify-content: center;
8222
+ inline-size: 2.25rem;
8223
+ block-size: 2.25rem;
8224
+ font-size: 1.25rem;
8225
+ color: var(--oc-primary);
8226
+ background: oklch(from var(--oc-primary) l c h / 0.1);
8227
+ border-radius: calc(var(--oc-radius) - 2px);
8228
+ }
8229
+ .oc-form-section__heading { display: grid; gap: 0.125rem; min-inline-size: 0; }
8230
+ .oc-form-section__title { margin: 0; font-size: 0.9375rem; font-weight: 600; }
8231
+ .oc-form-section__hint { margin: 0; font-size: 0.8125rem; color: var(--oc-muted-foreground); }
8232
+ .oc-form-section__status { font-size: 0.75rem; white-space: nowrap; }
8233
+ .oc-form-section__summary::after {
8234
+ content: "";
8235
+ inline-size: 1.125rem;
8236
+ block-size: 1.125rem;
8237
+ background: var(--oc-muted-foreground);
8238
+ -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;
8239
+ 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;
8240
+ transition: rotate var(--oc-transition-fast);
8241
+ }
8242
+ .oc-form-section[open] > .oc-form-section__summary::after { rotate: 180deg; }
8243
+ .oc-form-section__body {
8244
+ display: grid;
8245
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
8246
+ gap: 1rem;
8247
+ padding: 1rem;
8248
+ border-block-start: 1px solid var(--oc-border);
8249
+ }
8250
+ .oc-form-section__body > .oc-form-section__wide { grid-column: 1 / -1; }
8251
+ /* A section with an invalid field gets a red edge, also while closed, so an error is never hidden. */
8252
+ .oc-form-section:has(:user-invalid, [aria-invalid="true"]) { border-color: var(--oc-destructive); }
8253
+ .oc-form-section:has(:user-invalid, [aria-invalid="true"]) .oc-form-section__icon {
8254
+ color: var(--oc-destructive);
8255
+ background: oklch(from var(--oc-destructive) l c h / 0.1);
8256
+ }
8257
+
8258
+ @media (prefers-reduced-motion: reduce) {
8259
+ .oc-form-section__summary::after { transition: none; }
8260
+ }
8261
+
7557
8262
  /* parts/forms-language-select.css */
7558
8263
  /* File: packages/ui/src/parts/forms-language-select.css
7559
8264
  Map: wrapper (icon + native select) -> icon -> select control -> chevron -> states -> --ghost / --currency
@@ -7889,6 +8594,146 @@ button.oc-language-select:hover { background: var(--oc-surface); }
7889
8594
  font-size: 0.8rem;
7890
8595
  }
7891
8596
 
8597
+ /* parts/forms-quantity.css */
8598
+ /* File: packages/ui/src/parts/forms-quantity.css | Map: frame -> step buttons -> number input (no spinners) -> focus and disabled -> sizes */
8599
+ /* Quantity field: minus, a number input and plus in one frame. Without a script it is a normal number input
8600
+ (the browser's arrows and keyboard); form-extras.js makes the buttons step within min / max / step. */
8601
+
8602
+ .oc-quantity {
8603
+ display: inline-flex;
8604
+ inline-size: fit-content;
8605
+ justify-self: start;
8606
+ align-items: stretch;
8607
+ block-size: 2.25rem;
8608
+ background: var(--oc-background);
8609
+ border: 1px solid var(--oc-input, var(--oc-border));
8610
+ border-radius: var(--oc-radius);
8611
+ overflow: hidden;
8612
+ }
8613
+ .oc-quantity__step {
8614
+ display: inline-grid;
8615
+ place-items: center;
8616
+ flex-shrink: 0;
8617
+ inline-size: 2.25rem;
8618
+ padding: 0;
8619
+ font: inherit;
8620
+ color: var(--oc-muted-foreground);
8621
+ background: transparent;
8622
+ border: 0;
8623
+ cursor: pointer;
8624
+ }
8625
+ .oc-quantity__step > :is(svg, iconify-icon) { inline-size: 1rem; block-size: 1rem; font-size: 1rem; }
8626
+ .oc-quantity__step:hover { color: var(--oc-foreground); background: var(--oc-accent); }
8627
+ .oc-quantity__step:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: -2px; }
8628
+ .oc-quantity__step:disabled { opacity: 0.35; cursor: not-allowed; background: transparent; }
8629
+ .oc-quantity__input {
8630
+ inline-size: 3.25rem;
8631
+ min-inline-size: 0;
8632
+ padding: 0 0.25rem;
8633
+ font: inherit;
8634
+ font-size: 0.875rem;
8635
+ font-weight: 600;
8636
+ font-variant-numeric: tabular-nums;
8637
+ text-align: center;
8638
+ color: var(--oc-foreground);
8639
+ background: transparent;
8640
+ border: 0;
8641
+ border-inline: 1px solid var(--oc-border);
8642
+ -moz-appearance: textfield;
8643
+ appearance: textfield;
8644
+ }
8645
+ .oc-quantity__input::-webkit-outer-spin-button,
8646
+ .oc-quantity__input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
8647
+ .oc-quantity__input:focus { outline: none; }
8648
+ .oc-quantity:has(.oc-quantity__input:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
8649
+ .oc-quantity:has(.oc-quantity__input:disabled) { opacity: 0.5; }
8650
+ .oc-quantity--sm { block-size: 2rem; }
8651
+ .oc-quantity--sm .oc-quantity__step { inline-size: 2rem; }
8652
+ .oc-quantity--sm .oc-quantity__input { inline-size: 2.75rem; font-size: 0.8125rem; }
8653
+
8654
+ /* parts/forms-rating.css */
8655
+ /* File: packages/ui/src/parts/forms-rating.css | Map: star mask -> rating (display) -> sizes -> summary line -> rating input (radios, hover preview) -> focus */
8656
+ /* Star rating without JavaScript. .oc-rating shows a score (--oc-rating, halves and decimals work); .oc-rating-input
8657
+ is five radio buttons drawn as stars: :has() fills up to the checked or hovered star. */
8658
+
8659
+ .oc-rating,
8660
+ .oc-rating-input {
8661
+ --oc-rating-size: 1.125rem;
8662
+ --oc-rating-color: var(--oc-warning);
8663
+ --oc-rating-empty: oklch(from var(--oc-muted-foreground) l c h / 0.28);
8664
+ --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");
8665
+ }
8666
+
8667
+ /* Display: one bar of five stars; the fill is the score out of --oc-rating-max (default 5). */
8668
+ .oc-rating {
8669
+ --oc-rating: 0;
8670
+ --oc-rating-max: 5;
8671
+ display: inline-block;
8672
+ flex-shrink: 0;
8673
+ inline-size: calc(var(--oc-rating-size) * var(--oc-rating-max));
8674
+ block-size: var(--oc-rating-size);
8675
+ vertical-align: middle;
8676
+ background: linear-gradient(90deg, var(--oc-rating-color) calc(var(--oc-rating) / var(--oc-rating-max) * 100%), var(--oc-rating-empty) 0);
8677
+ -webkit-mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
8678
+ mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
8679
+ }
8680
+ .oc-rating--sm, .oc-rating-input.oc-rating-input--sm { --oc-rating-size: 0.875rem; }
8681
+ .oc-rating--lg, .oc-rating-input.oc-rating-input--lg { --oc-rating-size: 1.5rem; }
8682
+
8683
+ /* Summary: stars, the score and the number of reviews on one line. */
8684
+ .oc-rating-summary { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.5rem; font-size: 0.875rem; }
8685
+ .oc-rating-summary__score { font-weight: 700; font-variant-numeric: tabular-nums; }
8686
+ .oc-rating-summary__count { color: var(--oc-muted-foreground); }
8687
+ a.oc-rating-summary__count { text-decoration: underline; text-underline-offset: 0.2em; }
8688
+ a.oc-rating-summary__count:hover { color: var(--oc-foreground); }
8689
+
8690
+ /* Input: a fieldset with five labels (radio inside). The stars are one masked bar behind the labels. */
8691
+ .oc-rating-input {
8692
+ position: relative;
8693
+ display: inline-flex;
8694
+ inline-size: calc(var(--oc-rating-size) * 5 + 0.5rem);
8695
+ block-size: calc(var(--oc-rating-size) + 0.5rem);
8696
+ margin: 0;
8697
+ padding: 0.25rem;
8698
+ border: 0;
8699
+ border-radius: calc(var(--oc-radius) - 2px);
8700
+ }
8701
+ .oc-rating-input::before {
8702
+ content: "";
8703
+ position: absolute;
8704
+ inset: 0.25rem;
8705
+ 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);
8706
+ -webkit-mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
8707
+ mask: var(--oc-rating-star) left center / var(--oc-rating-size) var(--oc-rating-size) repeat-x;
8708
+ pointer-events: none;
8709
+ }
8710
+ .oc-rating-input > legend { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
8711
+ .oc-rating-input > label { position: relative; z-index: 1; flex: 0 0 var(--oc-rating-size); cursor: pointer; }
8712
+ .oc-rating-input > label > input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
8713
+ .oc-rating-input > label > span { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
8714
+
8715
+ .oc-rating-input:has(label:nth-of-type(1) input:checked) { --oc-rating-pick: 1; }
8716
+ .oc-rating-input:has(label:nth-of-type(2) input:checked) { --oc-rating-pick: 2; }
8717
+ .oc-rating-input:has(label:nth-of-type(3) input:checked) { --oc-rating-pick: 3; }
8718
+ .oc-rating-input:has(label:nth-of-type(4) input:checked) { --oc-rating-pick: 4; }
8719
+ .oc-rating-input:has(label:nth-of-type(5) input:checked) { --oc-rating-pick: 5; }
8720
+ /* Hover preview (pointer devices only, so a tap does not leave a preview behind). */
8721
+ @media (hover: hover) {
8722
+ .oc-rating-input:has(label:nth-of-type(1):hover) { --oc-rating-hover: 1; }
8723
+ .oc-rating-input:has(label:nth-of-type(2):hover) { --oc-rating-hover: 2; }
8724
+ .oc-rating-input:has(label:nth-of-type(3):hover) { --oc-rating-hover: 3; }
8725
+ .oc-rating-input:has(label:nth-of-type(4):hover) { --oc-rating-hover: 4; }
8726
+ .oc-rating-input:has(label:nth-of-type(5):hover) { --oc-rating-hover: 5; }
8727
+ }
8728
+ .oc-rating-input:has(input:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
8729
+ .oc-rating-input:has(input:disabled) { opacity: 0.5; }
8730
+ .oc-rating-input:has(input:disabled) > label,
8731
+ .oc-rating-input:has(input:disabled) > label > input { cursor: not-allowed; }
8732
+
8733
+ @media (forced-colors: active) {
8734
+ .oc-rating, .oc-rating-input::before { --oc-rating-color: CanvasText; --oc-rating-empty: GrayText; forced-color-adjust: none; }
8735
+ }
8736
+
7892
8737
  /* parts/forms-time-input.css */
7893
8738
  /* File: packages/ui/src/parts/forms-time-input.css
7894
8739
  Map: 24h text field (HH:mm) -> invalid state -> slot list (radio chips) -> slot states -> --sm
@@ -8063,7 +8908,7 @@ button.oc-language-select:hover { background: var(--oc-surface); }
8063
8908
  /* File: packages/ui/src/parts/media-attachment.css | Map: list -> file chip (icon by type, name, size, open/remove) -> type tones
8064
8909
  -> uploading progress + error -> image preview (in a message) -> thumbnail chip (in a composer) -> reduced motion
8065
8910
  Attachment: a file chip or an image preview inside a chat message or a composer. The icon tile shows the extension and is
8066
- tinted per type (--pdf, --doc, --sheet, --zip, --image). Based on the attachment picker of the Korrekt question chat. */
8911
+ tinted per type (--pdf, --doc, --sheet, --zip, --image). */
8067
8912
 
8068
8913
  .oc-attachment-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; padding: 0; list-style: none; }
8069
8914
  .oc-attachment-list--stack { flex-direction: column; flex-wrap: nowrap; }
@@ -8221,6 +9066,31 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
8221
9066
  .oc-attachment, .oc-attachment--uploading::after { transition: none; }
8222
9067
  }
8223
9068
 
9069
+ /* parts/media-barcode.css */
9070
+ /* File: packages/ui/src/parts/media-barcode.css | Map: barcode figure -> svg bars -> digits -> sizes (hero, compact) -> error */
9071
+ /* Barcode display: EAN-13, EAN-8, UPC-A or Code 128, drawn as SVG by
9072
+ packages/ui/src/barcode.js from data-value. Always black bars on white, in both themes, so scanners read it. */
9073
+
9074
+ .oc-barcode {
9075
+ display: inline-flex;
9076
+ flex-direction: column;
9077
+ align-items: center;
9078
+ gap: 0.25rem;
9079
+ margin: 0;
9080
+ padding: 0.75rem 1rem;
9081
+ color: var(--color-black-50);
9082
+ background: var(--color-white-50);
9083
+ border: 1px solid var(--oc-border);
9084
+ border-radius: var(--oc-radius);
9085
+ }
9086
+ .oc-barcode svg { display: block; inline-size: auto; block-size: var(--oc-barcode-height, 4rem); max-inline-size: 100%; }
9087
+ .oc-barcode__digits { font-family: var(--oc-font-mono); font-size: 0.8125rem; letter-spacing: 0.18em; color: inherit; }
9088
+ .oc-barcode--hero { padding: 1.25rem 1.5rem; --oc-barcode-height: 6rem; }
9089
+ .oc-barcode--hero .oc-barcode__digits { font-size: 1rem; }
9090
+ .oc-barcode--compact { padding: 0.375rem 0.5rem; --oc-barcode-height: 2.25rem; }
9091
+ .oc-barcode--compact .oc-barcode__digits { font-size: 0.6875rem; letter-spacing: 0.1em; }
9092
+ .oc-barcode[data-error] .oc-barcode__digits { color: var(--color-red-70); letter-spacing: 0; }
9093
+
8224
9094
  /* parts/media-browser-frame.css */
8225
9095
  /* File: packages/ui/src/parts/media-browser-frame.css | Map: frame -> chrome bar (dots, address pill, actions)
8226
9096
  -> view (screenshot slot, ratio) -> caption -> minimal / dark / tilt modifiers -> motion
@@ -8440,6 +9310,9 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
8440
9310
  line-height: 1;
8441
9311
  cursor: pointer;
8442
9312
  }
9313
+ /* Webicons in the buttons (inline SVG or <iconify-icon>): a fixed size, so nothing is cut off at the round edge. */
9314
+ .oc-chat-composer__tool > :is(svg, iconify-icon) { inline-size: 1.25rem; block-size: 1.25rem; font-size: 1.25rem; }
9315
+ .oc-chat-composer__send > :is(svg, iconify-icon) { inline-size: 1.125rem; block-size: 1.125rem; font-size: 1.125rem; }
8443
9316
  .oc-chat-composer__tool:hover { background: var(--oc-surface-strong); color: var(--oc-foreground); }
8444
9317
  .oc-chat-composer__tool:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
8445
9318
 
@@ -8644,9 +9517,11 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
8644
9517
  background: transparent;
8645
9518
  font-size: 1.15rem;
8646
9519
  line-height: 1;
9520
+ color: var(--oc-muted-foreground);
8647
9521
  cursor: pointer;
8648
9522
  }
8649
- .oc-emoji-picker__trigger:hover { background: var(--oc-surface-strong); }
9523
+ .oc-emoji-picker__trigger > :is(svg, iconify-icon) { inline-size: 1.25rem; block-size: 1.25rem; font-size: 1.25rem; }
9524
+ .oc-emoji-picker__trigger:hover { color: var(--oc-foreground); background: var(--oc-surface-strong); }
8650
9525
  .oc-emoji-picker__trigger:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
8651
9526
 
8652
9527
  /* The panel: floats above the trigger (implicit anchor of popovertarget), flips when there is no room. */
@@ -8850,6 +9725,216 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
8850
9725
  .oc-flip-card--hover:is(:hover, :focus-within) .oc-flip-card__face--back { opacity: 1; visibility: visible; }
8851
9726
  }
8852
9727
 
9728
+ /* parts/media-gallery.css */
9729
+ /* 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) */
9730
+ /* Product gallery without JavaScript: one large photo and a row of thumbnails. Every thumbnail is a label with a
9731
+ radio; :has() shows the slide at the same position. Arrow keys move between the photos (native radios). */
9732
+
9733
+ .oc-gallery { container-type: inline-size; inline-size: 100%; display: grid; gap: 0.75rem; min-inline-size: 0; }
9734
+ .oc-gallery__stage {
9735
+ position: relative;
9736
+ aspect-ratio: var(--oc-gallery-ratio, 4 / 3);
9737
+ overflow: hidden;
9738
+ background: var(--oc-muted);
9739
+ border: 1px solid var(--oc-border);
9740
+ border-radius: calc(var(--oc-radius) + 4px);
9741
+ }
9742
+ .oc-gallery__slide { display: none; margin: 0; block-size: 100%; }
9743
+ .oc-gallery__slide > img { display: block; inline-size: 100%; block-size: 100%; object-fit: var(--oc-gallery-fit, contain); }
9744
+ .oc-gallery:not(:has(.oc-gallery__thumb input:checked)) .oc-gallery__slide:first-of-type,
9745
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(1) input:checked) .oc-gallery__slide:nth-of-type(1),
9746
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(2) input:checked) .oc-gallery__slide:nth-of-type(2),
9747
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(3) input:checked) .oc-gallery__slide:nth-of-type(3),
9748
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(4) input:checked) .oc-gallery__slide:nth-of-type(4),
9749
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(5) input:checked) .oc-gallery__slide:nth-of-type(5),
9750
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(6) input:checked) .oc-gallery__slide:nth-of-type(6),
9751
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(7) input:checked) .oc-gallery__slide:nth-of-type(7),
9752
+ .oc-gallery:has(.oc-gallery__thumb:nth-of-type(8) input:checked) .oc-gallery__slide:nth-of-type(8) { display: block; }
9753
+ .oc-gallery__badge { position: absolute; inset: 0.75rem auto auto 0.75rem; z-index: 1; }
9754
+
9755
+ .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; }
9756
+ .oc-gallery__thumbs > legend { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
9757
+ .oc-gallery__thumb {
9758
+ position: relative;
9759
+ flex: 0 0 auto;
9760
+ inline-size: var(--oc-gallery-thumb, 4rem);
9761
+ aspect-ratio: 1;
9762
+ overflow: hidden;
9763
+ background: var(--oc-muted);
9764
+ border: 2px solid transparent;
9765
+ border-radius: var(--oc-radius);
9766
+ outline: 1px solid var(--oc-border);
9767
+ outline-offset: -1px;
9768
+ cursor: pointer;
9769
+ }
9770
+ .oc-gallery__thumb > img { display: block; inline-size: 100%; block-size: 100%; object-fit: cover; }
9771
+ .oc-gallery__thumb > input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
9772
+ .oc-gallery__thumb:hover { outline-color: var(--oc-muted-foreground); }
9773
+ .oc-gallery__thumb:has(input:checked) { border-color: var(--oc-primary); outline-color: transparent; }
9774
+ .oc-gallery__thumb:has(input:focus-visible) { box-shadow: 0 0 0 2px var(--oc-background), 0 0 0 4px var(--oc-ring); }
9775
+
9776
+ /* Side: thumbnails in a column on the left once the gallery itself is 32rem wide or more. The grid is always two
9777
+ tracks; below 32rem both children span them, so the query only moves the children (a container cannot query itself). */
9778
+ .oc-gallery--side { grid-template-columns: auto minmax(0, 1fr); }
9779
+ .oc-gallery--side > * { grid-column: 1 / -1; }
9780
+ @container (min-width: 32rem) {
9781
+ .oc-gallery--side > .oc-gallery__stage { grid-column: 2; grid-row: 1; }
9782
+ .oc-gallery--side > .oc-gallery__thumbs { grid-column: 1; grid-row: 1; flex-direction: column; overflow: visible; }
9783
+ }
9784
+
9785
+ /* parts/media-glass-liquid.css */
9786
+ /* File: packages/ui/src/parts/media-glass-liquid.css
9787
+ Map: liquid sheet (.oc-glass--liquid: clear tint, strong blur + saturation, inner light, specular rim) -> menu bar
9788
+ (.oc-glass-bar: items with icon and label, the current one in a raised bubble) -> icon-only and round button -> fallbacks
9789
+ Liquid glass in the style of recent desktop and phone systems, in CSS only: the backdrop is blurred and brightened,
9790
+ the edge catches light. Put it over a photo or colourful content; on a plain page it looks like a soft card. */
9791
+
9792
+ .oc-glass.oc-glass--liquid,
9793
+ .oc-glass--liquid,
9794
+ .oc-glass-bar,
9795
+ .oc-site-header--glass .oc-site-header__inner {
9796
+ --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.1), oklch(100% 0 0deg / 0.05));
9797
+ --oc-liquid-edge: light-dark(oklch(100% 0 0deg / 0.55), oklch(100% 0 0deg / 0.22));
9798
+ --oc-liquid-blur: 5px;
9799
+ position: relative;
9800
+ isolation: isolate;
9801
+ background: var(--oc-liquid-tint);
9802
+ border: 1px solid var(--oc-liquid-edge);
9803
+ /* Light enters at the top left and leaves at the bottom right; a soft glow runs along the inner edge,
9804
+ so the sheet reads as a thick, clear lens rather than frosted plastic. */
9805
+ box-shadow:
9806
+ 0 10px 30px -10px oklch(0% 0 0deg / 0.35),
9807
+ inset 2px 2px 1px -2px oklch(100% 0 0deg / 0.95),
9808
+ inset -2px -2px 1px -2px oklch(100% 0 0deg / 0.7),
9809
+ inset 0 0 10px 1px oklch(100% 0 0deg / 0.22),
9810
+ inset 0 -8px 16px -12px oklch(100% 0 0deg / 0.35);
9811
+ backdrop-filter: blur(var(--oc-liquid-blur)) saturate(1.9) brightness(1.1) contrast(1.05);
9812
+ }
9813
+ /* Specular rim: bright on the top-left and bottom-right edge, only on the border ring. */
9814
+ .oc-glass--liquid::before,
9815
+ .oc-glass-bar::before,
9816
+ .oc-site-header--glass .oc-site-header__inner::before {
9817
+ content: "";
9818
+ position: absolute;
9819
+ inset: -1px;
9820
+ z-index: 1;
9821
+ padding: 1.5px;
9822
+ border-radius: inherit;
9823
+ 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));
9824
+ mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
9825
+ mask-composite: exclude;
9826
+ pointer-events: none;
9827
+ }
9828
+
9829
+ /* Menu bar: a pill of items, each an icon over a short label. */
9830
+ .oc-glass-bar {
9831
+ display: inline-flex;
9832
+ align-items: center;
9833
+ gap: 0.2rem;
9834
+ padding: 0.35rem;
9835
+ border-radius: 999px;
9836
+ }
9837
+ .oc-glass-bar__item {
9838
+ position: relative;
9839
+ display: grid;
9840
+ justify-items: center;
9841
+ gap: 0.15rem;
9842
+ min-inline-size: 4.25rem;
9843
+ padding: 0.45rem 0.85rem;
9844
+ border: 0;
9845
+ border-radius: 999px;
9846
+ color: light-dark(oklch(22% 0.02 260deg / 0.78), oklch(100% 0 0deg / 0.82));
9847
+ background: none;
9848
+ font: inherit;
9849
+ font-size: 0.7rem;
9850
+ font-weight: 600;
9851
+ line-height: 1.1;
9852
+ text-decoration: none;
9853
+ cursor: pointer;
9854
+ transition: background-color 0.25s, color 0.25s, box-shadow 0.25s, scale 0.2s;
9855
+ }
9856
+ .oc-glass-bar__item > :is(svg, iconify-icon) { inline-size: 1.4rem; block-size: 1.4rem; font-size: 1.4rem; }
9857
+ .oc-glass-bar__item:hover { background: light-dark(oklch(100% 0 0deg / 0.28), oklch(100% 0 0deg / 0.08)); }
9858
+ .oc-glass-bar__item:active { scale: 0.94; }
9859
+ .oc-glass-bar__item:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
9860
+ /* The current item sits in a raised, lighter bubble. */
9861
+ .oc-glass-bar__item:is([aria-current="page"], [aria-pressed="true"], :has(> input:checked)) {
9862
+ color: light-dark(var(--oc-primary), var(--color-white-50));
9863
+ background: light-dark(oklch(100% 0 0deg / 0.62), oklch(100% 0 0deg / 0.24));
9864
+ box-shadow: inset 0 1px 1px oklch(100% 0 0deg / 0.85), 0 6px 14px -8px oklch(0% 0 0deg / 0.4);
9865
+ }
9866
+ .oc-glass-bar__item > input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
9867
+ .oc-glass-bar__item:has(> input:focus-visible) { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
9868
+
9869
+ /* Icons only, and a separate round button next to the bar (search, compose). */
9870
+ .oc-glass-bar--icons .oc-glass-bar__item { min-inline-size: 0; padding: 0.6rem; }
9871
+ .oc-glass-bar--round { padding: 0.35rem; border-radius: 999px; }
9872
+ .oc-glass-bar--round .oc-glass-bar__item { min-inline-size: 0; padding: 0.6rem; }
9873
+ .oc-glass-bar__sep { align-self: stretch; inline-size: 1px; margin: 0.4rem 0.15rem; background: var(--oc-liquid-edge); }
9874
+
9875
+ /* Less transparency asked for, or no backdrop-filter: a solid bar that still reads as one. */
9876
+ @media (prefers-reduced-transparency: reduce) {
9877
+ .oc-glass--liquid, .oc-glass-bar { --oc-liquid-tint: var(--oc-card); backdrop-filter: none; }
9878
+ }
9879
+ @supports not (backdrop-filter: blur(1px)) {
9880
+ .oc-glass--liquid, .oc-glass-bar { --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.85), oklch(25% 0.02 260deg / 0.9)); }
9881
+ }
9882
+ @media (prefers-reduced-motion: reduce) {
9883
+ .oc-glass-bar__item { transition: none; }
9884
+ }
9885
+
9886
+ /* parts/media-glass-options.css */
9887
+ /* File: packages/ui/src/parts/media-glass-options.css
9888
+ Map: amount (--clear: more see-through, --frost: more frosted) -> light content on a colourful backdrop -> stage (moving colour blobs to show it off)
9889
+ Extras for .oc-glass, .oc-glass--liquid and .oc-glass-bar (media-glass.css, media-glass-liquid.css). The selectors
9890
+ carry two classes so they win over the base glass whatever order the parts load in. */
9891
+
9892
+ /* Amount of glass: --clear lets more of the backdrop through, --frost hides more of it. */
9893
+ :is(.oc-glass, .oc-glass-bar).oc-glass--clear {
9894
+ --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));
9895
+ --oc-glass-blur: 10px;
9896
+ --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.05), oklch(100% 0 0deg / 0.03));
9897
+ --oc-liquid-blur: 2px;
9898
+ }
9899
+ :is(.oc-glass, .oc-glass-bar).oc-glass--frost {
9900
+ --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));
9901
+ --oc-glass-blur: 40px;
9902
+ --oc-liquid-tint: light-dark(oklch(100% 0 0deg / 0.38), oklch(100% 0 0deg / 0.12));
9903
+ --oc-liquid-blur: 18px;
9904
+ }
9905
+
9906
+
9907
+ /* Light content: white icons and labels for glass over a dark or very colourful backdrop. */
9908
+ .oc-glass-bar.oc-glass-bar--light .oc-glass-bar__item {
9909
+ color: oklch(100% 0 0deg / 0.92);
9910
+ text-shadow: 0 1px 2px oklch(0% 0 0deg / 0.25);
9911
+ }
9912
+ .oc-glass-bar.oc-glass-bar--light .oc-glass-bar__item:is([aria-current="page"], [aria-pressed="true"], :has(> input:checked)) {
9913
+ color: var(--color-white-50);
9914
+ background: oklch(100% 0 0deg / 0.2);
9915
+ }
9916
+ .oc-glass-bar.oc-glass-bar--light .oc-glass-bar__item:hover { background: oklch(100% 0 0deg / 0.12); }
9917
+
9918
+ /* Stage: round colour shapes that drift slowly, so the change through the glass shows. A background for a hero or a demo. */
9919
+ .oc-glass-stage {
9920
+ position: relative;
9921
+ overflow: hidden;
9922
+ background:
9923
+ radial-gradient(circle at 22% 72%, var(--color-magenta-50) 0 16%, transparent 16.5%),
9924
+ radial-gradient(circle at 82% 68%, var(--color-orange-50) 0 18%, transparent 18.5%),
9925
+ radial-gradient(circle at 58% 18%, var(--color-skycyan-50) 0 13%, transparent 13.5%),
9926
+ radial-gradient(60% 70% at 30% 30%, var(--color-magenta-60), transparent 70%),
9927
+ linear-gradient(120deg, var(--color-purple-60), var(--color-violet-80));
9928
+ background-size: 140% 140%;
9929
+ animation: oc-glass-stage 18s ease-in-out infinite alternate;
9930
+ }
9931
+ @keyframes oc-glass-stage {
9932
+ to { background-position: 100% 60%; }
9933
+ }
9934
+ @media (prefers-reduced-motion: reduce) {
9935
+ .oc-glass-stage { animation: none; }
9936
+ }
9937
+
8853
9938
  /* parts/media-glass.css */
8854
9939
  /* File: packages/ui/src/parts/media-glass.css
8855
9940
  Map: tokens (light-dark) -> sheet (.oc-glass: see-through, blurred, light top edge, soft shadow) -> panel (.oc-glass-panel:
@@ -8993,6 +10078,54 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
8993
10078
  .oc-image-zoom__canvas { transition: inline-size var(--oc-transition-normal); }
8994
10079
  }
8995
10080
 
10081
+ /* parts/media-lazy.css */
10082
+ /* 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
10083
+ Lazy loading. .oc-lazy: an image box that keeps its place, shows a shimmer (or --oc-lazy-preview, a tiny blurred
10084
+ copy) until the photo is there and then fades it in (lazy.js adds .is-loaded). .oc-lazy-section: content the browser
10085
+ only lays out and draws when it comes near the screen (content-visibility). */
10086
+
10087
+ .oc-lazy {
10088
+ --oc-lazy-ratio: 16 / 10;
10089
+ position: relative;
10090
+ display: block;
10091
+ margin: 0;
10092
+ overflow: hidden;
10093
+ aspect-ratio: var(--oc-lazy-ratio);
10094
+ border-radius: var(--oc-radius);
10095
+ background:
10096
+ linear-gradient(100deg, transparent 30%, oklch(from var(--oc-background) l c h / 0.55) 50%, transparent 70%) -150% 0 / 60% 100% no-repeat,
10097
+ var(--oc-muted);
10098
+ animation: oc-lazy-shimmer 1.4s ease-in-out infinite;
10099
+ }
10100
+ /* A tiny blurred preview instead of the shimmer: style="--oc-lazy-preview: url(photo-20px.webp)". */
10101
+ .oc-lazy[style*="--oc-lazy-preview"] { animation: none; background: var(--oc-muted); }
10102
+ .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; }
10103
+ .oc-lazy > img { position: relative; display: block; inline-size: 100%; block-size: 100%; object-fit: cover; opacity: 0; transition: opacity 0.45s ease; }
10104
+ .oc-lazy.is-loaded { animation: none; background: none; }
10105
+ .oc-lazy.is-loaded::before { display: none; }
10106
+ .oc-lazy.is-loaded > img { opacity: 1; }
10107
+ .oc-lazy.is-error { animation: none; }
10108
+ .oc-lazy.is-error > img { opacity: 0; }
10109
+ .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)); }
10110
+ /* Without the script (or when it is off) the photo shows as soon as it is there. */
10111
+ @media (scripting: none) { .oc-lazy > img { opacity: 1; } }
10112
+ @keyframes oc-lazy-shimmer { to { background-position: 250% 0, 0 0; } }
10113
+
10114
+ /* Sections far down a long page: laid out and drawn only when they come near the screen. */
10115
+ .oc-lazy-section { content-visibility: auto; contain-intrinsic-size: auto var(--oc-lazy-size, 600px); }
10116
+
10117
+ @media (prefers-reduced-motion: reduce) {
10118
+ .oc-lazy { animation: none; }
10119
+ .oc-lazy > img { transition: none; }
10120
+ }
10121
+
10122
+ /* In a table or a list: a small square thumbnail, and placeholder rows (.oc-lazy-row) while the next page loads. */
10123
+ .oc-lazy.oc-lazy--thumb { --oc-lazy-ratio: 1; inline-size: 2.5rem; flex: none; border-radius: calc(var(--oc-radius) - 2px); }
10124
+ .oc-lazy-row > :is(td, th) { padding-block: 0.6rem; }
10125
+ .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; }
10126
+ @keyframes oc-lazy-pulse { to { opacity: 0.45; } }
10127
+ @media (prefers-reduced-motion: reduce) { .oc-lazy-row .oc-lazy-bar { animation: none; } }
10128
+
8996
10129
  /* parts/media-lightbox.css */
8997
10130
  /* File: packages/ui/src/parts/media-lightbox.css | Map: thumbnails -> dialog + backdrop -> bar -> track + slides
8998
10131
  -> nav buttons -> caption + counter -> single image -> small screens -> motion
@@ -9171,7 +10304,7 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
9171
10304
  people, status, actions) -> wallpaper (masked doodles in a token color) -> scroll area (starts at the bottom)
9172
10305
  -> composer footer -> sending overlay (aria-busy) -> reduced motion
9173
10306
  Message thread: one conversation. The scroll area uses flex-direction: column-reverse, so it opens at the newest message
9174
- and stays there when messages are added, without a script. Based on the question chat of the Korrekt app. */
10307
+ and stays there when messages are added, without a script. */
9175
10308
 
9176
10309
  .oc-message-thread {
9177
10310
  --oc-message-thread-height: 32rem;
@@ -9309,7 +10442,7 @@ a.oc-attachment-image:hover > :is(img, .oc-media-art) { filter: brightness(1.05)
9309
10442
  -> grouping (:has) -> hover actions -> date divider + system line -> reduced motion
9310
10443
  Message: one chat bubble. Incoming on the left with the avatar, outgoing on the right in the primary tint.
9311
10444
  Consecutive messages of one sender group by themselves: the avatar shows once (on the last one), the name once (on the
9312
- first one). Based on the question chat of the Korrekt app. */
10445
+ first one). */
9313
10446
 
9314
10447
  .oc-message-list {
9315
10448
  --oc-message-in: var(--oc-card);
@@ -9879,6 +11012,104 @@ a.oc-message__quote:focus-visible { outline: 2px solid var(--oc-ring); outline-o
9879
11012
  .oc-watermark { --oc-watermark-opacity: 0.2; print-color-adjust: exact; }
9880
11013
  }
9881
11014
 
11015
+ /* parts/nav-topbar-variants.css */
11016
+ /* File: packages/ui/src/parts/nav-topbar-variants.css
11017
+ Map: brand (logo tile, room before the menu) -> centred menu -> plain links -> dark / brand colour -> floating pill
11018
+ -> strip above the bar -> footer bar (split, links, dark)
11019
+ Variants of the site header (.oc-topbar in docs-ui.css) and the footer bar (.oc-footerbar). Colours are tokens. */
11020
+
11021
+ /* Brand: a logo tile and the name, always with room before the menu. */
11022
+ .oc-topbar-brand { display: inline-flex; align-items: center; gap: 0.55rem; margin-inline-end: 1rem; }
11023
+ .oc-topbar-logo {
11024
+ display: grid;
11025
+ place-items: center;
11026
+ flex: none;
11027
+ inline-size: 1.85rem;
11028
+ block-size: 1.85rem;
11029
+ border-radius: 9px;
11030
+ color: var(--color-white-50);
11031
+ background: linear-gradient(135deg, var(--oc-accent-from, var(--oc-primary)), var(--oc-accent-to, var(--oc-primary)));
11032
+ font-size: 0.95rem;
11033
+ }
11034
+ .oc-topbar-logo > :is(svg, iconify-icon) { inline-size: 1.1rem; block-size: 1.1rem; font-size: 1.1rem; }
11035
+ .oc-topbar-nav { min-inline-size: 0; }
11036
+ .oc-topbar-actions .oc-btn { white-space: nowrap; }
11037
+
11038
+ /* Centred menu: brand left, menu in the middle, actions right (from 761px; smaller screens keep the burger). */
11039
+ @media (min-width: 761px) {
11040
+ .oc-topbar--center { display: grid; grid-template-columns: 1fr auto 1fr; }
11041
+ .oc-topbar--center .oc-topbar-brand { justify-self: start; }
11042
+ .oc-topbar--center .oc-topbar-actions { justify-self: end; }
11043
+ }
11044
+
11045
+ /* Plain links: no pill frames, the current page underlined. */
11046
+ .oc-topbar--plain .oc-navmenu--site .oc-navmenu-link {
11047
+ border-color: transparent;
11048
+ border-radius: 0;
11049
+ background: none;
11050
+ color: var(--oc-muted-foreground);
11051
+ box-shadow: inset 0 -2px 0 transparent;
11052
+ }
11053
+ .oc-topbar--plain .oc-navmenu--site .oc-navmenu-link:hover { color: var(--oc-foreground); border-color: transparent; background: none; }
11054
+ .oc-topbar--plain .oc-navmenu--site .oc-navmenu-link[aria-current="page"] {
11055
+ color: var(--oc-foreground);
11056
+ border-color: transparent;
11057
+ background: none;
11058
+ box-shadow: inset 0 -2px 0 var(--oc-primary);
11059
+ }
11060
+
11061
+ /* Dark bar and brand-colour bar: light text, links as soft pills. */
11062
+ .oc-topbar:is(.oc-topbar--dark, .oc-topbar--brand) { color: var(--color-white-50); border-block-end-color: transparent; }
11063
+ .oc-topbar.oc-topbar--dark { background: var(--oc-dark-band, var(--color-duskblue-95)); }
11064
+ .oc-topbar--brand { background: linear-gradient(100deg, var(--oc-accent-from, var(--oc-primary)), var(--oc-accent-to, var(--oc-primary))); }
11065
+ .oc-topbar:is(.oc-topbar--dark, .oc-topbar--brand) .oc-navmenu--site .oc-navmenu-link {
11066
+ color: oklch(from var(--color-white-50) l c h / 0.8);
11067
+ border-color: transparent;
11068
+ background: none;
11069
+ }
11070
+ .oc-topbar:is(.oc-topbar--dark, .oc-topbar--brand) .oc-navmenu--site .oc-navmenu-link:is(:hover, [aria-current="page"]) {
11071
+ color: var(--color-white-50);
11072
+ border-color: transparent;
11073
+ background: oklch(from var(--color-white-50) l c h / 0.14);
11074
+ }
11075
+ .oc-topbar--dark .oc-topbar-logo { background: oklch(from var(--color-white-50) l c h / 0.14); }
11076
+ .oc-topbar--brand .oc-topbar-logo { color: var(--oc-primary); background: var(--color-white-50); }
11077
+ .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); }
11078
+
11079
+ /* Floating: a rounded bar with room around it and a soft shadow (over a hero or a photo). */
11080
+ .oc-topbar--floating {
11081
+ margin: 0.75rem;
11082
+ border: 1px solid var(--oc-border);
11083
+ border-radius: 16px;
11084
+ box-shadow: 0 14px 34px -18px oklch(from var(--color-black-60) l c h / 0.35);
11085
+ }
11086
+
11087
+ /* Strip: a small row above the bar (contact, language, account). */
11088
+ .oc-topbar-strip {
11089
+ display: flex;
11090
+ flex-wrap: wrap;
11091
+ align-items: center;
11092
+ justify-content: space-between;
11093
+ gap: 0.25rem 1rem;
11094
+ padding: 0.35rem clamp(14px, 2vw, 28px);
11095
+ font-size: 0.78rem;
11096
+ color: var(--oc-muted-foreground);
11097
+ background: var(--oc-muted);
11098
+ }
11099
+ .oc-topbar-strip a { color: inherit; text-decoration: none; }
11100
+ .oc-topbar-strip a:hover { color: var(--oc-foreground); }
11101
+ .oc-topbar-strip > span { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 1rem; }
11102
+
11103
+ /* Footer bar: text left and links right (--split), a link list, a dark bar. */
11104
+ .oc-footerbar--split { justify-content: space-between; gap: 1rem; text-align: start; }
11105
+ .oc-footerbar--tall { block-size: auto; min-block-size: var(--ocm-footer-height, 2.25rem); padding-block: 0.85rem; flex-wrap: wrap; }
11106
+ .oc-footerbar-links { display: inline-flex; flex-wrap: wrap; gap: 0.25rem 1rem; margin: 0; padding: 0; list-style: none; }
11107
+ .oc-footerbar-links a { color: var(--oc-muted-foreground); text-decoration: none; }
11108
+ .oc-footerbar-links a:hover { color: var(--oc-foreground); }
11109
+ .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; }
11110
+ .oc-footerbar--dark :is(a, .oc-footerbar-links a) { color: oklch(from var(--color-white-50) l c h / 0.85); }
11111
+ .oc-footerbar--dark .oc-footerbar-links a:hover { color: var(--color-white-50); }
11112
+
9882
11113
  /* parts/site-fact-list.css */
9883
11114
  /* File: packages/ui/src/parts/site-fact-list.css
9884
11115
  Map: plain -> compact -> stacked -> accent values -> narrow screens
@@ -9906,6 +11137,78 @@ a.oc-message__quote:focus-visible { outline: 2px solid var(--oc-ring); outline-o
9906
11137
  /* Very narrow containers: wrap instead of squeezing label and value together. */
9907
11138
  .oc-fact-list > div { flex-wrap: wrap; }
9908
11139
 
11140
+ /* parts/site-header-variants.css */
11141
+ /* File: packages/ui/src/parts/site-header-variants.css
11142
+ Map: actions slot -> centred menu -> dark -> floating pill -> glass pill -> footer: centred, dark, newsletter, social
11143
+ Variants of the marketing header (.oc-site-header) and footer (.oc-site-footer) in showcase.css. The sticky header
11144
+ turns to glass while scrolling; the coloured variants keep their own background (no scroll animation). */
11145
+
11146
+ .oc-site-header__actions { display: flex; align-items: center; gap: 0.5rem; flex: none; }
11147
+ .oc-site-header__brand { white-space: nowrap; flex: none; }
11148
+ .oc-site-header__nav > :is(a, button):not(.oc-btn) { white-space: nowrap; }
11149
+
11150
+ /* Centred menu: brand left, menu in the middle, actions right. */
11151
+ @media (min-width: 861px) {
11152
+ .oc-site-header--center .oc-site-header__inner { display: grid; grid-template-columns: 1fr auto 1fr; }
11153
+ .oc-site-header--center .oc-site-header__brand { justify-self: start; }
11154
+ .oc-site-header--center .oc-site-header__actions { justify-self: end; }
11155
+ }
11156
+
11157
+ /* Dark: a solid dark bar with light text. */
11158
+ .oc-site-header--dark {
11159
+ animation: none;
11160
+ color: var(--color-white-50);
11161
+ background: var(--oc-dark-band, var(--color-duskblue-95));
11162
+ }
11163
+ .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); }
11164
+ .oc-site-header--dark .oc-site-header__nav a:not(.oc-btn) { color: oklch(from var(--color-white-50) l c h / 0.72); }
11165
+
11166
+ /* Floating: the bar as a rounded card with room around it. */
11167
+ .oc-site-header:is(.oc-site-header--floating, .oc-site-header--glass) { animation: none; padding: 0.75rem; }
11168
+ .oc-site-header--floating .oc-site-header__inner {
11169
+ block-size: 4rem;
11170
+ padding-inline: 1rem 0.75rem;
11171
+ border: 1px solid var(--oc-border);
11172
+ border-radius: 18px;
11173
+ background: var(--oc-background);
11174
+ box-shadow: 0 16px 36px -22px oklch(from var(--color-black-60) l c h / 0.45);
11175
+ }
11176
+
11177
+ /* Glass: a clear liquid-glass pill over a photo or a hero (look in media-glass-liquid.css). */
11178
+ .oc-site-header--glass .oc-site-header__inner { block-size: 3.75rem; padding-inline: 1.1rem 0.6rem; border-radius: 999px; }
11179
+ .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)); }
11180
+
11181
+ /* ---- Footer variants ---- */
11182
+ /* Centred: brand, one row of links, social icons and the bottom line, all centred. */
11183
+ .oc-site-footer--center { text-align: center; }
11184
+ .oc-site-footer--center :is(.oc-site-footer__about, .oc-site-footer__bottom) { justify-items: center; justify-content: center; }
11185
+ .oc-site-footer--center .oc-site-footer__about { display: grid; gap: 1rem; }
11186
+ .oc-site-footer--center .oc-site-footer__about p { margin: 0; max-inline-size: 34rem; }
11187
+ .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; }
11188
+ .oc-site-footer__social { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; padding: 0; list-style: none; }
11189
+ .oc-site-footer--center .oc-site-footer__social { justify-content: center; margin-block-start: 1.5rem; }
11190
+ .oc-site-footer__social a {
11191
+ display: grid;
11192
+ place-items: center;
11193
+ inline-size: 2.25rem;
11194
+ block-size: 2.25rem;
11195
+ border: 1px solid var(--oc-border);
11196
+ border-radius: 999px;
11197
+ transition: border-color 0.2s, color 0.2s;
11198
+ }
11199
+ .oc-site-footer__social a:hover { border-color: var(--oc-primary); color: var(--oc-primary); }
11200
+ .oc-site-footer__social :is(svg, iconify-icon) { inline-size: 1.1rem; block-size: 1.1rem; font-size: 1.1rem; }
11201
+
11202
+ /* Dark: light text on a dark band, the same deep blue in light and dark mode (--oc-dark-band), no top border. */
11203
+ .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)); }
11204
+ .oc-site-footer--dark :is(.oc-site-footer__title, .oc-site-header__brand, a:hover) { color: var(--color-white-50); }
11205
+ .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); }
11206
+
11207
+ /* Newsletter: a sign-up field in the brand column. */
11208
+ .oc-site-footer__signup { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-block-start: 1.25rem; max-inline-size: 24rem; }
11209
+ .oc-site-footer__signup .oc-input { flex: 1 1 12rem; min-inline-size: 0; }
11210
+ .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); }
11211
+
9909
11212
  /* parts/site-link-arrow.css */
9910
11213
  /* File: packages/ui/src/parts/site-link-arrow.css
9911
11214
  Map: focus ring -> back arrow -> external arrow -> color modifiers -> sizes -> reduced motion
@@ -10399,7 +11702,7 @@ a.oc-message__quote:focus-visible { outline: 2px solid var(--oc-ring); outline-o
10399
11702
  -> sizes (--sm, --lg) -> full width
10400
11703
  Prose: typography for long-form text from a CMS, blog or help page, where the HTML has no classes of its own.
10401
11704
  Every rule is scoped with :where(), so a utility class on an element inside still wins. Spacing is in em, so --sm and --lg
10402
- scale the whole text. Based on the help and maintenance pages of the Korrekt app. */
11705
+ scale the whole text. */
10403
11706
 
10404
11707
  .oc-prose {
10405
11708
  max-inline-size: 68ch;