@danieldeusing/design 0.28.1 → 0.30.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
@@ -112,13 +112,13 @@ Four dependency-free ES modules, tree-shakeable from `@danieldeusing/design/runt
112
112
  | `initDropdowns()` | `<details class="dropdown">` behaviour: one-open, click-away, Escape. |
113
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. |
114
114
  | `initTablePagination()` | Page every `<table data-table-id>` to 20 rows, with a 5/10/20/50/100/200 picker remembered per table. It has no sort and no filter — it hides all but one window of the rows a page has **already** filtered and sorted, so the order is always filter → sort → slice over the full set. A table without a `data-table-id` is left alone. |
115
+ | `initTableTools()` | Give every `<table data-table-tools>` a search box, per-column sort and filter controls in its header, and a bar naming whatever is in force. Markup contract: `<th data-col="key">`. Optional `data-filter="pick"` for a value list built from the column's own cells, `data-sort-type="num"`, and `data-value` on a `<td>` to sort by something it does not print. The view is remembered per table. |
115
116
  | `initAnimToggle()` | Wire `[data-anim-toggle]` buttons (`.anim-toggle`) to flip `html.anim-off` + persist it. |
116
- | `initResolutionZoom(1920)` | Scale the whole layout up on screens wider than the reference width. |
117
+ | `initResolutionZoom(1920)` | **Deprecated since 0.29.0 — a no-op.** Scaling above 1920 is CSS now (the fluid root font size in `tokens.css`), so it needs no script. Kept exported so a surface can bump its pin without editing its `<head>` in the same commit. Delete the call, and any inline pre-paint zoom block with it: a page that still sets `style.zoom` on top of 0.29.0 scales twice. |
117
118
 
118
119
  ```js
119
- import { applyStoredTheme, initThemeSwitcher, initDropdowns, initSelects, initTablePagination, initTerminal, initAnimToggle, initResolutionZoom } from "@danieldeusing/design/runtime";
120
+ import { applyStoredTheme, initThemeSwitcher, initDropdowns, initSelects, initTablePagination, initTerminal, initAnimToggle } from "@danieldeusing/design/runtime";
120
121
  applyStoredTheme(); // ideally inline, pre-paint
121
- initResolutionZoom(); // ideally inline, pre-paint (no flash)
122
122
  initThemeSwitcher();
123
123
  initDropdowns();
124
124
  initSelects();
@@ -1,4 +1,4 @@
1
- /*! danieldeusing-design v0.28.1 | MIT | https://github.com/danieldeusing/danieldeusing-design */
1
+ /*! danieldeusing-design v0.30.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
2
2
  /* ===== reset.css ===== */
3
3
  /*
4
4
  * danieldeusing-design — minimal reset.
@@ -287,6 +287,38 @@ html[data-theme="paper"] {
287
287
  A page whose content genuinely is not a column (a full-bleed dashboard
288
288
  table) sets `max-width: none` deliberately; it does not pick another number. */
289
289
  --content-w: 78rem;
290
+
291
+ /* ── SCALE ABOVE THE REFERENCE WIDTH ───────────────────────────────────────
292
+ Everything in this package is stated in rem, so moving the root font size
293
+ moves the type, the content column and the vertical rhythm together. That
294
+ is how a 4K or 8K panel gets a bigger page rather than a postage stamp of
295
+ 12px text in the middle of it.
296
+
297
+ WHY NOT `zoom`, WHICH THIS REPLACES. `initResolutionZoom()` did the same
298
+ arithmetic geometrically, and a geometric scale gives the page a pixel grid
299
+ that no longer matches the browser's. Anything injected into the document
300
+ from outside — a password manager's dropdown, a translation bar — reads a
301
+ field's position through the BROWSER's grid and writes it back into the
302
+ PAGE's grid, where it is multiplied by the scale factor. Measured on the
303
+ family login page: 1Password's dropdown landed 1.9x down and across from
304
+ the field it belonged to, on a window that zoomed 1.89x. Scaling the unit
305
+ instead of the space has no second grid, so there is nothing to mismatch.
306
+
307
+ THE ARITHMETIC IS DELIBERATELY THE OLD ONE. Above 1920 this reduces to
308
+ 100vw/120, i.e. exactly `16px * innerWidth/1920` — the same curve
309
+ initResolutionZoom drew, so no surface changes size on the day it migrates.
310
+ Below 1920 the calc goes negative and max() holds it at 1rem, matching the
311
+ old `Math.max(1, ...)` floor.
312
+
313
+ THE `1rem` TERM IS LOAD-BEARING, not decoration. On the root element `rem`
314
+ resolves against the browser's default font size, so a reader who has set
315
+ a larger default still gets it honoured and browser text zoom keeps working
316
+ (WCAG 1.4.4). A bare `100vw/120` would be pixel-exact and quietly override
317
+ both.
318
+
319
+ This lives in tokens.css rather than base.css because netmon loads
320
+ tokens.css + its own frozen chrome.css and never sees base.css. */
321
+ font-size: max(1rem, calc(1rem + (100vw - 1920px) / 120));
290
322
  /* Horizontal breathing room, i.e. Daniel's "same margins left and right".
291
323
  Vertical padding stays a page's own business — a doc and a dashboard do
292
324
  not want the same headroom, and pretending otherwise is what produced the
@@ -1588,6 +1620,125 @@ html.dgm-locked, html.dgm-locked body { overflow: hidden; }
1588
1620
  /* The strip is navigation for a screen. On paper the page IS the map. */
1589
1621
  @media print { .minimap { display: none !important; } }
1590
1622
 
1623
+ /* ── TABLE TOOLS: search on top, sort + filter in the header ──────────────────
1624
+ Paired with runtime/tabletools.js. The estate grew four generations of table
1625
+ filtering before this existed — cockpit's `tr.act-filters`, cockpit's older
1626
+ `tr.filters`, the family contacts table's bare box row — and each spent a
1627
+ whole row of vertical space advertising a capability idle on most visits.
1628
+
1629
+ Two controls in the <th> cost nothing when unused and sit on the column they
1630
+ act on. The filter is a `details.dropdown`, the SYSTEM's dropdown, so
1631
+ one-open / click-away / Escape come from initDropdowns() and are not
1632
+ reimplemented here. Only the parts that are genuinely table-specific are
1633
+ declared below. */
1634
+ .tbl-toolbar {
1635
+ display: flex;
1636
+ align-items: center;
1637
+ gap: 0.5rem;
1638
+ margin-block-end: 0.5rem;
1639
+ }
1640
+ .tbl-search {
1641
+ flex: 1 1 auto;
1642
+ min-inline-size: 0;
1643
+ font: inherit;
1644
+ font-size: var(--fs-base);
1645
+ color: var(--foreground);
1646
+ background: var(--background);
1647
+ border: 1px solid var(--border);
1648
+ padding: 0.25rem 0.5rem;
1649
+ }
1650
+ .tbl-search:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
1651
+
1652
+ /* The header cell keeps its label on the baseline and parks the controls at its
1653
+ trailing edge, so a column of numbers still reads as a column of numbers. */
1654
+ .tbl-tools {
1655
+ display: inline-flex;
1656
+ align-items: center;
1657
+ gap: 0.15rem;
1658
+ margin-inline-start: 0.4rem;
1659
+ vertical-align: baseline;
1660
+ }
1661
+ .tbl-sort,
1662
+ .tbl-filter > summary {
1663
+ background: transparent;
1664
+ border: 0;
1665
+ padding: 0 0.15rem;
1666
+ font: inherit;
1667
+ font-size: var(--fs-base);
1668
+ line-height: 1;
1669
+ color: var(--muted-foreground);
1670
+ cursor: pointer;
1671
+ opacity: 0.55;
1672
+ transition: opacity 0.15s ease, color 0.15s ease;
1673
+ }
1674
+ .tbl-sort:hover,
1675
+ .tbl-filter > summary:hover,
1676
+ th[aria-sort]:not([aria-sort="none"]) .tbl-sort { opacity: 1; color: var(--primary); }
1677
+ .tbl-sort:focus-visible,
1678
+ .tbl-filter > summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; opacity: 1; }
1679
+
1680
+ /* A COLUMN THAT IS FILTERING SAYS SO ON ITSELF, not only in the bar above. The
1681
+ bar can be scrolled off a long table; the header cannot, because it is what
1682
+ the reader is looking at when they wonder where a row went. */
1683
+ .tbl-filter.is-on > summary { opacity: 1; color: var(--primary); }
1684
+ .tbl-filter.is-on > summary::after { content: "•"; margin-inline-start: 0.1em; }
1685
+
1686
+ .tbl-filter-panel { padding: 0.4rem; max-block-size: 60vh; overflow-y: auto; }
1687
+ .tbl-filter-input {
1688
+ font: inherit;
1689
+ font-size: var(--fs-base);
1690
+ color: var(--foreground);
1691
+ background: var(--background);
1692
+ border: 1px solid var(--border);
1693
+ padding: 0.2rem 0.4rem;
1694
+ inline-size: 12rem;
1695
+ max-inline-size: 100%;
1696
+ }
1697
+
1698
+ /* ── WHAT IS IN FORCE, AND THE WAY BACK ───────────────────────────────────────
1699
+ A table that is already withholding rows for a reason nobody on screen gave is
1700
+ the failure this prevents: an empty table and a filtered one look nearly
1701
+ identical. The bar names every filter and any non-default sort, and carries
1702
+ the only way back.
1703
+
1704
+ `[hidden]` IS NOT OPTIONAL. `[hidden] { display: none }` is a UA rule at the
1705
+ lowest specificity, so `display: flex` here would beat it and the bar would
1706
+ render on every table whether or not anything was in force — the indicator
1707
+ wearing the very fault it reports. */
1708
+ .tbl-view {
1709
+ display: flex;
1710
+ flex-wrap: wrap;
1711
+ align-items: center;
1712
+ gap: 0.4rem;
1713
+ margin-block-end: 0.6rem;
1714
+ padding: 0.35rem 0.6rem;
1715
+ font-size: var(--fs-base);
1716
+ border-inline-start: 3px solid var(--primary);
1717
+ background: color-mix(in srgb, var(--primary) 10%, var(--background));
1718
+ }
1719
+ .tbl-view[hidden] { display: none; }
1720
+ .tbl-view-lead { color: var(--primary); font-weight: 700; }
1721
+ .tbl-view-lead::before { content: "▸ "; }
1722
+ .tbl-view-chip {
1723
+ border: 1px solid var(--border);
1724
+ background: var(--card);
1725
+ padding: 0.05rem 0.4rem;
1726
+ white-space: nowrap;
1727
+ }
1728
+ /* The way out sits at the far end of the line it undoes, and falls in beside the
1729
+ chips when the row wraps on a phone rather than stranding itself on a line. */
1730
+ .tbl-view [data-view-reset] { margin-inline-start: auto; }
1731
+
1732
+ @media (pointer: coarse) {
1733
+ .tbl-sort, .tbl-filter > summary { min-inline-size: 44px; min-block-size: 44px;
1734
+ display: inline-flex; align-items: center; justify-content: center; }
1735
+ }
1736
+ @media print {
1737
+ .tbl-toolbar, .tbl-tools { display: none !important; }
1738
+ /* The bar PRINTS. A printed table that is silently filtered is the same
1739
+ failure on paper, with no way to interrogate it at all. */
1740
+ }
1741
+
1591
1742
  /* ===== chrome.css ===== */
1592
1743
  /*
1593
1744
  * chrome.css — the app chrome kit: sticky header bar, fixed status footer,
@@ -2281,9 +2432,12 @@ button[data-tip]::after {
2281
2432
  * visibility:hidden. Printing it would silently produce blank pages — the
2282
2433
  * reader only finds out on paper. Everything is forced visible here
2283
2434
  * regardless of animation state.
2284
- * 2. `zoom` is reset. Pages that use the resolution-independent zoom trick
2285
- * (html.style.zoom = innerWidth / 1920) carry that multiplier into the
2286
- * print box, which is what makes a printout come out oversized.
2435
+ * 2. The screen's wide-display scaling is undone. Since 0.29.0 that scaling is
2436
+ * the fluid ROOT FONT SIZE in tokens.css, and pinning `font-size: 16px`
2437
+ * below is what neutralises it — a 4K screen would otherwise put 48px body
2438
+ * text on paper. The `zoom: 1` reset is kept beside it for any surface
2439
+ * still carrying the old `html.style.zoom = innerWidth / 1920` block,
2440
+ * which had the same effect on the print box.
2287
2441
  * 3. Mermaid bakes the ACTIVE theme's colours into the SVG at render time, so
2288
2442
  * a diagram rendered under `green`/`mono` is dark-on-dark on paper no
2289
2443
  * matter what the page tokens say. The SVG parts are repainted below.
@@ -2326,7 +2480,8 @@ button[data-tip]::after {
2326
2480
  /* ── document metrics ──────────────────────────────────────────────────
2327
2481
  The rem baseline is pinned so the design's rem scale lands where it was
2328
2482
  drawn, and the body size is stated in px so it is 12px on paper whatever
2329
- the browser's default font size is. `zoom` is a screen-only device. */
2483
+ the browser's default font size is. Both the fluid root size and `zoom` are
2484
+ screen-only devices; paper has no viewport width to track. */
2330
2485
  html {
2331
2486
  zoom: 1 !important;
2332
2487
  font-size: 16px;
@@ -1,2 +1,2 @@
1
- /*! danieldeusing-design v0.28.1 | 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-base: 0.75rem;--fs-lg: 0.9375rem;--fs-xl: 1.125rem;--space-section: 2.5rem;--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-base);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}tr[hidden]{display: none !important}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)}:where(section) + :where(section),:where(section.doc){margin-block-start: var(--space-section)}:where(section,.wrap,.body)>:where(h2){margin-block-start: var(--space-section)}:where(section,.wrap,.body)>:where(h3){margin-block-start: calc(var(--space-section) * 0.6)}:where(section,.wrap,.body)>:where(h2,h3):first-child{margin-block-start: 0}:where(h2,h3) + :where(p,table,.tablewrap,svg,.legend){margin-block-start: 0.6rem}:where(p) + :where(table,.tablewrap,svg,.legend){margin-block-start: 0.6rem}.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-base);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: var(--fs-base);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:disabled{opacity: 0.45;cursor: default;box-shadow: none;transform: none}.btn-terminal--compact{padding: 0.28rem 0.7rem;font-size: var(--fs-base)}.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}}.btn-terminal--edit{display: inline-flex;align-items: center;justify-content: center;padding: 0.3rem}.btn-terminal--edit::before{content: ""}.btn-terminal--edit::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='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04l-3.75-3.75-1.83 1.83 3.75 3.75 1.83-1.83z'/%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='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04l-3.75-3.75-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E") center / contain no-repeat}@media (pointer: coarse){.btn-terminal--edit{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)}input[type="text"],input[type="search"],input[type="url"],input[type="email"],input[type="password"],input[type="number"],input[type="tel"],input[type="date"],input:not([type]),textarea{max-width: 100%;padding: 0.35rem 0.5rem;font: inherit;font-size: var(--fs-base);line-height: var(--lh-tight);color: var(--foreground);background-color: transparent;border: 1px solid color-mix(in srgb,var(--foreground) 60%,transparent);transition: border-color 0.15s ease}textarea{line-height: var(--lh-base)}input[type="text"]:hover,input[type="search"]:hover,input[type="url"]:hover,input[type="email"]:hover,input[type="password"]:hover,input[type="number"]:hover,input[type="tel"]:hover,input[type="date"]:hover,input:not([type]):hover,textarea:hover{border-color: var(--primary)}input:disabled,textarea:disabled{opacity: 0.55;cursor: not-allowed}select,.select-trigger{appearance: none;-webkit-appearance: none;max-width: 100%;padding: 0.35rem 1.45rem 0.35rem 0.5rem;font: inherit;font-size: var(--fs-base);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-base);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-base);text-transform: uppercase;letter-spacing: 0.06em}.table-pager{display: flex;flex-wrap: wrap;align-items: center;gap: 0.45rem 0.9rem;margin-block-start: 0.6rem;font-size: var(--fs-base)}.table-pager-status{margin: 0;color: var(--muted-foreground)}.table-pager-size{display: inline-flex;align-items: center;gap: 0.4rem;margin-inline-start: auto;color: var(--muted-foreground)}.table-pager-nav{display: inline-flex;gap: 0.4rem}.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: var(--fs-base);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: var(--fs-base);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: var(--fs-base);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: var(--fs-base);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: var(--fs-base)}@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: var(--fs-base);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(--tablewrap-fade,var(--background,transparent)))}.tablewrap thead th{position: sticky;inset-block-start: 0;z-index: 1;background: var(--background,#f5efe2)}.tickstrip:not(:empty){--tablewrap-fade: var(--card);border: 1px solid var(--border);background: var(--card);margin: 1.6rem 0 1.5rem}.ticktable{width: 100%;border-collapse: collapse;font-size: var(--fs-base)}.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: var(--fs-base)}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-base);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: var(--fs-base);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: var(--fs-base)}.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: var(--fs-base);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: var(--fs-base)}.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]::after,th[data-tip]::after,button[data-tip]::after{content: "\FE0E\24D8";margin-inline-start: 0.35em;color: var(--muted-foreground);font-size: 0.9em;font-weight: 400;user-select: none;vertical-align: baseline}.minimap-bar[data-tip]::after,[data-tip][data-tip-bare]::after{content: none}#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: var(--fs-base);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]::after,th[data-tip]::after,button[data-tip]::after{content: none}.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.30.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;font-size: max(1rem,calc(1rem + (100vw - 1920px) / 120));--content-pad: 1.5rem;--fs-base: 0.75rem;--fs-lg: 0.9375rem;--fs-xl: 1.125rem;--space-section: 2.5rem;--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-base);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}tr[hidden]{display: none !important}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)}:where(section) + :where(section),:where(section.doc){margin-block-start: var(--space-section)}:where(section,.wrap,.body)>:where(h2){margin-block-start: var(--space-section)}:where(section,.wrap,.body)>:where(h3){margin-block-start: calc(var(--space-section) * 0.6)}:where(section,.wrap,.body)>:where(h2,h3):first-child{margin-block-start: 0}:where(h2,h3) + :where(p,table,.tablewrap,svg,.legend){margin-block-start: 0.6rem}:where(p) + :where(table,.tablewrap,svg,.legend){margin-block-start: 0.6rem}.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-base);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: var(--fs-base);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:disabled{opacity: 0.45;cursor: default;box-shadow: none;transform: none}.btn-terminal--compact{padding: 0.28rem 0.7rem;font-size: var(--fs-base)}.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}}.btn-terminal--edit{display: inline-flex;align-items: center;justify-content: center;padding: 0.3rem}.btn-terminal--edit::before{content: ""}.btn-terminal--edit::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='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04l-3.75-3.75-1.83 1.83 3.75 3.75 1.83-1.83z'/%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='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04l-3.75-3.75-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E") center / contain no-repeat}@media (pointer: coarse){.btn-terminal--edit{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)}input[type="text"],input[type="search"],input[type="url"],input[type="email"],input[type="password"],input[type="number"],input[type="tel"],input[type="date"],input:not([type]),textarea{max-width: 100%;padding: 0.35rem 0.5rem;font: inherit;font-size: var(--fs-base);line-height: var(--lh-tight);color: var(--foreground);background-color: transparent;border: 1px solid color-mix(in srgb,var(--foreground) 60%,transparent);transition: border-color 0.15s ease}textarea{line-height: var(--lh-base)}input[type="text"]:hover,input[type="search"]:hover,input[type="url"]:hover,input[type="email"]:hover,input[type="password"]:hover,input[type="number"]:hover,input[type="tel"]:hover,input[type="date"]:hover,input:not([type]):hover,textarea:hover{border-color: var(--primary)}input:disabled,textarea:disabled{opacity: 0.55;cursor: not-allowed}select,.select-trigger{appearance: none;-webkit-appearance: none;max-width: 100%;padding: 0.35rem 1.45rem 0.35rem 0.5rem;font: inherit;font-size: var(--fs-base);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-base);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-base);text-transform: uppercase;letter-spacing: 0.06em}.table-pager{display: flex;flex-wrap: wrap;align-items: center;gap: 0.45rem 0.9rem;margin-block-start: 0.6rem;font-size: var(--fs-base)}.table-pager-status{margin: 0;color: var(--muted-foreground)}.table-pager-size{display: inline-flex;align-items: center;gap: 0.4rem;margin-inline-start: auto;color: var(--muted-foreground)}.table-pager-nav{display: inline-flex;gap: 0.4rem}.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: var(--fs-base);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: var(--fs-base);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: var(--fs-base);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: var(--fs-base);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: var(--fs-base)}@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: var(--fs-base);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}}.tbl-toolbar{display: flex;align-items: center;gap: 0.5rem;margin-block-end: 0.5rem}.tbl-search{flex: 1 1 auto;min-inline-size: 0;font: inherit;font-size: var(--fs-base);color: var(--foreground);background: var(--background);border: 1px solid var(--border);padding: 0.25rem 0.5rem}.tbl-search:focus-visible{outline: 2px solid var(--ring);outline-offset: 1px}.tbl-tools{display: inline-flex;align-items: center;gap: 0.15rem;margin-inline-start: 0.4rem;vertical-align: baseline}.tbl-sort,.tbl-filter>summary{background: transparent;border: 0;padding: 0 0.15rem;font: inherit;font-size: var(--fs-base);line-height: 1;color: var(--muted-foreground);cursor: pointer;opacity: 0.55;transition: opacity 0.15s ease,color 0.15s ease}.tbl-sort:hover,.tbl-filter>summary:hover,th[aria-sort]:not([aria-sort="none"]) .tbl-sort{opacity: 1;color: var(--primary)}.tbl-sort:focus-visible,.tbl-filter>summary:focus-visible{outline: 2px solid var(--ring);outline-offset: 1px;opacity: 1}.tbl-filter.is-on>summary{opacity: 1;color: var(--primary)}.tbl-filter.is-on>summary::after{content: "•";margin-inline-start: 0.1em}.tbl-filter-panel{padding: 0.4rem;max-block-size: 60vh;overflow-y: auto}.tbl-filter-input{font: inherit;font-size: var(--fs-base);color: var(--foreground);background: var(--background);border: 1px solid var(--border);padding: 0.2rem 0.4rem;inline-size: 12rem;max-inline-size: 100%}.tbl-view{display: flex;flex-wrap: wrap;align-items: center;gap: 0.4rem;margin-block-end: 0.6rem;padding: 0.35rem 0.6rem;font-size: var(--fs-base);border-inline-start: 3px solid var(--primary);background: color-mix(in srgb,var(--primary) 10%,var(--background))}.tbl-view[hidden]{display: none}.tbl-view-lead{color: var(--primary);font-weight: 700}.tbl-view-lead::before{content: "▸ "}.tbl-view-chip{border: 1px solid var(--border);background: var(--card);padding: 0.05rem 0.4rem;white-space: nowrap}.tbl-view [data-view-reset]{margin-inline-start: auto}@media (pointer: coarse){.tbl-sort,.tbl-filter>summary{min-inline-size: 44px;min-block-size: 44px;display: inline-flex;align-items: center;justify-content: center}}@media print{.tbl-toolbar,.tbl-tools{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(--tablewrap-fade,var(--background,transparent)))}.tablewrap thead th{position: sticky;inset-block-start: 0;z-index: 1;background: var(--background,#f5efe2)}.tickstrip:not(:empty){--tablewrap-fade: var(--card);border: 1px solid var(--border);background: var(--card);margin: 1.6rem 0 1.5rem}.ticktable{width: 100%;border-collapse: collapse;font-size: var(--fs-base)}.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: var(--fs-base)}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-base);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: var(--fs-base);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: var(--fs-base)}.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: var(--fs-base);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: var(--fs-base)}.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]::after,th[data-tip]::after,button[data-tip]::after{content: "\FE0E\24D8";margin-inline-start: 0.35em;color: var(--muted-foreground);font-size: 0.9em;font-weight: 400;user-select: none;vertical-align: baseline}.minimap-bar[data-tip]::after,[data-tip][data-tip-bare]::after{content: none}#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: var(--fs-base);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]::after,th[data-tip]::after,button[data-tip]::after{content: none}.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.28.1",
3
+ "version": "0.30.0",
4
4
  "description": "The danieldeusing terminal design system \u2014 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
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * import { applyStoredTheme, initThemeSwitcher } from "@danieldeusing/design/runtime";
8
8
  *
9
- * The CSS works on its own; this layer adds theme switching, the resolution
10
- * zoom, dropdown behaviour, and the terminal typing animation.
9
+ * The CSS works on its own; this layer adds theme switching, dropdown
10
+ * behaviour, table tools and the terminal typing animation.
11
11
  */
12
12
 
13
13
  export * from "./theme.js";
@@ -23,3 +23,4 @@ export * from "./tablescroll.js";
23
23
  export * from "./pagination.js";
24
24
  export * from "./minimap.js";
25
25
  export * from "./tooltip.js";
26
+ export * from "./tabletools.js";
@@ -0,0 +1,404 @@
1
+ /*
2
+ * danieldeusing-design — a table's search, its per-column filters and its sort.
3
+ *
4
+ * Every table in the estate had been growing these by hand. cockpit's own
5
+ * `table-view.js` opens by explaining that `cockpitTable` "is copied into four
6
+ * pages and has drifted into three generations"; the family contacts table grew
7
+ * a row of bare filter boxes because nothing said what a table should look like.
8
+ * Consolidating inside one surface fixed it for that surface. This is the same
9
+ * move one level up, and the storage key is deliberately cockpit's own
10
+ * (`table-view:<id>`) so a reader's saved views survive the migration.
11
+ *
12
+ * ── THE ICONS LIVE IN THE HEADER, THE SEARCH LIVES ON TOP ─────────────────────
13
+ *
14
+ * A row of text boxes under the header (`tr.filters`) spends a whole row of
15
+ * vertical space announcing a capability that is idle on most visits, and it
16
+ * reads as a form to fill in. Two small controls in the `<th>` cost nothing when
17
+ * unused and sit on the column they act on. The filter opens a `<details
18
+ * class="dropdown">` — the system's existing dropdown, so one-open, click-away
19
+ * and Escape are already handled by initDropdowns() and are not reimplemented.
20
+ *
21
+ * ── COMPOSING WITH THE PAGER, WHICH ALSO OWNS `hidden` ────────────────────────
22
+ *
23
+ * runtime/pagination.js pages a table by setting `hidden` on the rows outside
24
+ * the window. If filtering ALSO used `hidden` the two would overwrite each
25
+ * other: the pager clears `hidden` on the first twenty rows in the tbody, which
26
+ * would include rows the filter had just excluded.
27
+ *
28
+ * So a filtered-out row is DETACHED from the tbody and held here, and the tbody
29
+ * is left containing exactly the matching rows in sort order. The pager then
30
+ * sees the set it is documented to expect — "filter and sort produce the rows,
31
+ * the pager slices them" — and needs no knowledge of this file. Its
32
+ * MutationObserver notices the childList change and re-pages on its own.
33
+ *
34
+ * Detaching rather than hiding also means `:nth-child` striping and the pager's
35
+ * own counts are honest without either of them having to know a filter exists.
36
+ *
37
+ * ── NORMALISE AGAINST TODAY'S COLUMNS ─────────────────────────────────────────
38
+ *
39
+ * Inherited from cockpit's engine, and the reason it is not a detail:
40
+ * localStorage outlives the code. A stored sortKey naming a column that has
41
+ * since been renamed would reach an undefined column and throw where the table
42
+ * should be. Unknown keys are dropped, and a direction only survives WITH the
43
+ * column it sorted — applying a remembered direction to a different column hands
44
+ * back a view the reader never chose.
45
+ */
46
+
47
+ const STORE_PREFIX = "table-view:";
48
+ const instances = new WeakMap();
49
+
50
+ const textOf = (el) => (el ? (el.textContent || "").trim() : "");
51
+
52
+ /* Sort value: `data-value` wins so a column can sort by something it does not
53
+ print (an ISO date under a friendly one, cents under a formatted amount). */
54
+ const cellValue = (row, index) => {
55
+ const cell = row.cells[index];
56
+ if (!cell) return "";
57
+ const explicit = cell.getAttribute("data-value");
58
+ return explicit === null ? textOf(cell) : explicit.trim();
59
+ };
60
+
61
+ /*
62
+ * THE DIRECTION IS APPLIED IN HERE, not by the caller, and that is the whole
63
+ * reason this takes `dir`. A blank cell sorts LAST IN BOTH DIRECTIONS — treating
64
+ * it as 0 would file "not reported" between the negative and the positive
65
+ * numbers, which reads as a measurement rather than an absence. Multiplying the
66
+ * comparator's result by the direction outside it inverts that rule along with
67
+ * everything else, so descending puts every blank FIRST. Caught by the fixture:
68
+ * ascending ended [...Carla, Dieter] and descending began [Dieter, Carla].
69
+ */
70
+ const compare = (a, b, type, dir) => {
71
+ const aBlank = a === "", bBlank = b === "";
72
+ if (aBlank && bBlank) return 0;
73
+ if (aBlank) return 1;
74
+ if (bBlank) return -1;
75
+ if (type === "num") {
76
+ const na = parseFloat(a.replace(/[^0-9.eE+-]/g, ""));
77
+ const nb = parseFloat(b.replace(/[^0-9.eE+-]/g, ""));
78
+ const aNaN = Number.isNaN(na), bNaN = Number.isNaN(nb);
79
+ if (aNaN && bNaN) return 0;
80
+ if (aNaN) return 1;
81
+ if (bNaN) return -1;
82
+ return dir * (na - nb);
83
+ }
84
+ return dir * a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
85
+ };
86
+
87
+ function columnsOf(table) {
88
+ const head = table.tHead && table.tHead.rows[0];
89
+ if (!head) return [];
90
+ const out = [];
91
+ for (let i = 0; i < head.cells.length; i += 1) {
92
+ const th = head.cells[i];
93
+ const key = th.getAttribute("data-col");
94
+ if (!key) continue; // a column opted out is left alone
95
+ out.push({
96
+ key, index: i, th,
97
+ label: th.getAttribute("data-col-label") || textOf(th),
98
+ type: th.getAttribute("data-sort-type") || "text",
99
+ filter: th.getAttribute("data-filter") || "text",
100
+ });
101
+ }
102
+ return out;
103
+ }
104
+
105
+ const defaults = (inst) => ({ sortKey: inst.defaultSortKey, dir: inst.defaultDir, filters: {}, search: "" });
106
+
107
+ const activeFilters = (inst, view) => {
108
+ const out = {};
109
+ for (const col of inst.columns) {
110
+ const value = view.filters && view.filters[col.key];
111
+ if (value) out[col.key] = String(value);
112
+ }
113
+ return out;
114
+ };
115
+
116
+ const isDefault = (inst, view) =>
117
+ view.sortKey === inst.defaultSortKey && view.dir === inst.defaultDir &&
118
+ !view.search && Object.keys(activeFilters(inst, view)).length === 0;
119
+
120
+ function normalize(inst, raw) {
121
+ const view = defaults(inst);
122
+ let stored;
123
+ try { stored = JSON.parse(raw); } catch { return view; }
124
+ if (!stored || typeof stored !== "object" || Array.isArray(stored)) return view;
125
+ const known = new Set(inst.columns.map((c) => c.key));
126
+ if (known.has(stored.sortKey)) {
127
+ view.sortKey = stored.sortKey;
128
+ if (stored.dir === 1 || stored.dir === -1) view.dir = stored.dir;
129
+ }
130
+ if (stored.filters && typeof stored.filters === "object" && !Array.isArray(stored.filters)) {
131
+ for (const key of Object.keys(stored.filters)) {
132
+ if (!known.has(key)) continue;
133
+ const text = String(stored.filters[key] ?? "").trim().toLowerCase();
134
+ if (text) view.filters[key] = text;
135
+ }
136
+ }
137
+ if (typeof stored.search === "string") view.search = stored.search.trim().toLowerCase();
138
+ return view;
139
+ }
140
+
141
+ /* Both wrapped: Safari in private mode THROWS on localStorage rather than
142
+ returning null, and a table that refuses to render because a preference could
143
+ not be read is worse than one that starts at its defaults. */
144
+ function save(inst) {
145
+ if (!inst.id) return;
146
+ try {
147
+ if (isDefault(inst, inst.view)) localStorage.removeItem(STORE_PREFIX + inst.id);
148
+ else localStorage.setItem(STORE_PREFIX + inst.id, JSON.stringify({
149
+ sortKey: inst.view.sortKey, dir: inst.view.dir,
150
+ filters: activeFilters(inst, inst.view), search: inst.view.search || "",
151
+ }));
152
+ } catch { /* the view still applies for this visit; only the memory is lost */ }
153
+ }
154
+
155
+ function restore(inst) {
156
+ if (!inst.id) return defaults(inst);
157
+ try { return normalize(inst, localStorage.getItem(STORE_PREFIX + inst.id)); }
158
+ catch { return defaults(inst); }
159
+ }
160
+
161
+ const matches = (inst, row) => {
162
+ const view = inst.view;
163
+ if (view.search) {
164
+ const hay = textOf(row).toLowerCase();
165
+ if (!hay.includes(view.search)) return false;
166
+ }
167
+ for (const col of inst.columns) {
168
+ const want = view.filters[col.key];
169
+ if (!want) continue;
170
+ const got = (cellValue(row, col.index) || "").toLowerCase();
171
+ // `pick` is an exact match and `text` is a contains — spelling both as
172
+ // "contains" would be a small lie about why a row is missing.
173
+ if (col.filter === "pick" ? got !== want : !got.includes(want)) return false;
174
+ }
175
+ return true;
176
+ };
177
+
178
+ export function applyTableView(table) {
179
+ const inst = instances.get(table);
180
+ if (!inst) return;
181
+ const body = table.tBodies[0];
182
+ if (!body) return;
183
+
184
+ const all = inst.allRows.slice();
185
+ const keep = [], drop = [];
186
+ for (const row of all) (matches(inst, row) ? keep : drop).push(row);
187
+
188
+ const col = inst.columns.find((c) => c.key === inst.view.sortKey);
189
+ if (col) {
190
+ keep.sort((a, b) => compare(cellValue(a, col.index), cellValue(b, col.index), col.type, inst.view.dir));
191
+ }
192
+
193
+ // Written in one pass so the pager's observer wakes once, not per row.
194
+ const frag = document.createDocumentFragment();
195
+ for (const row of keep) frag.appendChild(row);
196
+ for (const row of drop) if (row.parentNode) row.parentNode.removeChild(row);
197
+ body.appendChild(frag);
198
+
199
+ paintHeader(inst);
200
+ paintBar(inst);
201
+ }
202
+
203
+ function paintHeader(inst) {
204
+ for (const col of inst.columns) {
205
+ const sorted = inst.view.sortKey === col.key;
206
+ col.th.setAttribute("aria-sort", sorted ? (inst.view.dir === 1 ? "ascending" : "descending") : "none");
207
+ if (col.sortBtn) col.sortBtn.textContent = sorted ? (inst.view.dir === 1 ? "▲" : "▼") : "↕";
208
+ const on = Boolean(inst.view.filters[col.key]);
209
+ if (col.filterWrap) col.filterWrap.classList.toggle("is-on", on);
210
+ }
211
+ }
212
+
213
+ function paintBar(inst) {
214
+ const view = inst.view, bar = inst.bar;
215
+ const chips = [];
216
+ const chip = (text, title) => {
217
+ const s = document.createElement("span");
218
+ s.className = "tbl-view-chip";
219
+ s.textContent = text;
220
+ if (title) s.title = title;
221
+ return s;
222
+ };
223
+ const active = activeFilters(inst, view);
224
+ for (const col of inst.columns) {
225
+ const value = active[col.key];
226
+ if (!value) continue;
227
+ const exact = col.filter === "pick";
228
+ chips.push(chip(col.label + (exact ? " = " : " ~ ") + value,
229
+ exact ? `${col.label} is exactly “${value}”` : `${col.label} contains “${value}”`));
230
+ }
231
+ if (view.search) chips.push(chip("search ~ " + view.search, `any column contains “${view.search}”`));
232
+ if (view.sortKey !== inst.defaultSortKey || view.dir !== inst.defaultDir) {
233
+ const col = inst.columns.find((c) => c.key === view.sortKey);
234
+ if (col) chips.push(chip(`sorted by ${col.label} ${view.dir === 1 ? "▲" : "▼"}`,
235
+ `sorted by ${col.label}, ${view.dir === 1 ? "ascending" : "descending"}`));
236
+ }
237
+
238
+ bar.textContent = "";
239
+ if (!chips.length) { bar.hidden = true; return; }
240
+ const lead = document.createElement("span");
241
+ lead.className = "tbl-view-lead";
242
+ lead.title = "This table remembers its filters and its sort in this browser. Reset view puts it back to the order and the rows it ships with.";
243
+ lead.textContent = "saved view";
244
+ bar.appendChild(lead);
245
+ for (const c of chips) bar.appendChild(c);
246
+ const reset = document.createElement("button");
247
+ reset.type = "button";
248
+ reset.className = "btn-terminal btn-terminal--ghost btn-terminal--compact";
249
+ reset.setAttribute("data-view-reset", "");
250
+ reset.textContent = "reset view";
251
+ bar.appendChild(reset);
252
+ bar.hidden = false;
253
+ }
254
+
255
+ function buildHeaderControls(inst) {
256
+ for (const col of inst.columns) {
257
+ const tools = document.createElement("span");
258
+ tools.className = "tbl-tools";
259
+
260
+ const sort = document.createElement("button");
261
+ sort.type = "button";
262
+ sort.className = "tbl-sort";
263
+ sort.setAttribute("data-tip", "sort by " + col.label);
264
+ sort.setAttribute("aria-label", "sort by " + col.label);
265
+ sort.textContent = "↕";
266
+ sort.addEventListener("click", () => {
267
+ if (inst.view.sortKey === col.key) inst.view.dir = inst.view.dir === 1 ? -1 : 1;
268
+ else { inst.view.sortKey = col.key; inst.view.dir = 1; }
269
+ save(inst); applyTableView(inst.table);
270
+ });
271
+ col.sortBtn = sort;
272
+ tools.appendChild(sort);
273
+
274
+ const wrap = document.createElement("details");
275
+ wrap.className = "dropdown tbl-filter";
276
+ const summary = document.createElement("summary");
277
+ summary.setAttribute("data-tip", "filter " + col.label);
278
+ summary.setAttribute("aria-label", "filter " + col.label);
279
+ summary.textContent = "⌕";
280
+ wrap.appendChild(summary);
281
+
282
+ const panel = document.createElement("div");
283
+ panel.className = "dropdown-panel dropdown-panel--down tbl-filter-panel";
284
+
285
+ if (col.filter === "pick") {
286
+ // The list is built from the column's own cells, so it can never offer a
287
+ // value the table does not contain.
288
+ const seen = new Map();
289
+ for (const row of inst.allRows) {
290
+ const raw = cellValue(row, col.index);
291
+ if (raw) seen.set(raw.toLowerCase(), raw);
292
+ }
293
+ const any = document.createElement("button");
294
+ any.type = "button"; any.className = "dropdown-item"; any.textContent = "(any)";
295
+ any.addEventListener("click", () => {
296
+ inst.view.filters[col.key] = ""; wrap.open = false; save(inst); applyTableView(inst.table);
297
+ });
298
+ panel.appendChild(any);
299
+ for (const [lower, label] of [...seen.entries()].sort((a, b) => a[1].localeCompare(b[1]))) {
300
+ const b = document.createElement("button");
301
+ b.type = "button"; b.className = "dropdown-item"; b.textContent = label;
302
+ b.addEventListener("click", () => {
303
+ inst.view.filters[col.key] = lower; wrap.open = false; save(inst); applyTableView(inst.table);
304
+ });
305
+ panel.appendChild(b);
306
+ }
307
+ } else {
308
+ const input = document.createElement("input");
309
+ input.type = "search";
310
+ input.className = "tbl-filter-input";
311
+ input.placeholder = col.label + " contains…";
312
+ input.setAttribute("aria-label", "filter " + col.label);
313
+ // cockpit learned this one the expensive way: without it a password
314
+ // manager offers to fill every filter box on the page.
315
+ input.setAttribute("data-1p-ignore", "");
316
+ input.addEventListener("input", () => {
317
+ inst.view.filters[col.key] = input.value.trim().toLowerCase();
318
+ save(inst); applyTableView(inst.table);
319
+ });
320
+ col.filterInput = input;
321
+ panel.appendChild(input);
322
+ }
323
+
324
+ wrap.appendChild(panel);
325
+ col.filterWrap = wrap;
326
+ tools.appendChild(wrap);
327
+ col.th.appendChild(tools);
328
+ }
329
+ }
330
+
331
+ function enhance(table) {
332
+ if (instances.has(table)) return;
333
+ const columns = columnsOf(table);
334
+ if (!columns.length) return;
335
+ const body = table.tBodies[0];
336
+ if (!body) return;
337
+
338
+ const inst = {
339
+ table, columns,
340
+ id: table.getAttribute("data-table-id") || "",
341
+ allRows: [...body.rows],
342
+ defaultSortKey: table.getAttribute("data-sort-key") || columns[0].key,
343
+ defaultDir: table.getAttribute("data-sort-dir") === "desc" ? -1 : 1,
344
+ };
345
+ instances.set(table, inst);
346
+ inst.view = restore(inst);
347
+
348
+ // Before the scroll wrapper, never inside it — `.tablewrap` scrolls sideways,
349
+ // and a search box in there slides out of reach on exactly the wide tables
350
+ // that need one. Same reasoning as the pager's anchor, opposite side.
351
+ const anchor = table.closest(".tablewrap") || table;
352
+
353
+ const toolbar = document.createElement("div");
354
+ toolbar.className = "tbl-toolbar";
355
+ const search = document.createElement("input");
356
+ search.type = "search";
357
+ search.className = "tbl-search";
358
+ search.placeholder = "search this table…";
359
+ search.setAttribute("aria-label", "search this table");
360
+ search.setAttribute("data-1p-ignore", "");
361
+ search.value = inst.view.search || "";
362
+ search.addEventListener("input", () => {
363
+ inst.view.search = search.value.trim().toLowerCase();
364
+ save(inst); applyTableView(table);
365
+ });
366
+ toolbar.appendChild(search);
367
+ inst.searchInput = search;
368
+
369
+ const bar = document.createElement("div");
370
+ bar.className = "tbl-view";
371
+ bar.hidden = true;
372
+ bar.addEventListener("click", (event) => {
373
+ if (!event.target.closest("[data-view-reset]")) return;
374
+ inst.view = defaults(inst);
375
+ if (inst.searchInput) inst.searchInput.value = "";
376
+ for (const col of inst.columns) if (col.filterInput) col.filterInput.value = "";
377
+ save(inst); applyTableView(table);
378
+ });
379
+ inst.bar = bar;
380
+
381
+ anchor.before(toolbar);
382
+ anchor.before(bar);
383
+
384
+ buildHeaderControls(inst);
385
+ for (const col of inst.columns) {
386
+ if (col.filterInput) col.filterInput.value = inst.view.filters[col.key] || "";
387
+ }
388
+ applyTableView(table);
389
+ }
390
+
391
+ /**
392
+ * Give every `<table data-table-tools>` a search box, per-column sort and filter
393
+ * controls in its header, and a bar naming whatever is currently in force.
394
+ *
395
+ * Markup contract: `<th data-col="key">` on each column that participates.
396
+ * Optional: `data-filter="pick"` for a value list instead of a text box,
397
+ * `data-sort-type="num"`, `data-col-label`, and `data-value` on a `<td>` to sort
398
+ * by something other than what it prints.
399
+ *
400
+ * @param {ParentNode} [root=document]
401
+ */
402
+ export function initTableTools(root = document) {
403
+ for (const table of root.querySelectorAll("table[data-table-tools]")) enhance(table);
404
+ }
package/runtime/zoom.js CHANGED
@@ -1,25 +1,40 @@
1
1
  /*
2
- * danieldeusing-design — resolution-independent zoom.
2
+ * danieldeusing-design — resolution scaling.
3
3
  *
4
- * Lays the page out at a fixed reference width, then scales the whole view up
5
- * to fill wider screens — so 2K / 4K / 8K render the identical layout, just
6
- * bigger (a true zoom of everything, not font scaling). Never scales below 1×,
7
- * so screens up to the reference width keep their normal responsive layout.
4
+ * SUPERSEDED IN 0.29.0. Scaling above the reference width is now done in CSS,
5
+ * by `tokens.css` moving the ROOT FONT SIZE:
8
6
  *
9
- * Apply pre-paint (inline, in <head>) to avoid a flash at the unscaled size.
7
+ * font-size: max(1rem, calc(1rem + (100vw - 1920px) / 120));
8
+ *
9
+ * Above 1920 that reduces to 100vw/120 — precisely `16px * innerWidth/1920`,
10
+ * the curve this module used to draw with `zoom`. Nothing changes size; the
11
+ * mechanism changed.
12
+ *
13
+ * WHY. `zoom` scales the coordinate SPACE, which leaves the page with a pixel
14
+ * grid that no longer matches the browser's. Anything injected into the
15
+ * document from outside it — a password manager's dropdown, a translation bar,
16
+ * an extension overlay — measures a field through the browser's grid and writes
17
+ * the answer back into the page's, where it is multiplied again. Measured on
18
+ * the family login page: 1Password's dropdown landed 1.9x down and across from
19
+ * the field it belonged to, on a window whose zoom factor was 1.89.
20
+ *
21
+ * The same double-multiplication had already been paid for three times INSIDE
22
+ * this runtime — tooltip.js, lsnav.js and select.js all divide by the zoom
23
+ * before writing a length. Those divisions stay: they resolve to 1 once no
24
+ * consumer sets zoom, and they are still correct for a surface that has not
25
+ * migrated yet. Scaling the unit rather than the space removes the second grid
26
+ * altogether, so there is nothing left to compensate for.
10
27
  */
11
28
 
12
29
  /**
13
- * @param {number} [referenceWidth=1920] The width the layout is authored for.
14
- * On a viewport wider than this, the page is zoomed by viewport/reference.
30
+ * No longer does anything. Kept exported so a surface can bump its pin without
31
+ * its <head> having to change in the same commit.
32
+ *
33
+ * @deprecated since 0.29.0 — scaling is CSS now. Delete the call, and delete
34
+ * any inline pre-paint zoom block with it. A page that still assigns
35
+ * `document.documentElement.style.zoom` AND loads 0.29.0 scales TWICE.
36
+ * @param {number} [referenceWidth=1920] Ignored.
15
37
  */
16
38
  export function initResolutionZoom(referenceWidth = 1920) {
17
- const applyZoom = () => {
18
- // `zoom` is the right primitive here: unlike transform: scale it reflows
19
- // and keeps the document height correct. Widely supported in evergreen
20
- // browsers as of 2024.
21
- document.documentElement.style.zoom = String(Math.max(1, window.innerWidth / referenceWidth));
22
- };
23
- applyZoom();
24
- window.addEventListener("resize", applyZoom);
39
+ void referenceWidth;
25
40
  }
@@ -1046,3 +1046,122 @@ html.dgm-locked, html.dgm-locked body { overflow: hidden; }
1046
1046
 
1047
1047
  /* The strip is navigation for a screen. On paper the page IS the map. */
1048
1048
  @media print { .minimap { display: none !important; } }
1049
+
1050
+ /* ── TABLE TOOLS: search on top, sort + filter in the header ──────────────────
1051
+ Paired with runtime/tabletools.js. The estate grew four generations of table
1052
+ filtering before this existed — cockpit's `tr.act-filters`, cockpit's older
1053
+ `tr.filters`, the family contacts table's bare box row — and each spent a
1054
+ whole row of vertical space advertising a capability idle on most visits.
1055
+
1056
+ Two controls in the <th> cost nothing when unused and sit on the column they
1057
+ act on. The filter is a `details.dropdown`, the SYSTEM's dropdown, so
1058
+ one-open / click-away / Escape come from initDropdowns() and are not
1059
+ reimplemented here. Only the parts that are genuinely table-specific are
1060
+ declared below. */
1061
+ .tbl-toolbar {
1062
+ display: flex;
1063
+ align-items: center;
1064
+ gap: 0.5rem;
1065
+ margin-block-end: 0.5rem;
1066
+ }
1067
+ .tbl-search {
1068
+ flex: 1 1 auto;
1069
+ min-inline-size: 0;
1070
+ font: inherit;
1071
+ font-size: var(--fs-base);
1072
+ color: var(--foreground);
1073
+ background: var(--background);
1074
+ border: 1px solid var(--border);
1075
+ padding: 0.25rem 0.5rem;
1076
+ }
1077
+ .tbl-search:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
1078
+
1079
+ /* The header cell keeps its label on the baseline and parks the controls at its
1080
+ trailing edge, so a column of numbers still reads as a column of numbers. */
1081
+ .tbl-tools {
1082
+ display: inline-flex;
1083
+ align-items: center;
1084
+ gap: 0.15rem;
1085
+ margin-inline-start: 0.4rem;
1086
+ vertical-align: baseline;
1087
+ }
1088
+ .tbl-sort,
1089
+ .tbl-filter > summary {
1090
+ background: transparent;
1091
+ border: 0;
1092
+ padding: 0 0.15rem;
1093
+ font: inherit;
1094
+ font-size: var(--fs-base);
1095
+ line-height: 1;
1096
+ color: var(--muted-foreground);
1097
+ cursor: pointer;
1098
+ opacity: 0.55;
1099
+ transition: opacity 0.15s ease, color 0.15s ease;
1100
+ }
1101
+ .tbl-sort:hover,
1102
+ .tbl-filter > summary:hover,
1103
+ th[aria-sort]:not([aria-sort="none"]) .tbl-sort { opacity: 1; color: var(--primary); }
1104
+ .tbl-sort:focus-visible,
1105
+ .tbl-filter > summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; opacity: 1; }
1106
+
1107
+ /* A COLUMN THAT IS FILTERING SAYS SO ON ITSELF, not only in the bar above. The
1108
+ bar can be scrolled off a long table; the header cannot, because it is what
1109
+ the reader is looking at when they wonder where a row went. */
1110
+ .tbl-filter.is-on > summary { opacity: 1; color: var(--primary); }
1111
+ .tbl-filter.is-on > summary::after { content: "•"; margin-inline-start: 0.1em; }
1112
+
1113
+ .tbl-filter-panel { padding: 0.4rem; max-block-size: 60vh; overflow-y: auto; }
1114
+ .tbl-filter-input {
1115
+ font: inherit;
1116
+ font-size: var(--fs-base);
1117
+ color: var(--foreground);
1118
+ background: var(--background);
1119
+ border: 1px solid var(--border);
1120
+ padding: 0.2rem 0.4rem;
1121
+ inline-size: 12rem;
1122
+ max-inline-size: 100%;
1123
+ }
1124
+
1125
+ /* ── WHAT IS IN FORCE, AND THE WAY BACK ───────────────────────────────────────
1126
+ A table that is already withholding rows for a reason nobody on screen gave is
1127
+ the failure this prevents: an empty table and a filtered one look nearly
1128
+ identical. The bar names every filter and any non-default sort, and carries
1129
+ the only way back.
1130
+
1131
+ `[hidden]` IS NOT OPTIONAL. `[hidden] { display: none }` is a UA rule at the
1132
+ lowest specificity, so `display: flex` here would beat it and the bar would
1133
+ render on every table whether or not anything was in force — the indicator
1134
+ wearing the very fault it reports. */
1135
+ .tbl-view {
1136
+ display: flex;
1137
+ flex-wrap: wrap;
1138
+ align-items: center;
1139
+ gap: 0.4rem;
1140
+ margin-block-end: 0.6rem;
1141
+ padding: 0.35rem 0.6rem;
1142
+ font-size: var(--fs-base);
1143
+ border-inline-start: 3px solid var(--primary);
1144
+ background: color-mix(in srgb, var(--primary) 10%, var(--background));
1145
+ }
1146
+ .tbl-view[hidden] { display: none; }
1147
+ .tbl-view-lead { color: var(--primary); font-weight: 700; }
1148
+ .tbl-view-lead::before { content: "▸ "; }
1149
+ .tbl-view-chip {
1150
+ border: 1px solid var(--border);
1151
+ background: var(--card);
1152
+ padding: 0.05rem 0.4rem;
1153
+ white-space: nowrap;
1154
+ }
1155
+ /* The way out sits at the far end of the line it undoes, and falls in beside the
1156
+ chips when the row wraps on a phone rather than stranding itself on a line. */
1157
+ .tbl-view [data-view-reset] { margin-inline-start: auto; }
1158
+
1159
+ @media (pointer: coarse) {
1160
+ .tbl-sort, .tbl-filter > summary { min-inline-size: 44px; min-block-size: 44px;
1161
+ display: inline-flex; align-items: center; justify-content: center; }
1162
+ }
1163
+ @media print {
1164
+ .tbl-toolbar, .tbl-tools { display: none !important; }
1165
+ /* The bar PRINTS. A printed table that is silently filtered is the same
1166
+ failure on paper, with no way to interrogate it at all. */
1167
+ }
package/src/print.css CHANGED
@@ -17,9 +17,12 @@
17
17
  * visibility:hidden. Printing it would silently produce blank pages — the
18
18
  * reader only finds out on paper. Everything is forced visible here
19
19
  * regardless of animation state.
20
- * 2. `zoom` is reset. Pages that use the resolution-independent zoom trick
21
- * (html.style.zoom = innerWidth / 1920) carry that multiplier into the
22
- * print box, which is what makes a printout come out oversized.
20
+ * 2. The screen's wide-display scaling is undone. Since 0.29.0 that scaling is
21
+ * the fluid ROOT FONT SIZE in tokens.css, and pinning `font-size: 16px`
22
+ * below is what neutralises it — a 4K screen would otherwise put 48px body
23
+ * text on paper. The `zoom: 1` reset is kept beside it for any surface
24
+ * still carrying the old `html.style.zoom = innerWidth / 1920` block,
25
+ * which had the same effect on the print box.
23
26
  * 3. Mermaid bakes the ACTIVE theme's colours into the SVG at render time, so
24
27
  * a diagram rendered under `green`/`mono` is dark-on-dark on paper no
25
28
  * matter what the page tokens say. The SVG parts are repainted below.
@@ -62,7 +65,8 @@
62
65
  /* ── document metrics ──────────────────────────────────────────────────
63
66
  The rem baseline is pinned so the design's rem scale lands where it was
64
67
  drawn, and the body size is stated in px so it is 12px on paper whatever
65
- the browser's default font size is. `zoom` is a screen-only device. */
68
+ the browser's default font size is. Both the fluid root size and `zoom` are
69
+ screen-only devices; paper has no viewport width to track. */
66
70
  html {
67
71
  zoom: 1 !important;
68
72
  font-size: 16px;
package/src/tokens.css CHANGED
@@ -209,6 +209,38 @@ html[data-theme="paper"] {
209
209
  A page whose content genuinely is not a column (a full-bleed dashboard
210
210
  table) sets `max-width: none` deliberately; it does not pick another number. */
211
211
  --content-w: 78rem;
212
+
213
+ /* ── SCALE ABOVE THE REFERENCE WIDTH ───────────────────────────────────────
214
+ Everything in this package is stated in rem, so moving the root font size
215
+ moves the type, the content column and the vertical rhythm together. That
216
+ is how a 4K or 8K panel gets a bigger page rather than a postage stamp of
217
+ 12px text in the middle of it.
218
+
219
+ WHY NOT `zoom`, WHICH THIS REPLACES. `initResolutionZoom()` did the same
220
+ arithmetic geometrically, and a geometric scale gives the page a pixel grid
221
+ that no longer matches the browser's. Anything injected into the document
222
+ from outside — a password manager's dropdown, a translation bar — reads a
223
+ field's position through the BROWSER's grid and writes it back into the
224
+ PAGE's grid, where it is multiplied by the scale factor. Measured on the
225
+ family login page: 1Password's dropdown landed 1.9x down and across from
226
+ the field it belonged to, on a window that zoomed 1.89x. Scaling the unit
227
+ instead of the space has no second grid, so there is nothing to mismatch.
228
+
229
+ THE ARITHMETIC IS DELIBERATELY THE OLD ONE. Above 1920 this reduces to
230
+ 100vw/120, i.e. exactly `16px * innerWidth/1920` — the same curve
231
+ initResolutionZoom drew, so no surface changes size on the day it migrates.
232
+ Below 1920 the calc goes negative and max() holds it at 1rem, matching the
233
+ old `Math.max(1, ...)` floor.
234
+
235
+ THE `1rem` TERM IS LOAD-BEARING, not decoration. On the root element `rem`
236
+ resolves against the browser's default font size, so a reader who has set
237
+ a larger default still gets it honoured and browser text zoom keeps working
238
+ (WCAG 1.4.4). A bare `100vw/120` would be pixel-exact and quietly override
239
+ both.
240
+
241
+ This lives in tokens.css rather than base.css because netmon loads
242
+ tokens.css + its own frozen chrome.css and never sees base.css. */
243
+ font-size: max(1rem, calc(1rem + (100vw - 1920px) / 120));
212
244
  /* Horizontal breathing room, i.e. Daniel's "same margins left and right".
213
245
  Vertical padding stays a page's own business — a doc and a dashboard do
214
246
  not want the same headroom, and pretending otherwise is what produced the
@@ -65,19 +65,6 @@
65
65
  if (localStorage.getItem("ls-nav") === "off") document.documentElement.dataset.lsNav = "off";
66
66
  } catch {}
67
67
  })();
68
-
69
- // Resolution-independent zoom: lay out at a fixed 1920px reference, then scale the
70
- // whole view up on wider screens so the empty side-margins never exceed their
71
- // 1920-reference proportion (a true zoom of everything, never below 1×). Pre-paint
72
- // to avoid a flash.
73
- (() => {
74
- const REFERENCE_WIDTH = 1920;
75
- const applyZoom = () => {
76
- document.documentElement.style.zoom = String(Math.max(1, window.innerWidth / REFERENCE_WIDTH));
77
- };
78
- applyZoom();
79
- window.addEventListener("resize", applyZoom);
80
- })();
81
68
  </script>
82
69
 
83
70
  <!-- The design system, PINNED — and it must be. This page ships the system's own
@@ -98,13 +85,13 @@
98
85
  on docs.danieldeusing.de, NOT for a doc opened from disk over file://. -->
99
86
  <link
100
87
  rel="stylesheet"
101
- href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.28.1/dist/danieldeusing-design.min.css"
102
- onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.28.1.min.css'"
88
+ href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.30.0/dist/danieldeusing-design.min.css"
89
+ onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.30.0.min.css'"
103
90
  />
104
91
  <link
105
92
  rel="stylesheet"
106
- href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.28.1/src/fonts.css"
107
- onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.28.1.fonts.css'"
93
+ href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.30.0/src/fonts.css"
94
+ onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.30.0.fonts.css'"
108
95
  />
109
96
 
110
97
  <style>
@@ -343,7 +330,7 @@ flowchart TD
343
330
 
344
331
  <script type="module">
345
332
  import { applyStoredTheme, initThemeSwitcher, initDropdowns, initSelects, initTerminal, initAnimToggle, initBurgerNav, initLsNav, initTableScroll, initDiagramZoom, initMinimap, initTooltips } from
346
- "https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.28.1/runtime/index.js";
333
+ "https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.30.0/runtime/index.js";
347
334
  applyStoredTheme();
348
335
  initThemeSwitcher();
349
336
  initDropdowns();
@@ -33,19 +33,13 @@
33
33
 
34
34
  ── WHAT YOU MUST WIRE UP (the CSS alone is not enough) ──
35
35
  1. Pre-paint, INLINE in <head>, before any stylesheet — see the <head> block below.
36
- The theme, the rail state AND the resolution zoom must all be applied before first
37
- paint, or the page visibly flashes the default and jumps.
38
-
39
- THE ZOOM IS THE ONE PEOPLE FORGET, and it fails silently. It lays the page out at a
40
- 1920px reference and scales up beyond it, so a surface without it renders SMALLER
41
- than every other one on a wide screen and nothing looks broken, it just looks
42
- wrong next to its siblings. Cockpit's containers dashboard shipped without it and
43
- nobody noticed until the `ls -l` rail — the one component that renders identically
44
- on every page — appeared side by side at two different sizes.
45
-
46
- Use the inline block below, NOT `initResolutionZoom()` imported at the end of
47
- <body>: a module there runs after first paint, so the page paints unscaled and then
48
- jumps. The runtime export exists for surfaces that genuinely cannot inline.
36
+ The theme and the rail state must be applied before first paint, or the page
37
+ visibly flashes the default and jumps.
38
+
39
+ THERE IS NO ZOOM BLOCK ANY MORE (0.29.0). Scaling above 1920 is CSS now the
40
+ root font size in tokens.css so it needs no script and cannot be forgotten.
41
+ If you are copying an older page, DELETE its zoom IIFE: a page that still
42
+ assigns `document.documentElement.style.zoom` on top of 0.29.0 scales TWICE.
49
43
  2. At the end of <body>, import and call the runtime:
50
44
  import { initLsNav, initBurgerNav, initThemeSwitcher, initDropdowns,
51
45
  initAnimToggle, initTableScroll, initSelects }
@@ -83,17 +77,6 @@
83
77
  if (localStorage.getItem("ls-nav") === "off") document.documentElement.dataset.lsNav = "off";
84
78
  } catch {}
85
79
  })();
86
- // The 1920px reference every surface lays out against. Never below 1x: up to that width the
87
- // page keeps its normal responsive layout, and only wider screens scale — a true zoom of
88
- // everything, so graphics and text keep their proportions instead of drifting apart.
89
- (() => {
90
- const REFERENCE_WIDTH = 1920;
91
- const applyZoom = () => {
92
- document.documentElement.style.zoom = String(Math.max(1, window.innerWidth / REFERENCE_WIDTH));
93
- };
94
- applyZoom();
95
- window.addEventListener("resize", applyZoom);
96
- })();
97
80
  </script>
98
81
 
99
82
  <!-- ─────────────── header ─────────────── -->