@danieldeusing/design 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,15 +110,17 @@ Four dependency-free ES modules, tree-shakeable from `@danieldeusing/design/runt
110
110
  | `setTheme(name)` / `initThemeSwitcher()` | Switch themes and wire `[data-theme-value]` buttons + `[data-theme-label]`. |
111
111
  | `initTerminal()` | The `$ command` typing animation. No-ops under reduced motion / `html.anim-off`. |
112
112
  | `initDropdowns()` | `<details class="dropdown">` behaviour: one-open, click-away, Escape. |
113
+ | `initSelects()` | Replaces the OS dropdown on every `<select>` with the themed listbox — the option list is painted outside the page, so CSS alone can never reach it. Markup contract: none. The `<select>` keeps the value and still fires `input`/`change`, and selects rendered later are enhanced on their own. |
113
114
  | `initAnimToggle()` | Wire `[data-anim-toggle]` buttons (`.anim-toggle`) to flip `html.anim-off` + persist it. |
114
115
  | `initResolutionZoom(1920)` | Scale the whole layout up on screens wider than the reference width. |
115
116
 
116
117
  ```js
117
- import { applyStoredTheme, initThemeSwitcher, initDropdowns, initTerminal, initAnimToggle, initResolutionZoom } from "@danieldeusing/design/runtime";
118
+ import { applyStoredTheme, initThemeSwitcher, initDropdowns, initSelects, initTerminal, initAnimToggle, initResolutionZoom } from "@danieldeusing/design/runtime";
118
119
  applyStoredTheme(); // ideally inline, pre-paint
119
120
  initResolutionZoom(); // ideally inline, pre-paint (no flash)
120
121
  initThemeSwitcher();
121
122
  initDropdowns();
123
+ initSelects();
122
124
  initTerminal();
123
125
  initAnimToggle();
124
126
  ```
@@ -1,4 +1,4 @@
1
- /*! danieldeusing-design v0.19.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
1
+ /*! danieldeusing-design v0.21.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
2
2
  /* ===== reset.css ===== */
3
3
  /*
4
4
  * danieldeusing-design — minimal reset.
@@ -798,6 +798,207 @@ html[data-theme="warm"] .dropdown-item[data-theme-value="warm"] {
798
798
  text-shadow: 0 0 8px var(--glow);
799
799
  }
800
800
 
801
+ /* ─────────────────────────────────────────────────────────────────────────
802
+ Select — the estate's own dropdown, because a native one's list CANNOT be
803
+ styled. Pairs with runtime/select.js.
804
+
805
+ THE CONSTRAINT THAT DECIDES EVERYTHING HERE: a `<select>`'s option list is
806
+ drawn by the OPERATING SYSTEM, outside the page, and no stylesheet reaches
807
+ it. So every surface in the estate has been rendering a rounded, blue-
808
+ highlighted, system-font menu out of a terminal UI — the one component that
809
+ could not be made to look like the rest, no matter what the page declared.
810
+ `appearance: base-select` would fix it in Chrome 135+ and nowhere else, so
811
+ Safari would keep the system menu: the estate would then be inconsistent
812
+ WITH ITSELF, which is worse than being consistently wrong.
813
+
814
+ So the list is rebuilt in the page. runtime/select.js keeps the real
815
+ `<select>` as the value (it is still what a form submits and what page code
816
+ reads) and paints a button + listbox over it. THE MARKUP CONTRACT IS
817
+ NOTHING: author a plain `<select>`, exactly as with `<table>`.
818
+
819
+ TWO RENDERINGS, ONE DECLARATION. The bare `<select>` (JS off, or before the
820
+ runtime has run) and the `.select-trigger` that replaces it must be the same
821
+ control at the same size — a reader who tabs into a page mid-enhancement
822
+ must not see the box change shape. Splitting their look across two rules is
823
+ how they would drift, so the closed control is declared ONCE for both, by
824
+ ELEMENT and by class together. Five copies of `.cfg-sel` in cockpit are what
825
+ this replaces; none of them agreed about `:disabled`.
826
+
827
+ THE BORDER IS 60% OF --foreground, MEASURED, NOT --border. A control's edge
828
+ is the thing that says "this is a control" and WCAG 1.4.11 wants 3:1 for
829
+ that; `--border` is a CONTAINER hairline and measures 1.37 / 2.00 / 1.61 /
830
+ 1.42 against `--background` on the four themes — invisible as a control
831
+ edge, and correct for the cards it was made for. The estate's hand-rolled
832
+ selects used `currentColor 30%`, which is 1.70 at worst. 60% is the first
833
+ step that clears 3:1 on all four themes against ALL THREE surfaces a control
834
+ can land on (worst: warm 3.24 over --background, 3.24 over --muted). Mixed
835
+ with `transparent` rather than with a surface so it composites correctly
836
+ wherever it is dropped, and derived from a token so it stays per-theme.
837
+
838
+ THE CARET IS TWO GRADIENT HALVES, not an SVG. It has to be `currentColor` —
839
+ a data-URI with a baked fill would need declaring four times or be wrong on
840
+ two themes (the luminance gap in tokens.css), and a `::after` is impossible
841
+ because a `<select>` cannot have generated content. Two abutting squares,
842
+ each half-filled on the diagonal, draw the same ▾ on both renderings.
843
+ ───────────────────────────────────────────────────────────────────────── */
844
+ select,
845
+ .select-trigger {
846
+ appearance: none;
847
+ -webkit-appearance: none;
848
+ max-width: 100%;
849
+ padding: 0.28rem 1.45rem 0.28rem 0.5rem;
850
+ font: inherit;
851
+ font-size: var(--fs-sm);
852
+ line-height: var(--lh-tight);
853
+ text-align: left;
854
+ color: var(--foreground);
855
+ background-color: transparent;
856
+ background-image:
857
+ linear-gradient(45deg, transparent 50%, currentColor 50%),
858
+ linear-gradient(135deg, currentColor 50%, transparent 50%);
859
+ background-position:
860
+ right 0.78rem center,
861
+ right 0.5rem center;
862
+ background-size: 0.28rem 0.28rem;
863
+ background-repeat: no-repeat;
864
+ border: 1px solid color-mix(in srgb, var(--foreground) 60%, transparent);
865
+ cursor: pointer;
866
+ transition: border-color 0.15s ease;
867
+ }
868
+ select:hover,
869
+ .select-trigger:hover,
870
+ .select-trigger[aria-expanded="true"] {
871
+ border-color: var(--primary);
872
+ }
873
+ select:disabled,
874
+ .select-trigger:disabled {
875
+ opacity: 0.45;
876
+ cursor: default;
877
+ }
878
+ /* Open: the caret turns over, so the control says "the list is out" without
879
+ relying on the panel being in view — it may have flipped above the trigger. */
880
+ .select-trigger[aria-expanded="true"] {
881
+ background-image:
882
+ linear-gradient(135deg, transparent 50%, currentColor 50%),
883
+ linear-gradient(45deg, currentColor 50%, transparent 50%);
884
+ }
885
+
886
+ /* The wrapper the runtime puts around the pair. It is `relative` so the real
887
+ `<select>` can be laid exactly over the trigger: NOT `display: none`, and not
888
+ `visibility: hidden` either — Chrome refuses to show a validation bubble on a
889
+ control it cannot focus and then blocks the submit with no message at all, so
890
+ a `required` select would silently stop working. Transparent and over the
891
+ trigger, the bubble still points at the right box. */
892
+ .select-field {
893
+ position: relative;
894
+ display: inline-flex;
895
+ max-width: 100%;
896
+ vertical-align: middle;
897
+ }
898
+ .select-field > select {
899
+ position: absolute;
900
+ inset: 0;
901
+ width: 100%;
902
+ height: 100%;
903
+ opacity: 0;
904
+ pointer-events: none;
905
+ }
906
+ .select-trigger {
907
+ display: inline-flex;
908
+ align-items: center;
909
+ width: 100%;
910
+ }
911
+ .select-value {
912
+ /* min-width: 0 is what lets a flex item shrink below its content — without it
913
+ `text-overflow: ellipsis` never fires and a long option label blows the
914
+ control out of its column instead of being clipped. */
915
+ min-width: 0;
916
+ overflow: hidden;
917
+ text-overflow: ellipsis;
918
+ white-space: nowrap;
919
+ }
920
+ /* An empty flex item has no line box, so a select whose current option has no
921
+ text (`<option value="">`) would render a control HALF THE HEIGHT of the one
922
+ beside it. A zero-width space gives it a line and no width, and screen readers
923
+ ignore it — unlike an `&nbsp;`, which they read out. */
924
+ .select-value::after {
925
+ content: "\200b";
926
+ }
927
+
928
+ /* THE PANEL IS position: fixed AND APPENDED OUT OF THE FLOW, for two reasons
929
+ this estate hits constantly: selects live inside `.tablewrap` (which scrolls,
930
+ so it clips) and inside modal `<dialog>`s (a top layer, which nothing outside
931
+ can paint above). An absolutely-positioned panel would be cut off in the
932
+ first and hidden behind the second. runtime/select.js appends it to the
933
+ nearest open <dialog> when there is one and to <body> otherwise.
934
+ z-index 60 clears the scanline overlay (40), .dropdown-panel (50) and
935
+ footer.status (50). */
936
+ .select-panel {
937
+ position: fixed;
938
+ z-index: 60;
939
+ margin: 0;
940
+ padding: 0.15rem 0;
941
+ overflow-y: auto;
942
+ overscroll-behavior: contain;
943
+ list-style: none;
944
+ font-size: var(--fs-sm);
945
+ line-height: var(--lh-tight);
946
+ color: var(--popover-foreground);
947
+ background: var(--popover);
948
+ border: 1px solid color-mix(in srgb, var(--foreground) 60%, transparent);
949
+ box-shadow: 0 0 20px var(--glow-soft);
950
+ }
951
+
952
+ /* SELECTION CANNOT BE A COLOUR HERE, and that is measured, not felt. `--primary`
953
+ against `--popover-foreground` — the selected option's ink against its
954
+ neighbours' — is 1.65:1 on warm, 1.31 on green, 1.48 on mono, 1.27 on paper.
955
+ Two inks a reader cannot tell apart is not a marking. Same finding as the
956
+ rail's current row (0.19.0), same answer: POSITION AND AREA. The selected
957
+ option is the only one with a left edge marker, and it is bold; the colour is
958
+ the third signal, not the only one.
959
+
960
+ The keyboard's active option and the mouse's hover are the SAME state on
961
+ purpose — there is one pointer of attention, and showing two would ask the
962
+ reader which one Enter is going to take. */
963
+ .select-option {
964
+ display: flex;
965
+ align-items: center;
966
+ gap: 0.4rem;
967
+ padding: 0.28rem 0.9rem 0.28rem 0.55rem;
968
+ border-inline-start: 2px solid transparent;
969
+ white-space: nowrap;
970
+ cursor: pointer;
971
+ }
972
+ .select-option:hover,
973
+ .select-option[data-active="true"] {
974
+ background: var(--muted);
975
+ }
976
+ .select-option[aria-selected="true"] {
977
+ color: var(--primary);
978
+ font-weight: 700;
979
+ border-inline-start-color: var(--primary);
980
+ }
981
+ /* 65% of the ink, not 45%: below that warm lands at 2.94:1 and the option stops
982
+ being readable at all, and "unavailable" still has to be legible to be
983
+ understood as a choice that exists. 65% measures 3.75 at worst (warm). */
984
+ .select-option[aria-disabled="true"] {
985
+ color: color-mix(in srgb, var(--popover-foreground) 65%, var(--popover));
986
+ cursor: default;
987
+ }
988
+ .select-option[aria-disabled="true"]:hover {
989
+ background: none;
990
+ }
991
+ /* <optgroup> label. Nothing in the estate uses one today; it is here because a
992
+ listbox that silently drops the grouping a consumer wrote is worse than one
993
+ that never supported it. */
994
+ .select-group {
995
+ padding: 0.35rem 0.9rem 0.15rem;
996
+ color: var(--muted-foreground);
997
+ font-size: var(--fs-xs);
998
+ text-transform: uppercase;
999
+ letter-spacing: 0.06em;
1000
+ }
1001
+
801
1002
  /* ─────────────────────────────────────────────────────────────────────────
802
1003
  ELI5 — an "explain like I'm 5" callout box (first-time-term explainers).
803
1004
  ───────────────────────────────────────────────────────────────────────── */
@@ -1385,7 +1586,38 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
1385
1586
 
1386
1587
  /* ── ls-listing rows (shared by the desktop rail + mobile menu) ── */
1387
1588
  .ls-row { display: flex; align-items: baseline; gap: 0.9rem; }
1388
- .ls-perm { color: var(--muted-foreground); font-size: 0.72rem; letter-spacing: 0; }
1589
+ /* THE PERMISSION STRING SITS BEHIND THE NAME (0.20.0). Daniel: "the folder info should also
1590
+ be in some grey style color so that it does not have the same color as the path/folder
1591
+ name." It was `--muted-foreground` flat, and measured across the four themes that was worse
1592
+ than it looks in the source:
1593
+
1594
+ contrast between .ls-perm and the NAME beside it, before
1595
+ leaf row: 1.00 1.00 1.00 1.00 ← identical, on EVERY theme
1596
+ dir row: 1.20 2.34 3.45 3.00 (warm / green / mono / paper)
1597
+
1598
+ The leaf case is the severe one and it was invisible in review because the two rules read as
1599
+ different declarations: `.ls-perm` and `.ls-panel .ls-name` are both `--muted-foreground`, so
1600
+ `drwxr-xr-x` and the page name were literally the same ink and the row had no hierarchy at
1601
+ all. Most rows in a rail are leaves. On the DIRECTORY row the warm theme then compounds it —
1602
+ `--muted-foreground` #71614e beside `--primary` #8a4516 is a brown-grey next to a brown, 1.2:1,
1603
+ which the eye reads as one colour even though the hexes differ.
1604
+
1605
+ Mixed toward `--card`, the surface the panel is actually drawn on, rather than swapped for a
1606
+ different token: the point is to push it BACK, and no token in the set means "quieter than
1607
+ muted". 75% is the dimmest mix that still clears 3:1 against the panel on all four themes —
1608
+ warm is the binding one at 3.02 — because this string is not decoration. A trailing slash plus
1609
+ `drwxr-xr-x` is what says the thing has contents, so it has to stay readable, just not compete.
1610
+
1611
+ after: legibility vs panel 3.02 3.91 3.63 3.55
1612
+ vs leaf name 1.60 1.56 1.55 1.75
1613
+ vs dir name 1.92 3.67 5.35 5.24
1614
+
1615
+ Left at plain `.ls-perm` specificity on purpose: the you-are-here rule below tints this same
1616
+ element toward `--primary`, and it must keep winning. */
1617
+ .ls-perm {
1618
+ color: color-mix(in srgb, var(--muted-foreground) 75%, var(--card));
1619
+ font-size: 0.72rem; letter-spacing: 0;
1620
+ }
1389
1621
 
1390
1622
  /* DEPTH IS DRAWN, NOT JUST INDENTED. Whitespace alone did not carry it — with the
1391
1623
  permission column in front of every row, a child sat only ~1rem right of its
@@ -1817,6 +2049,7 @@ span[data-tip], th[data-tip] { border-bottom: 1px dotted var(--muted-foreground)
1817
2049
  .skip-link,
1818
2050
  .dropdown,
1819
2051
  .dropdown-panel,
2052
+ .select-panel,
1820
2053
  .anim-toggle,
1821
2054
  .cursor-block,
1822
2055
  .term-caret,
@@ -1,2 +1,2 @@
1
- /*! danieldeusing-design v0.19.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
2
- *,*::before,*::after{box-sizing: border-box}*{margin: 0}html{-webkit-text-size-adjust: 100%;text-size-adjust: 100%;tab-size: 4}body{min-height: 100vh;min-height: 100dvh;line-height: 1.5}img,picture,video,canvas,svg{display: block;max-width: 100%}input,button,textarea,select{font: inherit;color: inherit}p,h1,h2,h3,h4,h5,h6{overflow-wrap: break-word}ul,ol{list-style: none;padding: 0}a{color: inherit;text-decoration: none}table{border-collapse: collapse}:root{--background: #f5efe2;--foreground: #43352a;--card: #efe7d4;--card-foreground: #43352a;--popover: #efe7d4;--popover-foreground: #43352a;--primary: #8a4516;--primary-foreground: #f8f3e8;--secondary: #ece3cf;--secondary-foreground: #43352a;--muted: #ece3cf;--muted-foreground: #71614e;--accent: #8a4516;--accent-foreground: #f8f3e8;--destructive: #a02c2c;--success: #1a6b2e;--warning: #845600;--info: #1c5f9e;--pending: #5f3dc4;--border: #d9cdb6;--input: #d9cdb6;--ring: #8a4516;--glow: rgba(160,82,45,0.1);--glow-soft: rgba(160,82,45,0.05);--scanline-opacity: 0.15;--radius: 0rem;--font-mono: "JetBrains Mono Variable","JetBrains Mono","Menlo",monospace}html[data-theme="green"]{--background: #020604;--foreground: #4fdd7d;--card: #04110a;--card-foreground: #4fdd7d;--popover: #04110a;--popover-foreground: #4fdd7d;--primary: #33ff66;--primary-foreground: #02160a;--secondary: #071509;--secondary-foreground: #4fdd7d;--muted: #071509;--muted-foreground: #3da45e;--accent: #33ff66;--accent-foreground: #02160a;--destructive: #ff5c5c;--success: #4ade80;--warning: #f5b32b;--info: #4dabf7;--pending: #b197fc;--border: #1c4a2c;--input: #1c4a2c;--ring: #33ff66;--glow: rgba(51,255,102,0.35);--glow-soft: rgba(51,255,102,0.12);--scanline-opacity: 0.5}html[data-theme="mono"]{--background: #050505;--foreground: #d4d4d4;--card: #0d0d0d;--card-foreground: #d4d4d4;--popover: #0d0d0d;--popover-foreground: #d4d4d4;--primary: #ffffff;--primary-foreground: #050505;--secondary: #111111;--secondary-foreground: #d4d4d4;--muted: #111111;--muted-foreground: #8a8a8a;--accent: #ffffff;--accent-foreground: #050505;--destructive: #ff5c5c;--success: #4ade80;--warning: #f5b32b;--info: #4dabf7;--pending: #b197fc;--border: #333333;--input: #333333;--ring: #ffffff;--glow: rgba(255,255,255,0.25);--glow-soft: rgba(255,255,255,0.08);--scanline-opacity: 0.45}html[data-theme="paper"]{--background: #fafafa;--foreground: #1f1f1f;--card: #f1f1f1;--card-foreground: #1f1f1f;--popover: #f1f1f1;--popover-foreground: #1f1f1f;--primary: #000000;--primary-foreground: #fafafa;--secondary: #efefef;--secondary-foreground: #1f1f1f;--muted: #efefef;--muted-foreground: #595959;--accent: #000000;--accent-foreground: #fafafa;--destructive: #b3261e;--success: #1a7031;--warning: #8f5d00;--info: #1c5f9e;--pending: #5f3dc4;--border: #d4d4d4;--input: #d4d4d4;--ring: #000000;--glow: rgba(0,0,0,0.06);--glow-soft: rgba(0,0,0,0.04);--scanline-opacity: 0.12}:root{--content-w: 78rem;--content-pad: 1.5rem;--fs-xs: 0.625rem;--fs-sm: 0.6875rem;--fs-base: 0.75rem;--fs-md: 0.8125rem;--fs-lg: 0.9375rem;--fs-xl: 1.125rem;--fs-2xl: 1.5rem;--lh-tight: 1.3;--lh-base: 1.5;--field-label-w: 8.5rem}html{scroll-behavior: smooth}*,*::before,*::after{border-color: var(--border)}:focus-visible{outline: 2px solid var(--ring);outline-offset: 2px}body{background: var(--background);color: var(--foreground);font-family: var(--font-mono);font-size: var(--fs-base);line-height: var(--lh-base);overflow-x: hidden;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale}table{font-size: var(--fs-md);width: 100%}th,td{padding: 0.45rem 0.85rem;text-align: left;vertical-align: top;border-block-end: 1px solid color-mix(in srgb,var(--border) 55%,transparent)}thead th{border-block-end-color: var(--border);color: var(--muted-foreground);font-weight: 600}tbody tr:last-child>*{border-block-end: 0}tr>*:first-child{padding-inline-start: 0}tr>*:last-child{padding-inline-end: 0}button:not(:disabled),summary,[role="button"]{cursor: pointer}body::after{content: "";position: fixed;inset: 0;z-index: 40;pointer-events: none;background: repeating-linear-gradient( 0deg,rgba(0,0,0,0.25) 0px,rgba(0,0,0,0.25) 1px,transparent 1px,transparent 3px );opacity: var(--scanline-opacity)}::selection{background: var(--primary);color: var(--primary-foreground)}.glow{color: var(--primary);text-shadow: 0 0 8px var(--glow)}.glow-lg{color: var(--primary);text-shadow: 0 0 12px var(--glow)}.field-row{display: grid;grid-template-columns: var(--field-label-w) minmax(0,1fr);align-items: center;gap: 0.5rem 0.75rem;margin: 0.35rem 0}.field-row>.lbl{color: var(--muted-foreground);font-size: var(--fs-sm);text-align: left}.field-row>.field-val{display: flex;flex-wrap: wrap;align-items: center;gap: 0.4rem 0.9rem;min-width: 0}@media (max-width: 40rem){.field-row{grid-template-columns: 1fr;gap: 0.2rem}}.prompt{font-size: 0.75rem;font-weight: 700;color: var(--muted-foreground)}.prompt::before{content: "$ ";color: var(--primary)}.comment::before{content: "# ";color: var(--border)}.cursor-block{display: inline-block;width: 0.55em;height: 1em;margin-left: 8px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em);animation: ddd-blink 1.1s step-end infinite}@keyframes ddd-blink{50%{opacity: 0}}.btn-terminal{display: inline-block;background: var(--primary);color: var(--primary-foreground);font-weight: 700;padding: 12px 24px;box-shadow: 0 0 16px var(--glow);transition: box-shadow 0.15s ease,transform 0.15s ease}.btn-terminal::before{content: "> "}.btn-terminal--ghost{background: transparent;color: var(--primary);border: 1px solid var(--border);box-shadow: none;font-weight: 400}.btn-terminal--ghost::before{content: ""}.btn-terminal--ghost:hover{border-color: var(--primary);background: color-mix(in srgb,var(--primary) 10%,transparent);box-shadow: none}.btn-terminal:hover{box-shadow: 0 0 28px var(--glow);transform: translateY(-2px)}.btn-terminal--compact{padding: 0.28rem 0.7rem;font-size: var(--fs-sm)}.btn-terminal--destructive{display: inline-flex;align-items: center;justify-content: center;padding: 0.3rem;color: var(--destructive);border-color: color-mix(in srgb,var(--destructive) 45%,var(--border))}.btn-terminal--destructive::before{content: ""}.btn-terminal--destructive::after{content: "";display: block;width: 0.875rem;height: 0.875rem;background: currentColor;-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 3h6v3H9zM4 6h16v2H4zM6 9h12l-1 12H7z'/%3E%3C/svg%3E") center / contain no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 3h6v3H9zM4 6h16v2H4zM6 9h12l-1 12H7z'/%3E%3C/svg%3E") center / contain no-repeat}.btn-terminal--destructive:hover{border-color: var(--destructive);background: color-mix(in srgb,var(--destructive) 12%,transparent);box-shadow: none}@media (pointer: coarse){.btn-terminal--destructive{min-width: 44px;min-height: 44px}}.link-quiet{color: var(--muted-foreground);text-decoration: underline;text-underline-offset: 4px;transition: color 0.15s ease}.link-quiet:hover{color: var(--primary)}.anim-toggle{display: inline-flex;align-items: center;gap: 6px;font: inherit;color: var(--muted-foreground);background: none;border: 0;cursor: pointer;transition: color 0.15s ease}.anim-toggle:hover{color: var(--primary)}.anim-toggle[aria-pressed="true"] [data-anim-box]{color: var(--primary)}.dd-dot{display: inline-block;width: 11px;height: 11px;flex: none;border-radius: 50%;background: currentColor}.dd-flag{display: inline-block;width: 15px;height: 10px;flex: none;border: 1px solid var(--border);background-size: cover;background-repeat: no-repeat}.dd-flag-de{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='4' fill='%23262626'/><rect y='4' width='18' height='4' fill='%23c83232'/><rect y='8' width='18' height='4' fill='%23e8be32'/></svg>")}.dd-flag-en{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='12' fill='%23b22234'/><rect y='4' width='18' height='4' fill='%23ececec'/><rect width='8' height='6' fill='%233c3b6e'/></svg>")}.dd-flag-pt{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='12' fill='%231c9148'/><polygon points='9,1.5 15.5,6 9,10.5 2.5,6' fill='%23e8c832'/><rect x='8' y='5' width='2' height='2' fill='%232c3f87'/></svg>")}.dd-flag-es{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='6' height='12' fill='%2315734c'/><rect x='6' width='6' height='12' fill='%23ececec'/><rect x='12' width='6' height='12' fill='%23bf2233'/><rect x='8' y='5' width='2' height='2' fill='%237a5c3a'/></svg>")}.ascii-rule{margin: 1rem 0;border: 0;color: var(--border);line-height: 1;white-space: nowrap;overflow: hidden;user-select: none}.ascii-rule::before{content: "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"}.card-terminal{background: var(--card);border: 1px solid var(--border);transition: border-color 0.15s ease,box-shadow 0.15s ease}.card-terminal:hover{border-color: var(--primary);box-shadow: 0 0 20px var(--glow-soft)}.dropdown{position: relative}.dropdown>summary{display: inline-flex;align-items: center;gap: 6px;cursor: pointer;user-select: none;list-style: none;color: var(--muted-foreground);transition: color 0.15s ease}.dropdown>summary::-webkit-details-marker{display: none}.dropdown>summary:hover,.dropdown[open]>summary{color: var(--primary)}.dropdown-panel{position: absolute;right: 0;bottom: calc(100% + 12px);z-index: 50;min-width: 128px;margin: 0;padding: 4px 0;list-style: none;background: var(--card);border: 1px solid var(--border);border-radius: var(--radius);box-shadow: 0 0 20px var(--glow-soft)}.dropdown-panel--down{top: calc(100% + 8px);bottom: auto;left: 0;right: auto}.dropdown-item{display: flex;align-items: center;gap: 8px;width: 100%;padding: 5px 14px;text-align: left;font: inherit;line-height: 1.5;background: none;border: 0;color: var(--muted-foreground);cursor: pointer;white-space: nowrap;transition: color 0.15s ease}.dropdown-item:hover{color: var(--primary);background: var(--muted)}.dropdown-item[aria-current="true"],html:not([data-theme]) .dropdown-item[data-theme-value="warm"],html[data-theme="green"] .dropdown-item[data-theme-value="green"],html[data-theme="mono"] .dropdown-item[data-theme-value="mono"],html[data-theme="paper"] .dropdown-item[data-theme-value="paper"],html[data-theme="warm"] .dropdown-item[data-theme-value="warm"]{color: var(--primary);text-shadow: 0 0 8px var(--glow)}.eli5{margin: 1.25em 0;padding: 0.7em 1em;color: var(--foreground);background: color-mix(in srgb,var(--primary) 7%,var(--card));border: 1px solid var(--border);border-left: 3px solid var(--primary);border-radius: var(--radius);font-size: 0.92em;line-height: 1.6}.eli5::before{content: "ELI5";display: inline-flex;align-items: center;justify-content: center;margin-right: 0.6em;vertical-align: 0.08em;font-family: var(--font-mono);font-size: 0.72em;font-weight: 700;line-height: 1;letter-spacing: 0.04em;padding: 0.34em 0.6em;border-radius: var(--radius);background: var(--destructive);color: var(--primary-foreground)}.eli5-term{font-weight: 700;color: var(--primary)}html.anim-off *,html.anim-off *::before,html.anim-off *::after{animation: none !important}html.anim-off .cursor-block{display: none}@media (prefers-reduced-motion: no-preference){html.term-anim [data-term] .prompt:not(.term-live),html.term-anim [data-term] .prompt:not(.term-live)::before{color: transparent}html.term-anim [data-term] [data-term-out]:not(.term-show){visibility: hidden}html.term-anim [data-term] [data-term-out].term-show{animation: ddd-term-output 0.2s steps(3,end) both}@keyframes ddd-term-output{from{opacity: 0}}html.term-anim [data-term] [data-term-out].term-show>*{animation: ddd-term-output 0.25s steps(3,end) both}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(2){animation-delay: 0.09s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(3){animation-delay: 0.18s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(4){animation-delay: 0.27s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(5){animation-delay: 0.36s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(6){animation-delay: 0.45s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(7){animation-delay: 0.54s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(8){animation-delay: 0.63s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(9){animation-delay: 0.72s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(10){animation-delay: 0.81s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(n + 11){animation-delay: 0.9s}.term-caret{display: inline-block;width: 0.55em;height: 1em;margin-left: 2px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em)}}.tabs{display: flex;flex-wrap: wrap;gap: 0.15rem;margin: 1.35rem 0 0;border-bottom: 2px solid color-mix(in srgb,var(--border) 80%,transparent)}.tab{background: transparent;border: 0;margin-bottom: -2px;font: inherit;font-size: 0.86em;cursor: pointer;padding: 0.5rem 0.95rem;color: var(--muted-foreground);transition: background 0.12s ease,color 0.12s ease}.tab:hover{color: var(--primary);background: color-mix(in srgb,var(--primary) 10%,transparent)}.tab[aria-selected="true"]{background: var(--primary);color: var(--background);font-weight: 700}.tab:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}.tab--info{margin-inline-start: auto}@media (max-width: 40rem){.tab--info{margin-inline-start: 0}}@media (pointer: coarse),(max-width: 40rem){.tab{padding: 0.7rem 0.8rem;min-height: 44px}}section.doc.tab-panel{margin-top: 1.35rem}details.fold{border-top: 1px solid color-mix(in srgb,currentColor 15%,transparent)}details.fold>summary{display: flex;align-items: center;gap: 0.5rem;cursor: pointer;list-style: none;padding: 0.55rem 0;font-size: 0.85rem;color: var(--muted-foreground)}details.fold>summary::-webkit-details-marker{display: none}details.fold>summary::before{content: "\25B8";flex: none;opacity: 0.6}details.fold[open]>summary::before{content: "\25BE"}details.fold>summary:hover{color: var(--primary)}details.fold>summary:focus-visible{outline: 2px solid color-mix(in srgb,currentColor 45%,transparent);outline-offset: 2px}.fold-body{font-size: 0.9rem;padding: 0 0 0.7rem 1.1rem}.fold-body>* + *{margin-top: 0.6rem}.legend{list-style: none;padding: 0;margin: 0.7rem 0 0;display: grid;gap: 0.4rem 1.4rem;font-size: 0.86rem}@media (min-width: 52rem){.legend{grid-template-columns: repeat(2,minmax(0,1fr))}}.legend>li{line-height: 1.6}.legend>li>:first-child{margin-right: 0.45rem}.dgm-zoomable{position: relative;cursor: zoom-in}.dgm-zoomable::after{content: "⤢";position: absolute;top: 0.4rem;right: 0.5rem;font-size: 0.85rem;line-height: 1;opacity: 0.45;pointer-events: none;color: var(--muted-foreground)}.dgm-zoomable:hover::after,.dgm-zoomable:focus-visible::after{opacity: 1;color: var(--primary)}.dgm-zoomable:focus-visible{outline: 2px solid var(--primary);outline-offset: 3px}html.dgm-locked,html.dgm-locked body{overflow: hidden}.dgm-overlay{display: none;position: fixed;inset: 0;z-index: 90;background: color-mix(in srgb,var(--background) 96%,transparent);flex-direction: column}.dgm-overlay.open{display: flex}.dgm-bar{flex: none;display: flex;align-items: center;justify-content: flex-end;gap: 0.4rem;padding: 0.5rem 0.75rem;border-bottom: 1px solid var(--border);background: var(--card)}.dgm-btn{font: inherit;font-size: 0.82rem;cursor: pointer;padding: 0.3rem 0.7rem;min-height: 32px;background: transparent;border: 1px solid var(--border);color: var(--foreground)}.dgm-btn:hover{border-color: var(--primary);color: var(--primary)}.dgm-btn:focus-visible{outline: 2px solid var(--primary);outline-offset: 1px}.dgm-close{border-color: color-mix(in srgb,var(--destructive) 55%,transparent);color: var(--destructive)}.dgm-close:hover{border-color: var(--destructive);color: var(--destructive)}.dgm-stage{flex: 1;overflow: hidden;display: grid;place-items: center;cursor: grab;touch-action: none}.dgm-stage.is-grabbing{cursor: grabbing}.dgm-art{transform-origin: center center;will-change: transform}.dgm-art>svg,.dgm-art>img{display: block;max-width: none;max-height: none}@media print{.dgm-overlay,.dgm-zoomable::after{display: none !important}}.minimap{position: fixed;left: 0;top: var(--ls-nav-top,3rem);bottom: var(--ls-nav-bottom,2.2rem);z-index: 20;display: none;flex-direction: column;justify-content: center;gap: 0.5rem;width: 2rem;padding: 0.75rem 0 0.75rem 0.75rem;overflow-y: auto;overscroll-behavior: contain;scrollbar-width: none}.minimap::-webkit-scrollbar{display: none}@media (min-width: 64rem){.minimap{display: flex}}.minimap-bar{flex: 0 0 auto;width: var(--minimap-bar-w,100%);height: 2px;padding: 0;border: 0;border-radius: 1px;background: var(--border);cursor: pointer;transition: background 0.15s ease,transform 0.15s ease;transform-origin: left center}.minimap-bar:hover{background: var(--muted-foreground);transform: scaleY(2)}.minimap-bar[data-tip]{cursor: pointer}.minimap-bar:focus-visible{outline: 2px solid var(--ring);outline-offset: 3px}.minimap-bar.active{background: var(--primary);transform: scaleY(2)}@media print{.minimap{display: none !important}}.wrap{max-width: var(--content-w);margin-inline: auto;padding-inline: var(--content-pad)}.tablewrap{position: relative;overflow-x: auto;overflow-y: auto;max-block-size: var(--tablewrap-max-h,none)}.tablewrap::after{content: "";position: absolute;inset-block: 0;right: 0;width: 1.5rem;pointer-events: none;opacity: 0.9;background: linear-gradient(to right,transparent,var(--background,transparent))}.tablewrap thead th{position: sticky;inset-block-start: 0;z-index: 1;background: var(--background,#f5efe2)}.tickstrip:not(:empty){border: 1px solid var(--border);background: var(--card);margin: 1.6rem 0 1.5rem}.ticktable{width: 100%;border-collapse: collapse;font-size: 0.82rem}.ticktable td{padding: 0.5rem 0.75rem;vertical-align: baseline;border: 0;white-space: nowrap}.ticktable td.tick-stats{width: 100%;white-space: normal;text-align: right}.tick{border-bottom: 1px solid color-mix(in srgb,var(--border) 55%,transparent)}.tick:last-child{border-bottom: 0}.tick-dot{font-size: 0.7em;width: 1.5rem;padding-right: 0 !important;border-left: 3px solid transparent;padding-left: 0.6rem !important}.tick--ok .tick-dot{color: var(--success);border-left-color: color-mix(in srgb,var(--success) 45%,transparent)}.tick--stale .tick-dot{color: var(--destructive);border-left-color: var(--destructive)}.tick--never .tick-dot{color: var(--muted-foreground);border-left-color: color-mix(in srgb,var(--muted-foreground) 35%,transparent)}.tick-name{font-weight: 700;color: var(--foreground);letter-spacing: 0.01em}.tick-last,.tick-next{font-variant-numeric: tabular-nums;color: var(--muted-foreground)}.tick-next{opacity: 0.72}.tick--stale .tick-last{color: var(--destructive);font-weight: 700}.tick--never .tick-last{font-style: italic}.tick-stats{color: var(--muted-foreground)}.tick-stats b{font-weight: 700;color: var(--primary);font-variant-numeric: tabular-nums}.tick-sep{font-style: normal;opacity: 0.35;margin: 0 0.5rem}.tick--stale{background: color-mix(in srgb,var(--destructive) 8%,transparent)}@media (max-width: 40rem){.ticktable,.ticktable tbody,.tick,.ticktable td{display: block}.tick{padding: 0.4rem 0.5rem}.ticktable td{display: inline-block;padding: 0.1rem 0.5rem 0.1rem 0}.ticktable td.tick-stats{display: block;width: auto;text-align: left}}.skip-link{position: absolute;left: -999px;top: 0;z-index: 100;background: var(--card);border: 1px solid var(--primary);padding: 0.5rem 1rem;color: var(--primary)}.skip-link:focus{left: 0.5rem;top: 0.5rem}.visually-hidden{position: absolute;width: 1px;height: 1px;overflow: hidden;clip: rect(0 0 0 0);white-space: nowrap}header.bar{position: sticky;top: 0;z-index: 30;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.6rem 1.25rem;border-bottom: 1px solid var(--border);background: var(--card)}header.bar .brand{display: inline-flex;align-items: center;font-weight: 700;font-size: 1rem}header.bar .brand,header.bar .brand a{color: inherit;text-decoration: none}header.bar .dropdown summary{padding: 0.6rem 0.4rem}.bar-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}footer.status{position: fixed;inset-inline: 0;bottom: 0;z-index: 50;min-height: 2rem;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.35rem 1.25rem;border-top: 1px solid var(--border);background: var(--card);font-size: 0.7rem}footer.status .dropdown summary,footer.status .anim-toggle{padding: 0.55rem 0.4rem}.status-left{color: var(--muted-foreground);min-width: 0;overflow: hidden;text-overflow: ellipsis;white-space: nowrap}.status-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}.sep{width: 1px;height: 0.9rem;background: var(--border)}.doc-link{color: var(--muted-foreground);text-decoration: none;transition: color 0.15s ease}.doc-link:hover{color: var(--primary)}.doc-link--forward{color: var(--primary);white-space: nowrap}.doc-link--forward:hover{color: var(--primary);text-decoration: underline}.ls-group{display: block;padding: 0.55rem 0 0.15rem;font-size: var(--fs-xs);letter-spacing: 0.08em;color: var(--muted-foreground);opacity: 0.75}.ls-panel>li:first-child .ls-group{padding-top: 0}.ls-row{display: flex;align-items: baseline;gap: 0.9rem}.ls-perm{color: var(--muted-foreground);font-size: 0.72rem;letter-spacing: 0}.ls-row--sub .ls-name{margin-left: 0.55rem;padding-left: 0.75rem;border-left: 1px solid color-mix(in srgb,var(--muted-foreground) 40%,transparent)}.ls-row--sub2 .ls-name{margin-left: 1.7rem}.ls-panel .ls-name{color: var(--muted-foreground)}.ls-panel .ls-row--dir .ls-name{color: var(--primary);font-weight: 700}.ls-row[aria-current="page"]{background: color-mix(in srgb,var(--primary) 12%,transparent);box-shadow: inset 3px 0 0 0 var(--primary)}.ls-row[aria-current="page"] .ls-name{color: var(--primary);font-weight: 700;text-shadow: 0 0 8px var(--glow)}.ls-row[aria-current="page"] .ls-perm{color: color-mix(in srgb,var(--primary) 65%,transparent)}.ls-row[aria-current="page"]::after{content: "←";margin-left: auto;color: var(--primary)}.dropdown-panel.ls-panel{min-width: 17rem;left: auto !important;right: 0}:root{--ls-nav-w: 17rem;--ls-nav-top: 3rem;--ls-nav-bottom: 2.2rem}@media (min-width: 48.0625rem){html{--ls-nav-inset: 0rem}html:has(.ls-nav):not([data-ls-nav="off"]){--ls-nav-inset: var(--ls-nav-w)}body{padding-right: var(--ls-nav-inset)}header.bar,.bleed-rail{margin-right: calc(-1 * var(--ls-nav-inset))}.ls-nav{position: fixed;top: var(--ls-nav-top);right: 0;bottom: var(--ls-nav-bottom);width: var(--ls-nav-w);z-index: 25;display: flex;flex-direction: column;overflow: hidden;background: var(--card);border-left: 1px solid var(--border)}html:not(.anim-off) .ls-nav{transition: transform 0.18s ease}html[data-ls-nav="off"] .ls-nav{transform: translateX(100%)}.ls-nav-head{display: inline-flex;align-items: center;gap: 0.5rem;margin-left: 0.75rem}.ls-nav-title{font-weight: 700;font-size: 0.9rem}.ls-nav .ls-panel{flex: 1;min-height: 0;overflow-y: auto;list-style: none;margin: 0;padding: 0.5rem 0 1rem}.ls-nav-toggle{background: none;border: 0;font: inherit;cursor: pointer;line-height: 1;color: var(--muted-foreground);padding: 0.4rem 0.55rem}.ls-nav-toggle::after{content: "\00BB"}html[data-ls-nav="off"] .ls-nav-toggle::after{content: "\00AB"}.ls-nav-toggle:hover{color: var(--primary)}.ls-nav-toggle:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}}.nav-burger{display: none;background: none;border: 0;color: var(--muted-foreground);cursor: pointer;padding: 0.55rem;margin: -0.3rem -0.3rem -0.3rem 0}.nav-burger:hover{color: var(--primary)}.mobile-nav{display: none;list-style: none;margin: 0;padding: 0}.mobile-footer{display: none}@media (max-width: 48rem){.nav-burger{display: inline-flex;align-items: center}.site-nav{display: none;position: absolute;top: 100%;left: 0;right: 0;z-index: 40;background: var(--card);border-bottom: 1px solid var(--border);padding: 0.4rem 1.25rem 0.9rem;max-height: calc(100vh - 6rem);overflow-y: auto}.site-nav.open{display: block}.site-nav>.ls-nav,.mobile-nav{display: flex;flex-direction: column}.site-nav:has(>.ls-nav) .mobile-nav{display: none}.site-nav>.ls-nav .ls-row{align-items: center;min-height: 44px;border-bottom: 1px solid var(--border)}.site-nav>.ls-nav .ls-row .ls-name{display: flex;align-items: center;flex: 1;min-height: 44px}.site-nav>.ls-nav .ls-row:last-child{border-bottom: 0}.mobile-nav .mobile-item{display: flex;align-items: baseline;gap: 0.9rem;padding: 0.65rem 0;text-decoration: none;color: var(--foreground);font-size: 0.9rem;border-bottom: 1px solid var(--border)}.mobile-nav li:last-child .mobile-item{border-bottom: 0}.mobile-nav .mobile-item:active .ls-name,.mobile-nav .mobile-item:hover .ls-name{color: var(--primary)}.ls-nav-toggle{display: none}footer.status{display: none}.mobile-footer{display: flex;flex-direction: column;gap: 0.55rem;margin-top: 0.8rem;border-top: 1px solid var(--border);padding-top: 0.8rem;font-size: 0.85rem}.mobile-footer .doc-link{padding: 0.2rem 0}.mobile-footer .mobile-theme summary{list-style: none;display: flex;align-items: center;gap: 0.5rem;color: var(--muted-foreground);cursor: pointer;padding: 0.2rem 0}.mobile-footer .mobile-theme summary::-webkit-details-marker{display: none}.mobile-footer .mf-chev{margin-left: auto;opacity: 0.7}.mobile-footer .mf-panel{display: flex;flex-direction: column;margin-top: 0.3rem}.mobile-footer .mf-panel .dropdown-item{text-align: left;padding: 0.4rem 0.6rem}.mobile-footer .anim-toggle{display: inline-flex;gap: 0.5rem;align-items: center;background: none;border: 0;padding: 0.2rem 0;color: var(--muted-foreground);font: inherit;cursor: pointer}}[data-tip]{cursor: help}span[data-tip],th[data-tip]{border-bottom: 1px dotted var(--muted-foreground)}#ddtip{position: fixed;z-index: 9999;display: none;max-width: 340px;background: var(--background);color: var(--foreground);border: 1px solid var(--muted-foreground);border-radius: var(--radius);padding: 7px 10px;font-size: 0.72rem;line-height: 1.45;font-weight: 400;text-align: left;white-space: normal;pointer-events: none;box-shadow: 0 4px 14px rgba(0,0,0,0.25)}@media print{@page{margin: 14mm 12mm}:root,html,html[data-theme]{--background: #ffffff;--foreground: #111111;--card: #ffffff;--card-foreground: #111111;--popover: #ffffff;--popover-foreground: #111111;--primary: #000000;--primary-foreground: #ffffff;--secondary: #ffffff;--secondary-foreground: #111111;--muted: #ffffff;--muted-foreground: #444444;--accent: #000000;--accent-foreground: #ffffff;--destructive: #7a1c1c;--border: #9a9a9a;--input: #9a9a9a;--ring: #000000;--glow: transparent;--glow-soft: transparent;--scanline-opacity: 0}html{zoom: 1 !important;font-size: 16px;scroll-behavior: auto}body{background: #ffffff;color: var(--foreground);font-size: 12px;line-height: 1.45;overflow: visible}body::after{display: none !important}*,*::before,*::after{animation: none !important;transition: none !important;text-shadow: none !important;box-shadow: none !important}html.term-anim [data-term] [data-term-out],html.term-anim [data-term] [data-term-out]:not(.term-show),html.term-anim [data-term] [data-term-out]>*{visibility: visible !important;opacity: 1 !important}html.term-anim [data-term] .prompt:not(.term-live){color: var(--muted-foreground) !important}html.term-anim [data-term] .prompt:not(.term-live)::before{color: var(--primary) !important}header.bar,footer.status,.site-nav,.nav-burger,.mobile-nav,.mobile-footer,.skip-link,.dropdown,.dropdown-panel,.anim-toggle,.cursor-block,.term-caret,.ascii-rule,.toc,#ddtip{display: none !important}body{padding-right: 0 !important}[data-tip]{cursor: auto}span[data-tip],th[data-tip]{border-bottom: 0}.wrap{max-width: none !important;margin: 0 !important;padding: 0 !important}.layout{display: block !important}.content{min-width: 0}h1,.title{font-size: 1.5rem !important}h2{font-size: 1.15rem !important}h3,.sub{font-size: 1rem !important}h4{font-size: 0.9rem !important}.prompt{font-size: 0.7rem}h1,h2,h3,h4,.prompt,.toc-label{break-after: avoid-page;break-inside: avoid}p,li,blockquote{orphans: 3;widows: 3}pre,figure,blockquote,tr,img,.eli5,.card-terminal{break-inside: avoid}svg{break-inside: auto}thead{display: table-header-group}.tablewrap,.table-scroll,pre,.diagram{overflow: visible !important;max-width: 100% !important}pre{white-space: pre-wrap !important;overflow-wrap: anywhere;font-size: 0.75rem !important;background: transparent !important}pre.mermaid[data-processed]{white-space: normal !important}code{background: transparent !important;overflow-wrap: anywhere}table{width: 100% !important;table-layout: auto;border-collapse: collapse;font-size: 0.75rem !important}th,td{overflow-wrap: break-word;white-space: normal !important}td code,th code,td a,th a{overflow-wrap: anywhere}a{color: inherit;text-decoration: underline;text-underline-offset: 2px}a.card-terminal,a[class*="card"]{text-decoration: none}.glow,.glow-lg{color: inherit}.btn-terminal{border: 1px solid var(--border);background: transparent;color: var(--foreground)}.mermaid svg{display: block;margin-inline: auto;width: auto !important;height: auto !important;max-width: 100% !important;max-height: 235mm !important}.mermaid svg .node rect,.mermaid svg .node polygon,.mermaid svg .node circle,.mermaid svg .node path,.mermaid svg .label-container,.mermaid svg .cluster rect,.mermaid svg .actor,.mermaid svg .note{fill: #ffffff !important;stroke: #333333 !important}.mermaid svg .edgePath path,.mermaid svg .flowchart-link,.mermaid svg line,.mermaid svg .messageLine0,.mermaid svg .messageLine1{stroke: #333333 !important}.mermaid svg .arrowheadPath,.mermaid svg marker path,.mermaid svg marker polygon{fill: #333333 !important;stroke: #333333 !important}.mermaid svg text,.mermaid svg .nodeLabel,.mermaid svg .edgeLabel,.mermaid svg .cluster-label,.mermaid svg .messageText,.mermaid svg foreignObject div,.mermaid svg foreignObject span,.mermaid svg p{fill: #111111 !important;color: #111111 !important;background: transparent !important}.mermaid svg .edgeLabel rect,.mermaid svg .edgeLabel foreignObject>div{fill: #ffffff !important;background: #ffffff !important}}
1
+ /*! danieldeusing-design v0.21.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
2
+ *,*::before,*::after{box-sizing: border-box}*{margin: 0}html{-webkit-text-size-adjust: 100%;text-size-adjust: 100%;tab-size: 4}body{min-height: 100vh;min-height: 100dvh;line-height: 1.5}img,picture,video,canvas,svg{display: block;max-width: 100%}input,button,textarea,select{font: inherit;color: inherit}p,h1,h2,h3,h4,h5,h6{overflow-wrap: break-word}ul,ol{list-style: none;padding: 0}a{color: inherit;text-decoration: none}table{border-collapse: collapse}:root{--background: #f5efe2;--foreground: #43352a;--card: #efe7d4;--card-foreground: #43352a;--popover: #efe7d4;--popover-foreground: #43352a;--primary: #8a4516;--primary-foreground: #f8f3e8;--secondary: #ece3cf;--secondary-foreground: #43352a;--muted: #ece3cf;--muted-foreground: #71614e;--accent: #8a4516;--accent-foreground: #f8f3e8;--destructive: #a02c2c;--success: #1a6b2e;--warning: #845600;--info: #1c5f9e;--pending: #5f3dc4;--border: #d9cdb6;--input: #d9cdb6;--ring: #8a4516;--glow: rgba(160,82,45,0.1);--glow-soft: rgba(160,82,45,0.05);--scanline-opacity: 0.15;--radius: 0rem;--font-mono: "JetBrains Mono Variable","JetBrains Mono","Menlo",monospace}html[data-theme="green"]{--background: #020604;--foreground: #4fdd7d;--card: #04110a;--card-foreground: #4fdd7d;--popover: #04110a;--popover-foreground: #4fdd7d;--primary: #33ff66;--primary-foreground: #02160a;--secondary: #071509;--secondary-foreground: #4fdd7d;--muted: #071509;--muted-foreground: #3da45e;--accent: #33ff66;--accent-foreground: #02160a;--destructive: #ff5c5c;--success: #4ade80;--warning: #f5b32b;--info: #4dabf7;--pending: #b197fc;--border: #1c4a2c;--input: #1c4a2c;--ring: #33ff66;--glow: rgba(51,255,102,0.35);--glow-soft: rgba(51,255,102,0.12);--scanline-opacity: 0.5}html[data-theme="mono"]{--background: #050505;--foreground: #d4d4d4;--card: #0d0d0d;--card-foreground: #d4d4d4;--popover: #0d0d0d;--popover-foreground: #d4d4d4;--primary: #ffffff;--primary-foreground: #050505;--secondary: #111111;--secondary-foreground: #d4d4d4;--muted: #111111;--muted-foreground: #8a8a8a;--accent: #ffffff;--accent-foreground: #050505;--destructive: #ff5c5c;--success: #4ade80;--warning: #f5b32b;--info: #4dabf7;--pending: #b197fc;--border: #333333;--input: #333333;--ring: #ffffff;--glow: rgba(255,255,255,0.25);--glow-soft: rgba(255,255,255,0.08);--scanline-opacity: 0.45}html[data-theme="paper"]{--background: #fafafa;--foreground: #1f1f1f;--card: #f1f1f1;--card-foreground: #1f1f1f;--popover: #f1f1f1;--popover-foreground: #1f1f1f;--primary: #000000;--primary-foreground: #fafafa;--secondary: #efefef;--secondary-foreground: #1f1f1f;--muted: #efefef;--muted-foreground: #595959;--accent: #000000;--accent-foreground: #fafafa;--destructive: #b3261e;--success: #1a7031;--warning: #8f5d00;--info: #1c5f9e;--pending: #5f3dc4;--border: #d4d4d4;--input: #d4d4d4;--ring: #000000;--glow: rgba(0,0,0,0.06);--glow-soft: rgba(0,0,0,0.04);--scanline-opacity: 0.12}:root{--content-w: 78rem;--content-pad: 1.5rem;--fs-xs: 0.625rem;--fs-sm: 0.6875rem;--fs-base: 0.75rem;--fs-md: 0.8125rem;--fs-lg: 0.9375rem;--fs-xl: 1.125rem;--fs-2xl: 1.5rem;--lh-tight: 1.3;--lh-base: 1.5;--field-label-w: 8.5rem}html{scroll-behavior: smooth}*,*::before,*::after{border-color: var(--border)}:focus-visible{outline: 2px solid var(--ring);outline-offset: 2px}body{background: var(--background);color: var(--foreground);font-family: var(--font-mono);font-size: var(--fs-base);line-height: var(--lh-base);overflow-x: hidden;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale}table{font-size: var(--fs-md);width: 100%}th,td{padding: 0.45rem 0.85rem;text-align: left;vertical-align: top;border-block-end: 1px solid color-mix(in srgb,var(--border) 55%,transparent)}thead th{border-block-end-color: var(--border);color: var(--muted-foreground);font-weight: 600}tbody tr:last-child>*{border-block-end: 0}tr>*:first-child{padding-inline-start: 0}tr>*:last-child{padding-inline-end: 0}button:not(:disabled),summary,[role="button"]{cursor: pointer}body::after{content: "";position: fixed;inset: 0;z-index: 40;pointer-events: none;background: repeating-linear-gradient( 0deg,rgba(0,0,0,0.25) 0px,rgba(0,0,0,0.25) 1px,transparent 1px,transparent 3px );opacity: var(--scanline-opacity)}::selection{background: var(--primary);color: var(--primary-foreground)}.glow{color: var(--primary);text-shadow: 0 0 8px var(--glow)}.glow-lg{color: var(--primary);text-shadow: 0 0 12px var(--glow)}.field-row{display: grid;grid-template-columns: var(--field-label-w) minmax(0,1fr);align-items: center;gap: 0.5rem 0.75rem;margin: 0.35rem 0}.field-row>.lbl{color: var(--muted-foreground);font-size: var(--fs-sm);text-align: left}.field-row>.field-val{display: flex;flex-wrap: wrap;align-items: center;gap: 0.4rem 0.9rem;min-width: 0}@media (max-width: 40rem){.field-row{grid-template-columns: 1fr;gap: 0.2rem}}.prompt{font-size: 0.75rem;font-weight: 700;color: var(--muted-foreground)}.prompt::before{content: "$ ";color: var(--primary)}.comment::before{content: "# ";color: var(--border)}.cursor-block{display: inline-block;width: 0.55em;height: 1em;margin-left: 8px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em);animation: ddd-blink 1.1s step-end infinite}@keyframes ddd-blink{50%{opacity: 0}}.btn-terminal{display: inline-block;background: var(--primary);color: var(--primary-foreground);font-weight: 700;padding: 12px 24px;box-shadow: 0 0 16px var(--glow);transition: box-shadow 0.15s ease,transform 0.15s ease}.btn-terminal::before{content: "> "}.btn-terminal--ghost{background: transparent;color: var(--primary);border: 1px solid var(--border);box-shadow: none;font-weight: 400}.btn-terminal--ghost::before{content: ""}.btn-terminal--ghost:hover{border-color: var(--primary);background: color-mix(in srgb,var(--primary) 10%,transparent);box-shadow: none}.btn-terminal:hover{box-shadow: 0 0 28px var(--glow);transform: translateY(-2px)}.btn-terminal--compact{padding: 0.28rem 0.7rem;font-size: var(--fs-sm)}.btn-terminal--destructive{display: inline-flex;align-items: center;justify-content: center;padding: 0.3rem;color: var(--destructive);border-color: color-mix(in srgb,var(--destructive) 45%,var(--border))}.btn-terminal--destructive::before{content: ""}.btn-terminal--destructive::after{content: "";display: block;width: 0.875rem;height: 0.875rem;background: currentColor;-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 3h6v3H9zM4 6h16v2H4zM6 9h12l-1 12H7z'/%3E%3C/svg%3E") center / contain no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 3h6v3H9zM4 6h16v2H4zM6 9h12l-1 12H7z'/%3E%3C/svg%3E") center / contain no-repeat}.btn-terminal--destructive:hover{border-color: var(--destructive);background: color-mix(in srgb,var(--destructive) 12%,transparent);box-shadow: none}@media (pointer: coarse){.btn-terminal--destructive{min-width: 44px;min-height: 44px}}.link-quiet{color: var(--muted-foreground);text-decoration: underline;text-underline-offset: 4px;transition: color 0.15s ease}.link-quiet:hover{color: var(--primary)}.anim-toggle{display: inline-flex;align-items: center;gap: 6px;font: inherit;color: var(--muted-foreground);background: none;border: 0;cursor: pointer;transition: color 0.15s ease}.anim-toggle:hover{color: var(--primary)}.anim-toggle[aria-pressed="true"] [data-anim-box]{color: var(--primary)}.dd-dot{display: inline-block;width: 11px;height: 11px;flex: none;border-radius: 50%;background: currentColor}.dd-flag{display: inline-block;width: 15px;height: 10px;flex: none;border: 1px solid var(--border);background-size: cover;background-repeat: no-repeat}.dd-flag-de{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='4' fill='%23262626'/><rect y='4' width='18' height='4' fill='%23c83232'/><rect y='8' width='18' height='4' fill='%23e8be32'/></svg>")}.dd-flag-en{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='12' fill='%23b22234'/><rect y='4' width='18' height='4' fill='%23ececec'/><rect width='8' height='6' fill='%233c3b6e'/></svg>")}.dd-flag-pt{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='12' fill='%231c9148'/><polygon points='9,1.5 15.5,6 9,10.5 2.5,6' fill='%23e8c832'/><rect x='8' y='5' width='2' height='2' fill='%232c3f87'/></svg>")}.dd-flag-es{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='6' height='12' fill='%2315734c'/><rect x='6' width='6' height='12' fill='%23ececec'/><rect x='12' width='6' height='12' fill='%23bf2233'/><rect x='8' y='5' width='2' height='2' fill='%237a5c3a'/></svg>")}.ascii-rule{margin: 1rem 0;border: 0;color: var(--border);line-height: 1;white-space: nowrap;overflow: hidden;user-select: none}.ascii-rule::before{content: "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"}.card-terminal{background: var(--card);border: 1px solid var(--border);transition: border-color 0.15s ease,box-shadow 0.15s ease}.card-terminal:hover{border-color: var(--primary);box-shadow: 0 0 20px var(--glow-soft)}.dropdown{position: relative}.dropdown>summary{display: inline-flex;align-items: center;gap: 6px;cursor: pointer;user-select: none;list-style: none;color: var(--muted-foreground);transition: color 0.15s ease}.dropdown>summary::-webkit-details-marker{display: none}.dropdown>summary:hover,.dropdown[open]>summary{color: var(--primary)}.dropdown-panel{position: absolute;right: 0;bottom: calc(100% + 12px);z-index: 50;min-width: 128px;margin: 0;padding: 4px 0;list-style: none;background: var(--card);border: 1px solid var(--border);border-radius: var(--radius);box-shadow: 0 0 20px var(--glow-soft)}.dropdown-panel--down{top: calc(100% + 8px);bottom: auto;left: 0;right: auto}.dropdown-item{display: flex;align-items: center;gap: 8px;width: 100%;padding: 5px 14px;text-align: left;font: inherit;line-height: 1.5;background: none;border: 0;color: var(--muted-foreground);cursor: pointer;white-space: nowrap;transition: color 0.15s ease}.dropdown-item:hover{color: var(--primary);background: var(--muted)}.dropdown-item[aria-current="true"],html:not([data-theme]) .dropdown-item[data-theme-value="warm"],html[data-theme="green"] .dropdown-item[data-theme-value="green"],html[data-theme="mono"] .dropdown-item[data-theme-value="mono"],html[data-theme="paper"] .dropdown-item[data-theme-value="paper"],html[data-theme="warm"] .dropdown-item[data-theme-value="warm"]{color: var(--primary);text-shadow: 0 0 8px var(--glow)}select,.select-trigger{appearance: none;-webkit-appearance: none;max-width: 100%;padding: 0.28rem 1.45rem 0.28rem 0.5rem;font: inherit;font-size: var(--fs-sm);line-height: var(--lh-tight);text-align: left;color: var(--foreground);background-color: transparent;background-image: linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);background-position: right 0.78rem center,right 0.5rem center;background-size: 0.28rem 0.28rem;background-repeat: no-repeat;border: 1px solid color-mix(in srgb,var(--foreground) 60%,transparent);cursor: pointer;transition: border-color 0.15s ease}select:hover,.select-trigger:hover,.select-trigger[aria-expanded="true"]{border-color: var(--primary)}select:disabled,.select-trigger:disabled{opacity: 0.45;cursor: default}.select-trigger[aria-expanded="true"]{background-image: linear-gradient(135deg,transparent 50%,currentColor 50%),linear-gradient(45deg,currentColor 50%,transparent 50%)}.select-field{position: relative;display: inline-flex;max-width: 100%;vertical-align: middle}.select-field>select{position: absolute;inset: 0;width: 100%;height: 100%;opacity: 0;pointer-events: none}.select-trigger{display: inline-flex;align-items: center;width: 100%}.select-value{min-width: 0;overflow: hidden;text-overflow: ellipsis;white-space: nowrap}.select-value::after{content: "\200b"}.select-panel{position: fixed;z-index: 60;margin: 0;padding: 0.15rem 0;overflow-y: auto;overscroll-behavior: contain;list-style: none;font-size: var(--fs-sm);line-height: var(--lh-tight);color: var(--popover-foreground);background: var(--popover);border: 1px solid color-mix(in srgb,var(--foreground) 60%,transparent);box-shadow: 0 0 20px var(--glow-soft)}.select-option{display: flex;align-items: center;gap: 0.4rem;padding: 0.28rem 0.9rem 0.28rem 0.55rem;border-inline-start: 2px solid transparent;white-space: nowrap;cursor: pointer}.select-option:hover,.select-option[data-active="true"]{background: var(--muted)}.select-option[aria-selected="true"]{color: var(--primary);font-weight: 700;border-inline-start-color: var(--primary)}.select-option[aria-disabled="true"]{color: color-mix(in srgb,var(--popover-foreground) 65%,var(--popover));cursor: default}.select-option[aria-disabled="true"]:hover{background: none}.select-group{padding: 0.35rem 0.9rem 0.15rem;color: var(--muted-foreground);font-size: var(--fs-xs);text-transform: uppercase;letter-spacing: 0.06em}.eli5{margin: 1.25em 0;padding: 0.7em 1em;color: var(--foreground);background: color-mix(in srgb,var(--primary) 7%,var(--card));border: 1px solid var(--border);border-left: 3px solid var(--primary);border-radius: var(--radius);font-size: 0.92em;line-height: 1.6}.eli5::before{content: "ELI5";display: inline-flex;align-items: center;justify-content: center;margin-right: 0.6em;vertical-align: 0.08em;font-family: var(--font-mono);font-size: 0.72em;font-weight: 700;line-height: 1;letter-spacing: 0.04em;padding: 0.34em 0.6em;border-radius: var(--radius);background: var(--destructive);color: var(--primary-foreground)}.eli5-term{font-weight: 700;color: var(--primary)}html.anim-off *,html.anim-off *::before,html.anim-off *::after{animation: none !important}html.anim-off .cursor-block{display: none}@media (prefers-reduced-motion: no-preference){html.term-anim [data-term] .prompt:not(.term-live),html.term-anim [data-term] .prompt:not(.term-live)::before{color: transparent}html.term-anim [data-term] [data-term-out]:not(.term-show){visibility: hidden}html.term-anim [data-term] [data-term-out].term-show{animation: ddd-term-output 0.2s steps(3,end) both}@keyframes ddd-term-output{from{opacity: 0}}html.term-anim [data-term] [data-term-out].term-show>*{animation: ddd-term-output 0.25s steps(3,end) both}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(2){animation-delay: 0.09s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(3){animation-delay: 0.18s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(4){animation-delay: 0.27s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(5){animation-delay: 0.36s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(6){animation-delay: 0.45s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(7){animation-delay: 0.54s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(8){animation-delay: 0.63s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(9){animation-delay: 0.72s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(10){animation-delay: 0.81s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(n + 11){animation-delay: 0.9s}.term-caret{display: inline-block;width: 0.55em;height: 1em;margin-left: 2px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em)}}.tabs{display: flex;flex-wrap: wrap;gap: 0.15rem;margin: 1.35rem 0 0;border-bottom: 2px solid color-mix(in srgb,var(--border) 80%,transparent)}.tab{background: transparent;border: 0;margin-bottom: -2px;font: inherit;font-size: 0.86em;cursor: pointer;padding: 0.5rem 0.95rem;color: var(--muted-foreground);transition: background 0.12s ease,color 0.12s ease}.tab:hover{color: var(--primary);background: color-mix(in srgb,var(--primary) 10%,transparent)}.tab[aria-selected="true"]{background: var(--primary);color: var(--background);font-weight: 700}.tab:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}.tab--info{margin-inline-start: auto}@media (max-width: 40rem){.tab--info{margin-inline-start: 0}}@media (pointer: coarse),(max-width: 40rem){.tab{padding: 0.7rem 0.8rem;min-height: 44px}}section.doc.tab-panel{margin-top: 1.35rem}details.fold{border-top: 1px solid color-mix(in srgb,currentColor 15%,transparent)}details.fold>summary{display: flex;align-items: center;gap: 0.5rem;cursor: pointer;list-style: none;padding: 0.55rem 0;font-size: 0.85rem;color: var(--muted-foreground)}details.fold>summary::-webkit-details-marker{display: none}details.fold>summary::before{content: "\25B8";flex: none;opacity: 0.6}details.fold[open]>summary::before{content: "\25BE"}details.fold>summary:hover{color: var(--primary)}details.fold>summary:focus-visible{outline: 2px solid color-mix(in srgb,currentColor 45%,transparent);outline-offset: 2px}.fold-body{font-size: 0.9rem;padding: 0 0 0.7rem 1.1rem}.fold-body>* + *{margin-top: 0.6rem}.legend{list-style: none;padding: 0;margin: 0.7rem 0 0;display: grid;gap: 0.4rem 1.4rem;font-size: 0.86rem}@media (min-width: 52rem){.legend{grid-template-columns: repeat(2,minmax(0,1fr))}}.legend>li{line-height: 1.6}.legend>li>:first-child{margin-right: 0.45rem}.dgm-zoomable{position: relative;cursor: zoom-in}.dgm-zoomable::after{content: "⤢";position: absolute;top: 0.4rem;right: 0.5rem;font-size: 0.85rem;line-height: 1;opacity: 0.45;pointer-events: none;color: var(--muted-foreground)}.dgm-zoomable:hover::after,.dgm-zoomable:focus-visible::after{opacity: 1;color: var(--primary)}.dgm-zoomable:focus-visible{outline: 2px solid var(--primary);outline-offset: 3px}html.dgm-locked,html.dgm-locked body{overflow: hidden}.dgm-overlay{display: none;position: fixed;inset: 0;z-index: 90;background: color-mix(in srgb,var(--background) 96%,transparent);flex-direction: column}.dgm-overlay.open{display: flex}.dgm-bar{flex: none;display: flex;align-items: center;justify-content: flex-end;gap: 0.4rem;padding: 0.5rem 0.75rem;border-bottom: 1px solid var(--border);background: var(--card)}.dgm-btn{font: inherit;font-size: 0.82rem;cursor: pointer;padding: 0.3rem 0.7rem;min-height: 32px;background: transparent;border: 1px solid var(--border);color: var(--foreground)}.dgm-btn:hover{border-color: var(--primary);color: var(--primary)}.dgm-btn:focus-visible{outline: 2px solid var(--primary);outline-offset: 1px}.dgm-close{border-color: color-mix(in srgb,var(--destructive) 55%,transparent);color: var(--destructive)}.dgm-close:hover{border-color: var(--destructive);color: var(--destructive)}.dgm-stage{flex: 1;overflow: hidden;display: grid;place-items: center;cursor: grab;touch-action: none}.dgm-stage.is-grabbing{cursor: grabbing}.dgm-art{transform-origin: center center;will-change: transform}.dgm-art>svg,.dgm-art>img{display: block;max-width: none;max-height: none}@media print{.dgm-overlay,.dgm-zoomable::after{display: none !important}}.minimap{position: fixed;left: 0;top: var(--ls-nav-top,3rem);bottom: var(--ls-nav-bottom,2.2rem);z-index: 20;display: none;flex-direction: column;justify-content: center;gap: 0.5rem;width: 2rem;padding: 0.75rem 0 0.75rem 0.75rem;overflow-y: auto;overscroll-behavior: contain;scrollbar-width: none}.minimap::-webkit-scrollbar{display: none}@media (min-width: 64rem){.minimap{display: flex}}.minimap-bar{flex: 0 0 auto;width: var(--minimap-bar-w,100%);height: 2px;padding: 0;border: 0;border-radius: 1px;background: var(--border);cursor: pointer;transition: background 0.15s ease,transform 0.15s ease;transform-origin: left center}.minimap-bar:hover{background: var(--muted-foreground);transform: scaleY(2)}.minimap-bar[data-tip]{cursor: pointer}.minimap-bar:focus-visible{outline: 2px solid var(--ring);outline-offset: 3px}.minimap-bar.active{background: var(--primary);transform: scaleY(2)}@media print{.minimap{display: none !important}}.wrap{max-width: var(--content-w);margin-inline: auto;padding-inline: var(--content-pad)}.tablewrap{position: relative;overflow-x: auto;overflow-y: auto;max-block-size: var(--tablewrap-max-h,none)}.tablewrap::after{content: "";position: absolute;inset-block: 0;right: 0;width: 1.5rem;pointer-events: none;opacity: 0.9;background: linear-gradient(to right,transparent,var(--background,transparent))}.tablewrap thead th{position: sticky;inset-block-start: 0;z-index: 1;background: var(--background,#f5efe2)}.tickstrip:not(:empty){border: 1px solid var(--border);background: var(--card);margin: 1.6rem 0 1.5rem}.ticktable{width: 100%;border-collapse: collapse;font-size: 0.82rem}.ticktable td{padding: 0.5rem 0.75rem;vertical-align: baseline;border: 0;white-space: nowrap}.ticktable td.tick-stats{width: 100%;white-space: normal;text-align: right}.tick{border-bottom: 1px solid color-mix(in srgb,var(--border) 55%,transparent)}.tick:last-child{border-bottom: 0}.tick-dot{font-size: 0.7em;width: 1.5rem;padding-right: 0 !important;border-left: 3px solid transparent;padding-left: 0.6rem !important}.tick--ok .tick-dot{color: var(--success);border-left-color: color-mix(in srgb,var(--success) 45%,transparent)}.tick--stale .tick-dot{color: var(--destructive);border-left-color: var(--destructive)}.tick--never .tick-dot{color: var(--muted-foreground);border-left-color: color-mix(in srgb,var(--muted-foreground) 35%,transparent)}.tick-name{font-weight: 700;color: var(--foreground);letter-spacing: 0.01em}.tick-last,.tick-next{font-variant-numeric: tabular-nums;color: var(--muted-foreground)}.tick-next{opacity: 0.72}.tick--stale .tick-last{color: var(--destructive);font-weight: 700}.tick--never .tick-last{font-style: italic}.tick-stats{color: var(--muted-foreground)}.tick-stats b{font-weight: 700;color: var(--primary);font-variant-numeric: tabular-nums}.tick-sep{font-style: normal;opacity: 0.35;margin: 0 0.5rem}.tick--stale{background: color-mix(in srgb,var(--destructive) 8%,transparent)}@media (max-width: 40rem){.ticktable,.ticktable tbody,.tick,.ticktable td{display: block}.tick{padding: 0.4rem 0.5rem}.ticktable td{display: inline-block;padding: 0.1rem 0.5rem 0.1rem 0}.ticktable td.tick-stats{display: block;width: auto;text-align: left}}.skip-link{position: absolute;left: -999px;top: 0;z-index: 100;background: var(--card);border: 1px solid var(--primary);padding: 0.5rem 1rem;color: var(--primary)}.skip-link:focus{left: 0.5rem;top: 0.5rem}.visually-hidden{position: absolute;width: 1px;height: 1px;overflow: hidden;clip: rect(0 0 0 0);white-space: nowrap}header.bar{position: sticky;top: 0;z-index: 30;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.6rem 1.25rem;border-bottom: 1px solid var(--border);background: var(--card)}header.bar .brand{display: inline-flex;align-items: center;font-weight: 700;font-size: 1rem}header.bar .brand,header.bar .brand a{color: inherit;text-decoration: none}header.bar .dropdown summary{padding: 0.6rem 0.4rem}.bar-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}footer.status{position: fixed;inset-inline: 0;bottom: 0;z-index: 50;min-height: 2rem;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.35rem 1.25rem;border-top: 1px solid var(--border);background: var(--card);font-size: 0.7rem}footer.status .dropdown summary,footer.status .anim-toggle{padding: 0.55rem 0.4rem}.status-left{color: var(--muted-foreground);min-width: 0;overflow: hidden;text-overflow: ellipsis;white-space: nowrap}.status-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}.sep{width: 1px;height: 0.9rem;background: var(--border)}.doc-link{color: var(--muted-foreground);text-decoration: none;transition: color 0.15s ease}.doc-link:hover{color: var(--primary)}.doc-link--forward{color: var(--primary);white-space: nowrap}.doc-link--forward:hover{color: var(--primary);text-decoration: underline}.ls-group{display: block;padding: 0.55rem 0 0.15rem;font-size: var(--fs-xs);letter-spacing: 0.08em;color: var(--muted-foreground);opacity: 0.75}.ls-panel>li:first-child .ls-group{padding-top: 0}.ls-row{display: flex;align-items: baseline;gap: 0.9rem}.ls-perm{color: color-mix(in srgb,var(--muted-foreground) 75%,var(--card));font-size: 0.72rem;letter-spacing: 0}.ls-row--sub .ls-name{margin-left: 0.55rem;padding-left: 0.75rem;border-left: 1px solid color-mix(in srgb,var(--muted-foreground) 40%,transparent)}.ls-row--sub2 .ls-name{margin-left: 1.7rem}.ls-panel .ls-name{color: var(--muted-foreground)}.ls-panel .ls-row--dir .ls-name{color: var(--primary);font-weight: 700}.ls-row[aria-current="page"]{background: color-mix(in srgb,var(--primary) 12%,transparent);box-shadow: inset 3px 0 0 0 var(--primary)}.ls-row[aria-current="page"] .ls-name{color: var(--primary);font-weight: 700;text-shadow: 0 0 8px var(--glow)}.ls-row[aria-current="page"] .ls-perm{color: color-mix(in srgb,var(--primary) 65%,transparent)}.ls-row[aria-current="page"]::after{content: "←";margin-left: auto;color: var(--primary)}.dropdown-panel.ls-panel{min-width: 17rem;left: auto !important;right: 0}:root{--ls-nav-w: 17rem;--ls-nav-top: 3rem;--ls-nav-bottom: 2.2rem}@media (min-width: 48.0625rem){html{--ls-nav-inset: 0rem}html:has(.ls-nav):not([data-ls-nav="off"]){--ls-nav-inset: var(--ls-nav-w)}body{padding-right: var(--ls-nav-inset)}header.bar,.bleed-rail{margin-right: calc(-1 * var(--ls-nav-inset))}.ls-nav{position: fixed;top: var(--ls-nav-top);right: 0;bottom: var(--ls-nav-bottom);width: var(--ls-nav-w);z-index: 25;display: flex;flex-direction: column;overflow: hidden;background: var(--card);border-left: 1px solid var(--border)}html:not(.anim-off) .ls-nav{transition: transform 0.18s ease}html[data-ls-nav="off"] .ls-nav{transform: translateX(100%)}.ls-nav-head{display: inline-flex;align-items: center;gap: 0.5rem;margin-left: 0.75rem}.ls-nav-title{font-weight: 700;font-size: 0.9rem}.ls-nav .ls-panel{flex: 1;min-height: 0;overflow-y: auto;list-style: none;margin: 0;padding: 0.5rem 0 1rem}.ls-nav-toggle{background: none;border: 0;font: inherit;cursor: pointer;line-height: 1;color: var(--muted-foreground);padding: 0.4rem 0.55rem}.ls-nav-toggle::after{content: "\00BB"}html[data-ls-nav="off"] .ls-nav-toggle::after{content: "\00AB"}.ls-nav-toggle:hover{color: var(--primary)}.ls-nav-toggle:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}}.nav-burger{display: none;background: none;border: 0;color: var(--muted-foreground);cursor: pointer;padding: 0.55rem;margin: -0.3rem -0.3rem -0.3rem 0}.nav-burger:hover{color: var(--primary)}.mobile-nav{display: none;list-style: none;margin: 0;padding: 0}.mobile-footer{display: none}@media (max-width: 48rem){.nav-burger{display: inline-flex;align-items: center}.site-nav{display: none;position: absolute;top: 100%;left: 0;right: 0;z-index: 40;background: var(--card);border-bottom: 1px solid var(--border);padding: 0.4rem 1.25rem 0.9rem;max-height: calc(100vh - 6rem);overflow-y: auto}.site-nav.open{display: block}.site-nav>.ls-nav,.mobile-nav{display: flex;flex-direction: column}.site-nav:has(>.ls-nav) .mobile-nav{display: none}.site-nav>.ls-nav .ls-row{align-items: center;min-height: 44px;border-bottom: 1px solid var(--border)}.site-nav>.ls-nav .ls-row .ls-name{display: flex;align-items: center;flex: 1;min-height: 44px}.site-nav>.ls-nav .ls-row:last-child{border-bottom: 0}.mobile-nav .mobile-item{display: flex;align-items: baseline;gap: 0.9rem;padding: 0.65rem 0;text-decoration: none;color: var(--foreground);font-size: 0.9rem;border-bottom: 1px solid var(--border)}.mobile-nav li:last-child .mobile-item{border-bottom: 0}.mobile-nav .mobile-item:active .ls-name,.mobile-nav .mobile-item:hover .ls-name{color: var(--primary)}.ls-nav-toggle{display: none}footer.status{display: none}.mobile-footer{display: flex;flex-direction: column;gap: 0.55rem;margin-top: 0.8rem;border-top: 1px solid var(--border);padding-top: 0.8rem;font-size: 0.85rem}.mobile-footer .doc-link{padding: 0.2rem 0}.mobile-footer .mobile-theme summary{list-style: none;display: flex;align-items: center;gap: 0.5rem;color: var(--muted-foreground);cursor: pointer;padding: 0.2rem 0}.mobile-footer .mobile-theme summary::-webkit-details-marker{display: none}.mobile-footer .mf-chev{margin-left: auto;opacity: 0.7}.mobile-footer .mf-panel{display: flex;flex-direction: column;margin-top: 0.3rem}.mobile-footer .mf-panel .dropdown-item{text-align: left;padding: 0.4rem 0.6rem}.mobile-footer .anim-toggle{display: inline-flex;gap: 0.5rem;align-items: center;background: none;border: 0;padding: 0.2rem 0;color: var(--muted-foreground);font: inherit;cursor: pointer}}[data-tip]{cursor: help}span[data-tip],th[data-tip]{border-bottom: 1px dotted var(--muted-foreground)}#ddtip{position: fixed;z-index: 9999;display: none;max-width: 340px;background: var(--background);color: var(--foreground);border: 1px solid var(--muted-foreground);border-radius: var(--radius);padding: 7px 10px;font-size: 0.72rem;line-height: 1.45;font-weight: 400;text-align: left;white-space: normal;pointer-events: none;box-shadow: 0 4px 14px rgba(0,0,0,0.25)}@media print{@page{margin: 14mm 12mm}:root,html,html[data-theme]{--background: #ffffff;--foreground: #111111;--card: #ffffff;--card-foreground: #111111;--popover: #ffffff;--popover-foreground: #111111;--primary: #000000;--primary-foreground: #ffffff;--secondary: #ffffff;--secondary-foreground: #111111;--muted: #ffffff;--muted-foreground: #444444;--accent: #000000;--accent-foreground: #ffffff;--destructive: #7a1c1c;--border: #9a9a9a;--input: #9a9a9a;--ring: #000000;--glow: transparent;--glow-soft: transparent;--scanline-opacity: 0}html{zoom: 1 !important;font-size: 16px;scroll-behavior: auto}body{background: #ffffff;color: var(--foreground);font-size: 12px;line-height: 1.45;overflow: visible}body::after{display: none !important}*,*::before,*::after{animation: none !important;transition: none !important;text-shadow: none !important;box-shadow: none !important}html.term-anim [data-term] [data-term-out],html.term-anim [data-term] [data-term-out]:not(.term-show),html.term-anim [data-term] [data-term-out]>*{visibility: visible !important;opacity: 1 !important}html.term-anim [data-term] .prompt:not(.term-live){color: var(--muted-foreground) !important}html.term-anim [data-term] .prompt:not(.term-live)::before{color: var(--primary) !important}header.bar,footer.status,.site-nav,.nav-burger,.mobile-nav,.mobile-footer,.skip-link,.dropdown,.dropdown-panel,.select-panel,.anim-toggle,.cursor-block,.term-caret,.ascii-rule,.toc,#ddtip{display: none !important}body{padding-right: 0 !important}[data-tip]{cursor: auto}span[data-tip],th[data-tip]{border-bottom: 0}.wrap{max-width: none !important;margin: 0 !important;padding: 0 !important}.layout{display: block !important}.content{min-width: 0}h1,.title{font-size: 1.5rem !important}h2{font-size: 1.15rem !important}h3,.sub{font-size: 1rem !important}h4{font-size: 0.9rem !important}.prompt{font-size: 0.7rem}h1,h2,h3,h4,.prompt,.toc-label{break-after: avoid-page;break-inside: avoid}p,li,blockquote{orphans: 3;widows: 3}pre,figure,blockquote,tr,img,.eli5,.card-terminal{break-inside: avoid}svg{break-inside: auto}thead{display: table-header-group}.tablewrap,.table-scroll,pre,.diagram{overflow: visible !important;max-width: 100% !important}pre{white-space: pre-wrap !important;overflow-wrap: anywhere;font-size: 0.75rem !important;background: transparent !important}pre.mermaid[data-processed]{white-space: normal !important}code{background: transparent !important;overflow-wrap: anywhere}table{width: 100% !important;table-layout: auto;border-collapse: collapse;font-size: 0.75rem !important}th,td{overflow-wrap: break-word;white-space: normal !important}td code,th code,td a,th a{overflow-wrap: anywhere}a{color: inherit;text-decoration: underline;text-underline-offset: 2px}a.card-terminal,a[class*="card"]{text-decoration: none}.glow,.glow-lg{color: inherit}.btn-terminal{border: 1px solid var(--border);background: transparent;color: var(--foreground)}.mermaid svg{display: block;margin-inline: auto;width: auto !important;height: auto !important;max-width: 100% !important;max-height: 235mm !important}.mermaid svg .node rect,.mermaid svg .node polygon,.mermaid svg .node circle,.mermaid svg .node path,.mermaid svg .label-container,.mermaid svg .cluster rect,.mermaid svg .actor,.mermaid svg .note{fill: #ffffff !important;stroke: #333333 !important}.mermaid svg .edgePath path,.mermaid svg .flowchart-link,.mermaid svg line,.mermaid svg .messageLine0,.mermaid svg .messageLine1{stroke: #333333 !important}.mermaid svg .arrowheadPath,.mermaid svg marker path,.mermaid svg marker polygon{fill: #333333 !important;stroke: #333333 !important}.mermaid svg text,.mermaid svg .nodeLabel,.mermaid svg .edgeLabel,.mermaid svg .cluster-label,.mermaid svg .messageText,.mermaid svg foreignObject div,.mermaid svg foreignObject span,.mermaid svg p{fill: #111111 !important;color: #111111 !important;background: transparent !important}.mermaid svg .edgeLabel rect,.mermaid svg .edgeLabel foreignObject>div{fill: #ffffff !important;background: #ffffff !important}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieldeusing/design",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "The danieldeusing terminal design system — framework-agnostic CSS tokens, components, and a vanilla-JS runtime for the CRT/JetBrains-Mono look used across danieldeusing.de, seedr, and briefs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/runtime/index.js CHANGED
@@ -13,6 +13,7 @@
13
13
  export * from "./theme.js";
14
14
  export * from "./zoom.js";
15
15
  export * from "./dropdown.js";
16
+ export * from "./select.js";
16
17
  export * from "./anim.js";
17
18
  export * from "./terminal.js";
18
19
  export * from "./nav.js";
@@ -0,0 +1,539 @@
1
+ /*
2
+ * select.js — replaces the OPERATING SYSTEM's dropdown with the estate's own.
3
+ *
4
+ * Markup contract:
5
+ * <select>…</select>
6
+ *
7
+ * That is the whole contract. Like `initTableScroll()`, this takes plain HTML
8
+ * and needs no classes, no wrapper and no data attributes. Call `initSelects()`
9
+ * once; every `<select>` on the page is enhanced, and so is every one rendered
10
+ * afterwards — cockpit rebuilds its config tables out of innerHTML on every
11
+ * poll, so a widget that only enhanced what existed at load would work until
12
+ * the first refresh and then quietly stop.
13
+ *
14
+ * WHY A REPLACEMENT AND NOT CSS. A `<select>`'s option list is painted by the
15
+ * OS outside the document: rounded corners, a blue system highlight, the system
16
+ * font. No stylesheet reaches it. Chrome 135+ can style it with
17
+ * `appearance: base-select`, but Safari and Firefox cannot, and a fix that
18
+ * lands on one browser leaves the estate disagreeing with ITSELF, which is
19
+ * worse than being consistently wrong. So the list is rebuilt in the page.
20
+ *
21
+ * THE <select> STAYS AND STAYS AUTHORITATIVE. It is not cloned, mirrored or
22
+ * replaced by hidden inputs: it remains the element that holds the value, that
23
+ * a form submits, that `select.value` reads, and that emits `input`/`change`.
24
+ * Page code sees exactly what it saw before — that is what let 28 call sites
25
+ * adopt this without a single edit. It is laid transparently OVER the trigger
26
+ * rather than `display: none`, because Chrome refuses to show a validation
27
+ * bubble on an unfocusable control and then blocks the submit with no message
28
+ * at all, which would silently break every `required` select.
29
+ *
30
+ * Keyboard follows the ARIA APG select-only combobox: Enter/Space/Arrow open,
31
+ * Up/Down move, Home/End jump, printable characters type ahead (a repeated
32
+ * character cycles, as a native select does), Enter selects, Escape closes
33
+ * without changing anything, Tab moves on. Focus never leaves the trigger —
34
+ * the active option is pointed at with `aria-activedescendant` — so there is
35
+ * nowhere for it to get stuck.
36
+ */
37
+
38
+ const TYPEAHEAD_MS = 700;
39
+ const GAP = 4; // visual px between the trigger and the panel
40
+ const EDGE = 8; // keep the panel this far off the viewport edge
41
+
42
+ const enhanced = new WeakMap();
43
+ let counter = 0;
44
+ let openInstance = null;
45
+ let documentObserver = null;
46
+ let globalsInstalled = false;
47
+
48
+ /*
49
+ * Consumers set `zoom` on <html> (initResolutionZoom lays every page out
50
+ * against a 1920px reference), and that puts the two halves of any positioning
51
+ * sum in different coordinate spaces: getBoundingClientRect() and
52
+ * innerWidth/innerHeight are VISUAL px, already multiplied, while style.left is
53
+ * a CSS length the browser multiplies AGAIN on the way out. Writing a rect
54
+ * straight into a length therefore applies the zoom twice — an error that grows
55
+ * with distance from the origin, which is how it survives review (it looks fine
56
+ * near the top left). Fixed twice before in this runtime: tooltip.js and
57
+ * lsnav.js. Divide on the WRITE; never "fix" a comparison whose operands are
58
+ * both already visual.
59
+ */
60
+ const zoomOf = () => Number(getComputedStyle(document.documentElement).zoom) || 1;
61
+
62
+ const optionsOf = (instance) => instance.select.options;
63
+ const label = (element) => (element.textContent || "").trim();
64
+
65
+ const firstEnabled = (instance) => {
66
+ const options = optionsOf(instance);
67
+ for (let i = 0; i < options.length; i += 1) if (!options[i].disabled) return i;
68
+ return -1;
69
+ };
70
+ const lastEnabled = (instance) => {
71
+ const options = optionsOf(instance);
72
+ for (let i = options.length - 1; i >= 0; i -= 1) if (!options[i].disabled) return i;
73
+ return -1;
74
+ };
75
+
76
+ /* ── the closed control ─────────────────────────────────────────────────── */
77
+
78
+ function syncTrigger(instance) {
79
+ const { select, trigger, value } = instance;
80
+ const option = select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null;
81
+ value.textContent = option ? label(option) : "";
82
+ trigger.disabled = select.disabled;
83
+ }
84
+
85
+ /*
86
+ * The trigger's accessible name is the LABEL plus the CURRENT VALUE — "lines,
87
+ * 200" — which is what a native select announces and what the APG's select-only
88
+ * combobox prescribes. Pointing `aria-labelledby` at the trigger's own id is
89
+ * how the value gets into the name; it looks like a mistake and is the pattern.
90
+ *
91
+ * The label is found the same three ways the platform finds it, in the platform's
92
+ * order, so a page that already labels its select correctly needs no change:
93
+ * an explicit aria-label, an explicit aria-labelledby, then a <label> — whether
94
+ * associated by `for=` or by wrapping.
95
+ */
96
+ function nameTrigger(instance) {
97
+ const { select, trigger } = instance;
98
+ const explicit = select.getAttribute("aria-label");
99
+ if (explicit) {
100
+ trigger.setAttribute("aria-label", explicit);
101
+ return;
102
+ }
103
+ let labelId = select.getAttribute("aria-labelledby");
104
+ if (!labelId) {
105
+ const element =
106
+ (select.id && document.querySelector(`label[for="${CSS.escape(select.id)}"]`)) ||
107
+ select.closest("label");
108
+ if (element) {
109
+ if (!element.id) element.id = `${instance.id}-label`;
110
+ labelId = element.id;
111
+ }
112
+ }
113
+ if (labelId) trigger.setAttribute("aria-labelledby", `${labelId} ${trigger.id}`);
114
+ }
115
+
116
+ function enhance(select) {
117
+ if (enhanced.has(select)) return;
118
+ // `multiple` and `size > 1` are not popups — the platform renders them inline
119
+ // and there is no OS menu to replace. `data-select="off"` is the opt-out.
120
+ if (select.multiple || select.size > 1) return;
121
+ if (select.dataset.select === "off") return;
122
+ if (select.parentElement?.classList.contains("select-field")) return;
123
+ if (!select.parentNode) return;
124
+
125
+ counter += 1;
126
+ const id = `dd-select-${counter}`;
127
+
128
+ const field = document.createElement("span");
129
+ field.className = "select-field";
130
+ // A page sizes its control on the <select> (`style="max-width:18rem"`), and
131
+ // once the select is out of the flow that sizing has nothing to act on. The
132
+ // wrapper is what occupies the space now, so it takes the inline style. Copied,
133
+ // not moved: the select's own style attribute is still the page's to read.
134
+ const inlineStyle = select.getAttribute("style");
135
+ if (inlineStyle) field.setAttribute("style", inlineStyle);
136
+
137
+ const trigger = document.createElement("button");
138
+ trigger.type = "button";
139
+ trigger.className = "select-trigger";
140
+ trigger.id = `${id}-trigger`;
141
+ trigger.setAttribute("role", "combobox");
142
+ trigger.setAttribute("aria-haspopup", "listbox");
143
+ trigger.setAttribute("aria-expanded", "false");
144
+ trigger.setAttribute("aria-controls", `${id}-panel`);
145
+
146
+ const value = document.createElement("span");
147
+ value.className = "select-value";
148
+ trigger.appendChild(value);
149
+
150
+ // A tooltip anchored to a control nobody can hover is a tooltip that never
151
+ // shows. Both flavours move to the visible element; the select keeps its copy
152
+ // so page code that reads the attribute still finds it.
153
+ for (const attribute of ["title", "data-tip"]) {
154
+ const text = select.getAttribute(attribute);
155
+ if (text !== null) trigger.setAttribute(attribute, text);
156
+ }
157
+
158
+ select.parentNode.insertBefore(field, select);
159
+ field.appendChild(select);
160
+ field.appendChild(trigger);
161
+ select.setAttribute("tabindex", "-1");
162
+ select.setAttribute("aria-hidden", "true");
163
+
164
+ const instance = { select, field, trigger, value, id, panel: null, items: [], active: -1, typed: "", typedAt: 0 };
165
+ enhanced.set(select, instance);
166
+ nameTrigger(instance);
167
+ syncTrigger(instance);
168
+
169
+ trigger.addEventListener("click", () => (instance.panel ? close(instance, true) : open(instance)));
170
+ trigger.addEventListener("keydown", (event) => onKeydown(instance, event));
171
+ // Focus aimed at the hidden control — a <label> click, or page code calling
172
+ // select.focus() — belongs to the one the reader can see.
173
+ select.addEventListener("focus", () => trigger.focus());
174
+ // A page that sets the value itself and announces it the normal way is honoured.
175
+ // Our own dispatch lands here too; re-syncing an already-synced trigger is a no-op.
176
+ select.addEventListener("change", () => syncTrigger(instance));
177
+
178
+ // Cockpit rewrites a select's options from fetched data — a new model list, a
179
+ // new credential list, a filter column derived from the rows that just arrived.
180
+ // Without this the trigger would keep showing the label of an option that is no
181
+ // longer in the list, which reads as the page having lost the setting.
182
+ instance.observer = new MutationObserver(() => {
183
+ syncTrigger(instance);
184
+ if (instance.panel) {
185
+ // The list changed under an open panel. Rebuild it rather than show a stale
186
+ // one; focus is on the trigger throughout, so nothing is disturbed.
187
+ close(instance, false);
188
+ open(instance);
189
+ }
190
+ });
191
+ instance.observer.observe(select, {
192
+ childList: true,
193
+ subtree: true,
194
+ characterData: true,
195
+ attributes: true,
196
+ attributeFilter: ["disabled", "selected", "value", "label"],
197
+ });
198
+ }
199
+
200
+ /* ── the panel ──────────────────────────────────────────────────────────── */
201
+
202
+ function buildPanel(instance) {
203
+ const panel = document.createElement("ul");
204
+ panel.className = "select-panel";
205
+ panel.id = `${instance.id}-panel`;
206
+ panel.setAttribute("role", "listbox");
207
+ panel.tabIndex = -1;
208
+
209
+ instance.items = [];
210
+ let index = 0;
211
+ const addOption = (option) => {
212
+ const item = document.createElement("li");
213
+ item.className = "select-option";
214
+ item.id = `${instance.id}-o${index}`;
215
+ item.setAttribute("role", "option");
216
+ item.setAttribute("aria-selected", String(index === instance.select.selectedIndex));
217
+ if (option.disabled) item.setAttribute("aria-disabled", "true");
218
+ item.textContent = label(option);
219
+ item.dataset.index = String(index);
220
+ instance.items[index] = item;
221
+ index += 1;
222
+ return item;
223
+ };
224
+
225
+ // Walked child by child rather than over `select.options`, so an <optgroup>
226
+ // keeps its heading. The walk order is document order, which is exactly the
227
+ // order `select.options` flattens to — that is what keeps `data-index` a valid
228
+ // index into the real control.
229
+ for (const child of instance.select.children) {
230
+ if (child.tagName === "OPTGROUP") {
231
+ const heading = document.createElement("li");
232
+ heading.className = "select-group";
233
+ heading.setAttribute("role", "presentation");
234
+ heading.textContent = child.label;
235
+ panel.appendChild(heading);
236
+ for (const option of child.children) {
237
+ if (option.tagName === "OPTION") panel.appendChild(addOption(option));
238
+ }
239
+ } else if (child.tagName === "OPTION") {
240
+ panel.appendChild(addOption(child));
241
+ }
242
+ }
243
+ return panel;
244
+ }
245
+
246
+ /*
247
+ * THE PANEL IS BUILT FRESH ON EVERY OPEN, from the select's options as they are
248
+ * at that instant. That is not laziness about caching — it is the only way the
249
+ * list cannot be stale, and staleness is the failure this widget is most likely
250
+ * to have shipped: cockpit replaces a select's options from fetched data all the
251
+ * time, and a snapshot taken at enhance time would show a model list from before
252
+ * the last poll while the value underneath had moved on.
253
+ */
254
+ function open(instance) {
255
+ if (instance.select.disabled) return;
256
+ if (openInstance && openInstance !== instance) close(openInstance, false);
257
+
258
+ const panel = buildPanel(instance);
259
+ instance.panel = panel;
260
+ // Appended to the nearest <dialog> when there is one: a modal dialog is in the
261
+ // top layer and nothing outside it can paint above it, so a panel on <body>
262
+ // would open behind the dialog that owns the select. Everywhere else <body> is
263
+ // right — it escapes `.tablewrap`'s scroll clipping, which is where most of
264
+ // cockpit's selects live.
265
+ (instance.trigger.closest("dialog") || document.body).appendChild(panel);
266
+ instance.trigger.setAttribute("aria-expanded", "true");
267
+ openInstance = instance;
268
+
269
+ // Options are never focusable; the panel is pointed at with
270
+ // aria-activedescendant instead. preventDefault on mousedown is what keeps
271
+ // focus on the trigger when an option is clicked.
272
+ panel.addEventListener("mousedown", (event) => event.preventDefault());
273
+ panel.addEventListener("click", (event) => {
274
+ const item = event.target.closest(".select-option");
275
+ if (item) commit(instance, Number(item.dataset.index));
276
+ });
277
+
278
+ const selected = instance.select.selectedIndex;
279
+ setActive(instance, selected >= 0 && !instance.select.options[selected].disabled ? selected : firstEnabled(instance));
280
+ position(instance);
281
+ }
282
+
283
+ function close(instance, focusTrigger) {
284
+ if (instance.panel) {
285
+ instance.panel.remove();
286
+ instance.panel = null;
287
+ }
288
+ instance.items = [];
289
+ instance.active = -1;
290
+ instance.typed = "";
291
+ instance.trigger.setAttribute("aria-expanded", "false");
292
+ instance.trigger.removeAttribute("aria-activedescendant");
293
+ if (openInstance === instance) openInstance = null;
294
+ if (focusTrigger) instance.trigger.focus();
295
+ }
296
+
297
+ function setActive(instance, index) {
298
+ const previous = instance.items[instance.active];
299
+ if (previous) previous.removeAttribute("data-active");
300
+ instance.active = index;
301
+ const item = instance.items[index];
302
+ if (!item) {
303
+ instance.trigger.removeAttribute("aria-activedescendant");
304
+ return;
305
+ }
306
+ item.setAttribute("data-active", "true");
307
+ instance.trigger.setAttribute("aria-activedescendant", item.id);
308
+ item.scrollIntoView({ block: "nearest" });
309
+ }
310
+
311
+ function commit(instance, index) {
312
+ const option = optionsOf(instance)[index];
313
+ if (!option || option.disabled) return; // a disabled option is not a choice; the list stays open
314
+ const changed = instance.select.selectedIndex !== index;
315
+ if (changed) {
316
+ instance.select.selectedIndex = index;
317
+ syncTrigger(instance);
318
+ }
319
+ // Closed BEFORE the events go out: a `change` handler here re-renders the table
320
+ // this select lives in, so anything touching the instance afterwards would be
321
+ // touching a detached node.
322
+ close(instance, true);
323
+ if (changed) {
324
+ // A native select fires `input` and THEN `change`, and both bubble. Cockpit's
325
+ // table filters listen for both on a container element, so dispatching only
326
+ // `change` would make this a quieter control than the one it replaced.
327
+ instance.select.dispatchEvent(new Event("input", { bubbles: true }));
328
+ instance.select.dispatchEvent(new Event("change", { bubbles: true }));
329
+ }
330
+ }
331
+
332
+ function position(instance) {
333
+ const { trigger, panel } = instance;
334
+ const zoom = zoomOf();
335
+ const rect = trigger.getBoundingClientRect(); // visual px
336
+
337
+ // offsetWidth is a LAYOUT length, already in the same space as the panel's own
338
+ // min-width, so this one must NOT be divided. Mixing the two is the trap.
339
+ panel.style.minWidth = `${trigger.offsetWidth}px`;
340
+ panel.style.maxWidth = `${(window.innerWidth - EDGE * 2) / zoom}px`;
341
+ panel.style.maxHeight = "";
342
+
343
+ const spaceBelow = window.innerHeight - rect.bottom - EDGE - GAP;
344
+ const spaceAbove = rect.top - EDGE - GAP;
345
+ const wanted = panel.getBoundingClientRect().height;
346
+ // Flip only when flipping actually helps. A long list near the bottom of a tall
347
+ // page has room in neither direction, and flipping it there just moves the
348
+ // clipping to the other end.
349
+ const flip = wanted > spaceBelow && spaceAbove > spaceBelow;
350
+ const room = Math.max(flip ? spaceAbove : spaceBelow, 72);
351
+ if (wanted > room) panel.style.maxHeight = `${room / zoom}px`;
352
+
353
+ const box = panel.getBoundingClientRect(); // visual px, after the clamp
354
+ const x = Math.min(Math.max(rect.left, EDGE), Math.max(EDGE, window.innerWidth - box.width - EDGE));
355
+ const y = flip ? rect.top - GAP - box.height : rect.bottom + GAP;
356
+ panel.style.left = `${x / zoom}px`;
357
+ panel.style.top = `${y / zoom}px`;
358
+ }
359
+
360
+ /* ── keyboard ───────────────────────────────────────────────────────────── */
361
+
362
+ const isPrintable = (event) =>
363
+ event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
364
+
365
+ function step(instance, direction) {
366
+ const options = optionsOf(instance);
367
+ for (let i = instance.active + direction; i >= 0 && i < options.length; i += direction) {
368
+ if (!options[i].disabled) {
369
+ setActive(instance, i);
370
+ return;
371
+ }
372
+ }
373
+ }
374
+
375
+ function typeahead(instance, character) {
376
+ const now = Date.now();
377
+ if (now - instance.typedAt > TYPEAHEAD_MS) instance.typed = "";
378
+ instance.typedAt = now;
379
+ instance.typed += character.toLowerCase();
380
+
381
+ // One character repeated CYCLES through the options starting with it, which is
382
+ // what a native select does — pressing "c" three times walks three c-options
383
+ // rather than hunting for "ccc".
384
+ const repeated = instance.typed.length > 1 && new Set(instance.typed).size === 1;
385
+ const needle = repeated ? instance.typed[0] : instance.typed;
386
+ const advance = repeated || instance.typed.length === 1;
387
+ const options = optionsOf(instance);
388
+ const from = advance ? instance.active + 1 : Math.max(instance.active, 0);
389
+ for (let i = 0; i < options.length; i += 1) {
390
+ const index = (from + i + options.length) % options.length;
391
+ if (options[index].disabled) continue;
392
+ if (label(options[index]).toLowerCase().startsWith(needle)) {
393
+ setActive(instance, index);
394
+ return;
395
+ }
396
+ }
397
+ }
398
+
399
+ function onKeydown(instance, event) {
400
+ const isOpen = Boolean(instance.panel);
401
+ const key = event.key;
402
+
403
+ if (key === "Escape") {
404
+ if (!isOpen) return;
405
+ // BOTH, and both matter. preventDefault stops the UA's close-request, and
406
+ // stopPropagation stops the page's own handler: without them, dismissing this
407
+ // list inside a modal <dialog> closes the dialog as well, losing the form.
408
+ event.preventDefault();
409
+ event.stopPropagation();
410
+ close(instance, true);
411
+ return;
412
+ }
413
+ if (key === "Tab") {
414
+ if (isOpen) close(instance, false); // Tab moves on and commits nothing
415
+ return;
416
+ }
417
+
418
+ if (!isOpen) {
419
+ if (key === "Enter" || key === " " || key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End") {
420
+ event.preventDefault();
421
+ open(instance);
422
+ if (key === "Home") setActive(instance, firstEnabled(instance));
423
+ else if (key === "End") setActive(instance, lastEnabled(instance));
424
+ return;
425
+ }
426
+ if (isPrintable(event)) {
427
+ event.preventDefault();
428
+ open(instance);
429
+ typeahead(instance, key);
430
+ }
431
+ return;
432
+ }
433
+
434
+ // A space CONTINUES a live typeahead rather than selecting — option labels here
435
+ // contain spaces ("public — posts on the PR"), so treating it as Enter would
436
+ // make half of them untypeable.
437
+ if (key === " " && instance.typed && Date.now() - instance.typedAt <= TYPEAHEAD_MS) {
438
+ event.preventDefault();
439
+ typeahead(instance, key);
440
+ return;
441
+ }
442
+ switch (key) {
443
+ case "Enter":
444
+ case " ":
445
+ event.preventDefault();
446
+ commit(instance, instance.active);
447
+ return;
448
+ case "ArrowDown":
449
+ event.preventDefault();
450
+ step(instance, 1);
451
+ return;
452
+ case "ArrowUp":
453
+ event.preventDefault();
454
+ step(instance, -1);
455
+ return;
456
+ case "Home":
457
+ event.preventDefault();
458
+ setActive(instance, firstEnabled(instance));
459
+ return;
460
+ case "End":
461
+ event.preventDefault();
462
+ setActive(instance, lastEnabled(instance));
463
+ return;
464
+ default:
465
+ if (isPrintable(event)) {
466
+ event.preventDefault();
467
+ typeahead(instance, key);
468
+ }
469
+ }
470
+ }
471
+
472
+ /* ── document-level wiring, installed once ──────────────────────────────── */
473
+
474
+ function installGlobals() {
475
+ if (globalsInstalled) return;
476
+ globalsInstalled = true;
477
+
478
+ document.addEventListener(
479
+ "pointerdown",
480
+ (event) => {
481
+ if (!openInstance) return;
482
+ const target = event.target;
483
+ if (openInstance.panel.contains(target) || openInstance.field.contains(target)) return;
484
+ close(openInstance, false);
485
+ },
486
+ true,
487
+ );
488
+ document.addEventListener("focusin", (event) => {
489
+ if (!openInstance) return;
490
+ if (openInstance.field.contains(event.target) || openInstance.panel.contains(event.target)) return;
491
+ close(openInstance, false);
492
+ });
493
+ // The trigger moves when the page or a scroll container moves under it. Capture,
494
+ // because most of these selects sit in a `.tablewrap` that scrolls on its own and
495
+ // a scroll event there does not bubble.
496
+ addEventListener("resize", () => openInstance && position(openInstance));
497
+ addEventListener("scroll", () => openInstance && position(openInstance), true);
498
+ // form.reset() rewinds selectedIndex without firing an event or touching the DOM,
499
+ // so nothing else here would notice. One delegated listener rather than one per
500
+ // select: these selects are re-created on every render and the <form> is not, so
501
+ // per-select listeners would pile up on it for the life of the page.
502
+ document.addEventListener("reset", (event) => {
503
+ const form = event.target;
504
+ queueMicrotask(() => {
505
+ for (const select of form.querySelectorAll?.("select") ?? []) {
506
+ const instance = enhanced.get(select);
507
+ if (instance) syncTrigger(instance);
508
+ }
509
+ });
510
+ });
511
+ }
512
+
513
+ /**
514
+ * Replace every native `<select>` dropdown on the page with the design system's
515
+ * own, and keep doing so for selects rendered later.
516
+ *
517
+ * @param {ParentNode} [root=document] Where to look for the initial pass.
518
+ */
519
+ export function initSelects(root = document) {
520
+ installGlobals();
521
+ for (const select of root.querySelectorAll("select")) enhance(select);
522
+
523
+ if (documentObserver) return;
524
+ documentObserver = new MutationObserver((records) => {
525
+ // A re-render can take the open panel's trigger out of the document from
526
+ // underneath it — a background poll rewriting the table it sits in. The panel
527
+ // is on <body>, so it would be left floating over a control that no longer
528
+ // exists.
529
+ if (openInstance && !openInstance.trigger.isConnected) close(openInstance, false);
530
+ for (const record of records) {
531
+ for (const node of record.addedNodes) {
532
+ if (node.nodeType !== 1) continue;
533
+ if (node.tagName === "SELECT") enhance(node);
534
+ else for (const select of node.querySelectorAll("select")) enhance(select);
535
+ }
536
+ }
537
+ });
538
+ documentObserver.observe(document.documentElement, { childList: true, subtree: true });
539
+ }
package/src/chrome.css CHANGED
@@ -276,7 +276,38 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
276
276
 
277
277
  /* ── ls-listing rows (shared by the desktop rail + mobile menu) ── */
278
278
  .ls-row { display: flex; align-items: baseline; gap: 0.9rem; }
279
- .ls-perm { color: var(--muted-foreground); font-size: 0.72rem; letter-spacing: 0; }
279
+ /* THE PERMISSION STRING SITS BEHIND THE NAME (0.20.0). Daniel: "the folder info should also
280
+ be in some grey style color so that it does not have the same color as the path/folder
281
+ name." It was `--muted-foreground` flat, and measured across the four themes that was worse
282
+ than it looks in the source:
283
+
284
+ contrast between .ls-perm and the NAME beside it, before
285
+ leaf row: 1.00 1.00 1.00 1.00 ← identical, on EVERY theme
286
+ dir row: 1.20 2.34 3.45 3.00 (warm / green / mono / paper)
287
+
288
+ The leaf case is the severe one and it was invisible in review because the two rules read as
289
+ different declarations: `.ls-perm` and `.ls-panel .ls-name` are both `--muted-foreground`, so
290
+ `drwxr-xr-x` and the page name were literally the same ink and the row had no hierarchy at
291
+ all. Most rows in a rail are leaves. On the DIRECTORY row the warm theme then compounds it —
292
+ `--muted-foreground` #71614e beside `--primary` #8a4516 is a brown-grey next to a brown, 1.2:1,
293
+ which the eye reads as one colour even though the hexes differ.
294
+
295
+ Mixed toward `--card`, the surface the panel is actually drawn on, rather than swapped for a
296
+ different token: the point is to push it BACK, and no token in the set means "quieter than
297
+ muted". 75% is the dimmest mix that still clears 3:1 against the panel on all four themes —
298
+ warm is the binding one at 3.02 — because this string is not decoration. A trailing slash plus
299
+ `drwxr-xr-x` is what says the thing has contents, so it has to stay readable, just not compete.
300
+
301
+ after: legibility vs panel 3.02 3.91 3.63 3.55
302
+ vs leaf name 1.60 1.56 1.55 1.75
303
+ vs dir name 1.92 3.67 5.35 5.24
304
+
305
+ Left at plain `.ls-perm` specificity on purpose: the you-are-here rule below tints this same
306
+ element toward `--primary`, and it must keep winning. */
307
+ .ls-perm {
308
+ color: color-mix(in srgb, var(--muted-foreground) 75%, var(--card));
309
+ font-size: 0.72rem; letter-spacing: 0;
310
+ }
280
311
 
281
312
  /* DEPTH IS DRAWN, NOT JUST INDENTED. Whitespace alone did not carry it — with the
282
313
  permission column in front of every row, a child sat only ~1rem right of its
@@ -362,6 +362,207 @@ html[data-theme="warm"] .dropdown-item[data-theme-value="warm"] {
362
362
  text-shadow: 0 0 8px var(--glow);
363
363
  }
364
364
 
365
+ /* ─────────────────────────────────────────────────────────────────────────
366
+ Select — the estate's own dropdown, because a native one's list CANNOT be
367
+ styled. Pairs with runtime/select.js.
368
+
369
+ THE CONSTRAINT THAT DECIDES EVERYTHING HERE: a `<select>`'s option list is
370
+ drawn by the OPERATING SYSTEM, outside the page, and no stylesheet reaches
371
+ it. So every surface in the estate has been rendering a rounded, blue-
372
+ highlighted, system-font menu out of a terminal UI — the one component that
373
+ could not be made to look like the rest, no matter what the page declared.
374
+ `appearance: base-select` would fix it in Chrome 135+ and nowhere else, so
375
+ Safari would keep the system menu: the estate would then be inconsistent
376
+ WITH ITSELF, which is worse than being consistently wrong.
377
+
378
+ So the list is rebuilt in the page. runtime/select.js keeps the real
379
+ `<select>` as the value (it is still what a form submits and what page code
380
+ reads) and paints a button + listbox over it. THE MARKUP CONTRACT IS
381
+ NOTHING: author a plain `<select>`, exactly as with `<table>`.
382
+
383
+ TWO RENDERINGS, ONE DECLARATION. The bare `<select>` (JS off, or before the
384
+ runtime has run) and the `.select-trigger` that replaces it must be the same
385
+ control at the same size — a reader who tabs into a page mid-enhancement
386
+ must not see the box change shape. Splitting their look across two rules is
387
+ how they would drift, so the closed control is declared ONCE for both, by
388
+ ELEMENT and by class together. Five copies of `.cfg-sel` in cockpit are what
389
+ this replaces; none of them agreed about `:disabled`.
390
+
391
+ THE BORDER IS 60% OF --foreground, MEASURED, NOT --border. A control's edge
392
+ is the thing that says "this is a control" and WCAG 1.4.11 wants 3:1 for
393
+ that; `--border` is a CONTAINER hairline and measures 1.37 / 2.00 / 1.61 /
394
+ 1.42 against `--background` on the four themes — invisible as a control
395
+ edge, and correct for the cards it was made for. The estate's hand-rolled
396
+ selects used `currentColor 30%`, which is 1.70 at worst. 60% is the first
397
+ step that clears 3:1 on all four themes against ALL THREE surfaces a control
398
+ can land on (worst: warm 3.24 over --background, 3.24 over --muted). Mixed
399
+ with `transparent` rather than with a surface so it composites correctly
400
+ wherever it is dropped, and derived from a token so it stays per-theme.
401
+
402
+ THE CARET IS TWO GRADIENT HALVES, not an SVG. It has to be `currentColor` —
403
+ a data-URI with a baked fill would need declaring four times or be wrong on
404
+ two themes (the luminance gap in tokens.css), and a `::after` is impossible
405
+ because a `<select>` cannot have generated content. Two abutting squares,
406
+ each half-filled on the diagonal, draw the same ▾ on both renderings.
407
+ ───────────────────────────────────────────────────────────────────────── */
408
+ select,
409
+ .select-trigger {
410
+ appearance: none;
411
+ -webkit-appearance: none;
412
+ max-width: 100%;
413
+ padding: 0.28rem 1.45rem 0.28rem 0.5rem;
414
+ font: inherit;
415
+ font-size: var(--fs-sm);
416
+ line-height: var(--lh-tight);
417
+ text-align: left;
418
+ color: var(--foreground);
419
+ background-color: transparent;
420
+ background-image:
421
+ linear-gradient(45deg, transparent 50%, currentColor 50%),
422
+ linear-gradient(135deg, currentColor 50%, transparent 50%);
423
+ background-position:
424
+ right 0.78rem center,
425
+ right 0.5rem center;
426
+ background-size: 0.28rem 0.28rem;
427
+ background-repeat: no-repeat;
428
+ border: 1px solid color-mix(in srgb, var(--foreground) 60%, transparent);
429
+ cursor: pointer;
430
+ transition: border-color 0.15s ease;
431
+ }
432
+ select:hover,
433
+ .select-trigger:hover,
434
+ .select-trigger[aria-expanded="true"] {
435
+ border-color: var(--primary);
436
+ }
437
+ select:disabled,
438
+ .select-trigger:disabled {
439
+ opacity: 0.45;
440
+ cursor: default;
441
+ }
442
+ /* Open: the caret turns over, so the control says "the list is out" without
443
+ relying on the panel being in view — it may have flipped above the trigger. */
444
+ .select-trigger[aria-expanded="true"] {
445
+ background-image:
446
+ linear-gradient(135deg, transparent 50%, currentColor 50%),
447
+ linear-gradient(45deg, currentColor 50%, transparent 50%);
448
+ }
449
+
450
+ /* The wrapper the runtime puts around the pair. It is `relative` so the real
451
+ `<select>` can be laid exactly over the trigger: NOT `display: none`, and not
452
+ `visibility: hidden` either — Chrome refuses to show a validation bubble on a
453
+ control it cannot focus and then blocks the submit with no message at all, so
454
+ a `required` select would silently stop working. Transparent and over the
455
+ trigger, the bubble still points at the right box. */
456
+ .select-field {
457
+ position: relative;
458
+ display: inline-flex;
459
+ max-width: 100%;
460
+ vertical-align: middle;
461
+ }
462
+ .select-field > select {
463
+ position: absolute;
464
+ inset: 0;
465
+ width: 100%;
466
+ height: 100%;
467
+ opacity: 0;
468
+ pointer-events: none;
469
+ }
470
+ .select-trigger {
471
+ display: inline-flex;
472
+ align-items: center;
473
+ width: 100%;
474
+ }
475
+ .select-value {
476
+ /* min-width: 0 is what lets a flex item shrink below its content — without it
477
+ `text-overflow: ellipsis` never fires and a long option label blows the
478
+ control out of its column instead of being clipped. */
479
+ min-width: 0;
480
+ overflow: hidden;
481
+ text-overflow: ellipsis;
482
+ white-space: nowrap;
483
+ }
484
+ /* An empty flex item has no line box, so a select whose current option has no
485
+ text (`<option value="">`) would render a control HALF THE HEIGHT of the one
486
+ beside it. A zero-width space gives it a line and no width, and screen readers
487
+ ignore it — unlike an `&nbsp;`, which they read out. */
488
+ .select-value::after {
489
+ content: "\200b";
490
+ }
491
+
492
+ /* THE PANEL IS position: fixed AND APPENDED OUT OF THE FLOW, for two reasons
493
+ this estate hits constantly: selects live inside `.tablewrap` (which scrolls,
494
+ so it clips) and inside modal `<dialog>`s (a top layer, which nothing outside
495
+ can paint above). An absolutely-positioned panel would be cut off in the
496
+ first and hidden behind the second. runtime/select.js appends it to the
497
+ nearest open <dialog> when there is one and to <body> otherwise.
498
+ z-index 60 clears the scanline overlay (40), .dropdown-panel (50) and
499
+ footer.status (50). */
500
+ .select-panel {
501
+ position: fixed;
502
+ z-index: 60;
503
+ margin: 0;
504
+ padding: 0.15rem 0;
505
+ overflow-y: auto;
506
+ overscroll-behavior: contain;
507
+ list-style: none;
508
+ font-size: var(--fs-sm);
509
+ line-height: var(--lh-tight);
510
+ color: var(--popover-foreground);
511
+ background: var(--popover);
512
+ border: 1px solid color-mix(in srgb, var(--foreground) 60%, transparent);
513
+ box-shadow: 0 0 20px var(--glow-soft);
514
+ }
515
+
516
+ /* SELECTION CANNOT BE A COLOUR HERE, and that is measured, not felt. `--primary`
517
+ against `--popover-foreground` — the selected option's ink against its
518
+ neighbours' — is 1.65:1 on warm, 1.31 on green, 1.48 on mono, 1.27 on paper.
519
+ Two inks a reader cannot tell apart is not a marking. Same finding as the
520
+ rail's current row (0.19.0), same answer: POSITION AND AREA. The selected
521
+ option is the only one with a left edge marker, and it is bold; the colour is
522
+ the third signal, not the only one.
523
+
524
+ The keyboard's active option and the mouse's hover are the SAME state on
525
+ purpose — there is one pointer of attention, and showing two would ask the
526
+ reader which one Enter is going to take. */
527
+ .select-option {
528
+ display: flex;
529
+ align-items: center;
530
+ gap: 0.4rem;
531
+ padding: 0.28rem 0.9rem 0.28rem 0.55rem;
532
+ border-inline-start: 2px solid transparent;
533
+ white-space: nowrap;
534
+ cursor: pointer;
535
+ }
536
+ .select-option:hover,
537
+ .select-option[data-active="true"] {
538
+ background: var(--muted);
539
+ }
540
+ .select-option[aria-selected="true"] {
541
+ color: var(--primary);
542
+ font-weight: 700;
543
+ border-inline-start-color: var(--primary);
544
+ }
545
+ /* 65% of the ink, not 45%: below that warm lands at 2.94:1 and the option stops
546
+ being readable at all, and "unavailable" still has to be legible to be
547
+ understood as a choice that exists. 65% measures 3.75 at worst (warm). */
548
+ .select-option[aria-disabled="true"] {
549
+ color: color-mix(in srgb, var(--popover-foreground) 65%, var(--popover));
550
+ cursor: default;
551
+ }
552
+ .select-option[aria-disabled="true"]:hover {
553
+ background: none;
554
+ }
555
+ /* <optgroup> label. Nothing in the estate uses one today; it is here because a
556
+ listbox that silently drops the grouping a consumer wrote is worse than one
557
+ that never supported it. */
558
+ .select-group {
559
+ padding: 0.35rem 0.9rem 0.15rem;
560
+ color: var(--muted-foreground);
561
+ font-size: var(--fs-xs);
562
+ text-transform: uppercase;
563
+ letter-spacing: 0.06em;
564
+ }
565
+
365
566
  /* ─────────────────────────────────────────────────────────────────────────
366
567
  ELI5 — an "explain like I'm 5" callout box (first-time-term explainers).
367
568
  ───────────────────────────────────────────────────────────────────────── */
package/src/print.css CHANGED
@@ -122,6 +122,7 @@
122
122
  .skip-link,
123
123
  .dropdown,
124
124
  .dropdown-panel,
125
+ .select-panel,
125
126
  .anim-toggle,
126
127
  .cursor-block,
127
128
  .term-caret,
@@ -98,13 +98,13 @@
98
98
  on docs.danieldeusing.de, NOT for a doc opened from disk over file://. -->
99
99
  <link
100
100
  rel="stylesheet"
101
- href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.19.0/dist/danieldeusing-design.min.css"
102
- onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.19.0.min.css'"
101
+ href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.21.0/dist/danieldeusing-design.min.css"
102
+ onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.21.0.min.css'"
103
103
  />
104
104
  <link
105
105
  rel="stylesheet"
106
- href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.19.0/src/fonts.css"
107
- onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.19.0.fonts.css'"
106
+ href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.21.0/src/fonts.css"
107
+ onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.21.0.fonts.css'"
108
108
  />
109
109
 
110
110
  <style>
@@ -340,8 +340,8 @@ flowchart TD
340
340
  </footer>
341
341
 
342
342
  <script type="module">
343
- import { applyStoredTheme, initThemeSwitcher, initDropdowns, initTerminal, initAnimToggle, initBurgerNav, initLsNav, initTableScroll, initDiagramZoom, initMinimap, initTooltips } from
344
- "https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.19.0/runtime/index.js";
343
+ import { applyStoredTheme, initThemeSwitcher, initDropdowns, initSelects, initTerminal, initAnimToggle, initBurgerNav, initLsNav, initTableScroll, initDiagramZoom, initMinimap, initTooltips } from
344
+ "https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.21.0/runtime/index.js";
345
345
  applyStoredTheme();
346
346
  initThemeSwitcher();
347
347
  initDropdowns();
@@ -354,6 +354,11 @@ flowchart TD
354
354
  // Every table gets a .tablewrap parent, so a table wider than its column scrolls
355
355
  // itself instead of pushing the whole page sideways. Already-wrapped ones are skipped.
356
356
  initTableScroll();
357
+ // Every <select> gets the estate's dropdown instead of the OS one. Called
358
+ // unconditionally, beside initTableScroll() and for the same reason: both have no
359
+ // markup contract, so both are a no-op on a page that has none of their element —
360
+ // and neither can be forgotten on the day a page grows one.
361
+ initSelects();
357
362
 
358
363
  // Mermaid diagrams — themed from the design tokens, re-rendered on theme switch.
359
364
  // Delete this block (and the pre.mermaid CSS) if the page has no diagrams.
@@ -48,7 +48,7 @@
48
48
  jumps. The runtime export exists for surfaces that genuinely cannot inline.
49
49
  2. At the end of <body>, import and call the runtime:
50
50
  import { initLsNav, initBurgerNav, initThemeSwitcher, initDropdowns,
51
- initAnimToggle, initTableScroll }
51
+ initAnimToggle, initTableScroll, initSelects }
52
52
  from "@danieldeusing/design/runtime";
53
53
  initBurgerNav() is not optional here: the markup below ships a .nav-burger, and
54
54
  below the 48rem breakpoint that button is the ONLY way to reach the navigation.
@@ -59,6 +59,11 @@
59
59
  4. `initTableScroll()` wraps every table so a wide one scrolls itself rather than
60
60
  scrolling the whole page sideways. The markup contract is nothing — author a
61
61
  plain <table>. Call it again after rendering more rows.
62
+ 5. `initSelects()` replaces the OS dropdown on every <select> with the estate's own —
63
+ the one component CSS alone can never fix, because the option list is drawn outside
64
+ the page. The markup contract is nothing here either: author a plain <select>, and
65
+ it stays the element that holds the value and fires input/change. Selects rendered
66
+ later are picked up on their own, so there is nothing to call again.
62
67
  -->
63
68
 
64
69
  <!-- ─────────────── <head> ─────────────── -->