@danieldeusing/design 0.29.0 → 0.31.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,6 +112,7 @@ 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
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
 
@@ -1,4 +1,4 @@
1
- /*! danieldeusing-design v0.29.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
1
+ /*! danieldeusing-design v0.31.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
2
2
  /* ===== reset.css ===== */
3
3
  /*
4
4
  * danieldeusing-design — minimal reset.
@@ -1620,6 +1620,125 @@ html.dgm-locked, html.dgm-locked body { overflow: hidden; }
1620
1620
  /* The strip is navigation for a screen. On paper the page IS the map. */
1621
1621
  @media print { .minimap { display: none !important; } }
1622
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
+
1623
1742
  /* ===== chrome.css ===== */
1624
1743
  /*
1625
1744
  * chrome.css — the app chrome kit: sticky header bar, fixed status footer,
@@ -1,2 +1,2 @@
1
- /*! danieldeusing-design v0.29.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}}.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.31.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.29.0",
3
+ "version": "0.31.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,557 @@
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
+ /* A header cell's own words, without the controls injected into it. */
53
+ const labelOf = (th) => {
54
+ let out = "";
55
+ for (const node of th.childNodes) {
56
+ if (node.nodeType === 1 && node.classList && node.classList.contains("tbl-tools")) continue;
57
+ out += node.textContent || "";
58
+ }
59
+ return out.trim();
60
+ };
61
+
62
+ /* Sort value: `data-value` wins so a column can sort by something it does not
63
+ print (an ISO date under a friendly one, cents under a formatted amount). */
64
+ const cellValue = (row, index) => {
65
+ const cell = row.cells[index];
66
+ if (!cell) return "";
67
+ const explicit = cell.getAttribute("data-value");
68
+ return explicit === null ? textOf(cell) : explicit.trim();
69
+ };
70
+
71
+ /*
72
+ * THE DIRECTION IS APPLIED IN HERE, not by the caller, and that is the whole
73
+ * reason this takes `dir`. A blank cell sorts LAST IN BOTH DIRECTIONS — treating
74
+ * it as 0 would file "not reported" between the negative and the positive
75
+ * numbers, which reads as a measurement rather than an absence. Multiplying the
76
+ * comparator's result by the direction outside it inverts that rule along with
77
+ * everything else, so descending puts every blank FIRST. Caught by the fixture:
78
+ * ascending ended [...Carla, Dieter] and descending began [Dieter, Carla].
79
+ */
80
+ const compare = (a, b, type, dir) => {
81
+ const aBlank = a === "", bBlank = b === "";
82
+ if (aBlank && bBlank) return 0;
83
+ if (aBlank) return 1;
84
+ if (bBlank) return -1;
85
+ if (type === "num") {
86
+ const na = parseFloat(a.replace(/[^0-9.eE+-]/g, ""));
87
+ const nb = parseFloat(b.replace(/[^0-9.eE+-]/g, ""));
88
+ const aNaN = Number.isNaN(na), bNaN = Number.isNaN(nb);
89
+ if (aNaN && bNaN) return 0;
90
+ if (aNaN) return 1;
91
+ if (bNaN) return -1;
92
+ return dir * (na - nb);
93
+ }
94
+ return dir * a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
95
+ };
96
+
97
+ function columnsOf(table) {
98
+ const head = table.tHead && table.tHead.rows[0];
99
+ if (!head) return [];
100
+ const out = [];
101
+ for (let i = 0; i < head.cells.length; i += 1) {
102
+ const th = head.cells[i];
103
+ const key = th.getAttribute("data-col");
104
+ if (!key) continue; // a column opted out is left alone
105
+ out.push({
106
+ key, index: i, th,
107
+ // The label must EXCLUDE the controls this file injects into the same cell.
108
+ // textOf(th) after a snapshot picks up the sort and filter glyphs, and the
109
+ // view bar then reads "sorted by name↕⌕ ▼". data-col-label wins when set.
110
+ label: th.getAttribute("data-col-label") || labelOf(th),
111
+ type: th.getAttribute("data-sort-type") || "text",
112
+ filter: th.getAttribute("data-filter") || "text",
113
+ });
114
+ }
115
+ return out;
116
+ }
117
+
118
+ const defaults = (inst) => ({ sortKey: inst.defaultSortKey, dir: inst.defaultDir, filters: {}, search: "" });
119
+
120
+ const activeFilters = (inst, view) => {
121
+ const out = {};
122
+ for (const col of inst.columns) {
123
+ const value = view.filters && view.filters[col.key];
124
+ if (value) out[col.key] = String(value);
125
+ }
126
+ return out;
127
+ };
128
+
129
+ const isDefault = (inst, view) =>
130
+ view.sortKey === inst.defaultSortKey && view.dir === inst.defaultDir &&
131
+ !view.search && Object.keys(activeFilters(inst, view)).length === 0;
132
+
133
+ function normalize(inst, raw) {
134
+ const view = defaults(inst);
135
+ let stored;
136
+ try { stored = JSON.parse(raw); } catch { return view; }
137
+ if (!stored || typeof stored !== "object" || Array.isArray(stored)) return view;
138
+ const known = new Set(inst.columns.map((c) => c.key));
139
+ if (known.has(stored.sortKey)) {
140
+ view.sortKey = stored.sortKey;
141
+ if (stored.dir === 1 || stored.dir === -1) view.dir = stored.dir;
142
+ }
143
+ if (stored.filters && typeof stored.filters === "object" && !Array.isArray(stored.filters)) {
144
+ for (const key of Object.keys(stored.filters)) {
145
+ if (!known.has(key)) continue;
146
+ const text = String(stored.filters[key] ?? "").trim().toLowerCase();
147
+ if (text) view.filters[key] = text;
148
+ }
149
+ }
150
+ if (typeof stored.search === "string") view.search = stored.search.trim().toLowerCase();
151
+ return view;
152
+ }
153
+
154
+ /* Both wrapped: Safari in private mode THROWS on localStorage rather than
155
+ returning null, and a table that refuses to render because a preference could
156
+ not be read is worse than one that starts at its defaults. */
157
+ function save(inst) {
158
+ if (!inst.id) return;
159
+ try {
160
+ if (isDefault(inst, inst.view)) localStorage.removeItem(STORE_PREFIX + inst.id);
161
+ else localStorage.setItem(STORE_PREFIX + inst.id, JSON.stringify({
162
+ sortKey: inst.view.sortKey, dir: inst.view.dir,
163
+ filters: activeFilters(inst, inst.view), search: inst.view.search || "",
164
+ }));
165
+ } catch { /* the view still applies for this visit; only the memory is lost */ }
166
+ }
167
+
168
+ function restore(inst) {
169
+ if (!inst.id) return defaults(inst);
170
+ try { return normalize(inst, localStorage.getItem(STORE_PREFIX + inst.id)); }
171
+ catch { return defaults(inst); }
172
+ }
173
+
174
+ const matches = (inst, row) => {
175
+ const view = inst.view;
176
+ if (view.search) {
177
+ const hay = textOf(row).toLowerCase();
178
+ if (!hay.includes(view.search)) return false;
179
+ }
180
+ for (const col of inst.columns) {
181
+ const want = view.filters[col.key];
182
+ if (!want) continue;
183
+ const got = (cellValue(row, col.index) || "").toLowerCase();
184
+ // `pick` is an exact match and `text` is a contains — spelling both as
185
+ // "contains" would be a small lie about why a row is missing.
186
+ if (col.filter === "pick" ? got !== want : !got.includes(want)) return false;
187
+ }
188
+ return true;
189
+ };
190
+
191
+ export function applyTableView(table) {
192
+ const inst = instances.get(table);
193
+ if (!inst) return;
194
+ const body = table.tBodies[0];
195
+ if (!body) return;
196
+
197
+ const keep = [], drop = [];
198
+ for (const row of inst.allRows) (matches(inst, row) ? keep : drop).push(row);
199
+
200
+ const col = inst.columns.find((c) => c.key === inst.view.sortKey);
201
+ if (col) {
202
+ keep.sort((a, b) => compare(cellValue(a, col.index), cellValue(b, col.index), col.type, inst.view.dir));
203
+ }
204
+
205
+ /*
206
+ * A DETAIL ROW IS NOT A ROW. An expandable table puts a second <tr> under the
207
+ * one it belongs to — the contacts book's per-person panel, spanning every
208
+ * column. Treated as data it would be filtered on its own text and sorted away
209
+ * from its parent, which is how a detail panel ends up under a stranger.
210
+ *
211
+ * So `data-row-for="<key>"` marks a child of `data-row-key="<key>"`: it is
212
+ * excluded from matching and from the sort, and simply follows its parent
213
+ * wherever the parent lands. A child whose parent is filtered out goes with it.
214
+ */
215
+ const frag = document.createDocumentFragment();
216
+ for (const row of keep) {
217
+ frag.appendChild(row);
218
+ for (const child of inst.childrenOf.get(row) || []) frag.appendChild(child);
219
+ }
220
+ for (const row of drop) {
221
+ for (const child of inst.childrenOf.get(row) || []) child.remove();
222
+ row.remove();
223
+ }
224
+ body.appendChild(frag);
225
+ // What we just wrote, so the observer can tell OUR output from a real
226
+ // re-render. A synchronous "applying" flag cannot: MutationObserver delivers
227
+ // asynchronously, so the flag is already back to false when the callback
228
+ // runs, and the observer re-enters forever. pagination.js avoids the same
229
+ // trap by writing only real changes; this is that discipline for a reorder.
230
+ inst.lastWritten = [...body.rows];
231
+
232
+ paintHeader(inst);
233
+ paintBar(inst);
234
+ }
235
+
236
+ function paintHeader(inst) {
237
+ for (const col of inst.columns) {
238
+ const sorted = inst.view.sortKey === col.key;
239
+ col.th.setAttribute("aria-sort", sorted ? (inst.view.dir === 1 ? "ascending" : "descending") : "none");
240
+ if (col.sortBtn) col.sortBtn.textContent = sorted ? (inst.view.dir === 1 ? "▲" : "▼") : "↕";
241
+ const on = Boolean(inst.view.filters[col.key]);
242
+ if (col.filterWrap) col.filterWrap.classList.toggle("is-on", on);
243
+ }
244
+ }
245
+
246
+ function paintBar(inst) {
247
+ const view = inst.view, bar = inst.bar;
248
+ const chips = [];
249
+ const chip = (text, title) => {
250
+ const s = document.createElement("span");
251
+ s.className = "tbl-view-chip";
252
+ s.textContent = text;
253
+ if (title) s.title = title;
254
+ return s;
255
+ };
256
+ const active = activeFilters(inst, view);
257
+ for (const col of inst.columns) {
258
+ const value = active[col.key];
259
+ if (!value) continue;
260
+ const exact = col.filter === "pick";
261
+ chips.push(chip(col.label + (exact ? " = " : " ~ ") + value,
262
+ exact ? `${col.label} is exactly “${value}”` : `${col.label} contains “${value}”`));
263
+ }
264
+ if (view.search) chips.push(chip("search ~ " + view.search, `any column contains “${view.search}”`));
265
+ if (view.sortKey !== inst.defaultSortKey || view.dir !== inst.defaultDir) {
266
+ const col = inst.columns.find((c) => c.key === view.sortKey);
267
+ if (col) chips.push(chip(`sorted by ${col.label} ${view.dir === 1 ? "▲" : "▼"}`,
268
+ `sorted by ${col.label}, ${view.dir === 1 ? "ascending" : "descending"}`));
269
+ }
270
+
271
+ bar.textContent = "";
272
+ if (!chips.length) { bar.hidden = true; return; }
273
+ const lead = document.createElement("span");
274
+ lead.className = "tbl-view-lead";
275
+ 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.";
276
+ lead.textContent = "saved view";
277
+ bar.appendChild(lead);
278
+ for (const c of chips) bar.appendChild(c);
279
+ const reset = document.createElement("button");
280
+ reset.type = "button";
281
+ reset.className = "btn-terminal btn-terminal--ghost btn-terminal--compact";
282
+ reset.setAttribute("data-view-reset", "");
283
+ reset.textContent = "reset view";
284
+ bar.appendChild(reset);
285
+ bar.hidden = false;
286
+ }
287
+
288
+ function buildHeaderControls(inst) {
289
+ for (const col of inst.columns) {
290
+ const tools = document.createElement("span");
291
+ tools.className = "tbl-tools";
292
+
293
+ const sort = document.createElement("button");
294
+ sort.type = "button";
295
+ sort.className = "tbl-sort";
296
+ sort.setAttribute("data-tip", "sort by " + col.label);
297
+ sort.setAttribute("aria-label", "sort by " + col.label);
298
+ sort.textContent = "↕";
299
+ sort.addEventListener("click", () => {
300
+ if (inst.view.sortKey === col.key) inst.view.dir = inst.view.dir === 1 ? -1 : 1;
301
+ else { inst.view.sortKey = col.key; inst.view.dir = 1; }
302
+ save(inst); applyTableView(inst.table);
303
+ });
304
+ col.sortBtn = sort;
305
+ tools.appendChild(sort);
306
+
307
+ const wrap = document.createElement("details");
308
+ wrap.className = "dropdown tbl-filter";
309
+ const summary = document.createElement("summary");
310
+ summary.setAttribute("data-tip", "filter " + col.label);
311
+ summary.setAttribute("aria-label", "filter " + col.label);
312
+ summary.textContent = "⌕";
313
+ wrap.appendChild(summary);
314
+
315
+ const panel = document.createElement("div");
316
+ panel.className = "dropdown-panel dropdown-panel--down tbl-filter-panel";
317
+
318
+ if (col.filter === "pick") {
319
+ // The list is built from the column's own cells, so it can never offer a
320
+ // value the table does not contain.
321
+ const seen = new Map();
322
+ for (const row of inst.allRows) {
323
+ const raw = cellValue(row, col.index);
324
+ if (raw) seen.set(raw.toLowerCase(), raw);
325
+ }
326
+ const any = document.createElement("button");
327
+ any.type = "button"; any.className = "dropdown-item"; any.textContent = "(any)";
328
+ any.addEventListener("click", () => {
329
+ inst.view.filters[col.key] = ""; wrap.open = false; save(inst); applyTableView(inst.table);
330
+ });
331
+ panel.appendChild(any);
332
+ for (const [lower, label] of [...seen.entries()].sort((a, b) => a[1].localeCompare(b[1]))) {
333
+ const b = document.createElement("button");
334
+ b.type = "button"; b.className = "dropdown-item"; b.textContent = label;
335
+ b.addEventListener("click", () => {
336
+ inst.view.filters[col.key] = lower; wrap.open = false; save(inst); applyTableView(inst.table);
337
+ });
338
+ panel.appendChild(b);
339
+ }
340
+ } else {
341
+ const input = document.createElement("input");
342
+ input.type = "search";
343
+ input.className = "tbl-filter-input";
344
+ input.placeholder = col.label + " contains…";
345
+ input.setAttribute("aria-label", "filter " + col.label);
346
+ // cockpit learned this one the expensive way: without it a password
347
+ // manager offers to fill every filter box on the page.
348
+ input.setAttribute("data-1p-ignore", "");
349
+ input.addEventListener("input", () => {
350
+ inst.view.filters[col.key] = input.value.trim().toLowerCase();
351
+ save(inst); applyTableView(inst.table);
352
+ });
353
+ col.filterInput = input;
354
+ panel.appendChild(input);
355
+ }
356
+
357
+ wrap.appendChild(panel);
358
+ col.filterWrap = wrap;
359
+ tools.appendChild(wrap);
360
+ col.th.appendChild(tools);
361
+ }
362
+ }
363
+
364
+ /*
365
+ * Re-read the rows and the header from the DOM as it is NOW.
366
+ *
367
+ * Every table in this estate that is worth filtering is rendered from data and
368
+ * rewritten wholesale — `contact-rows.innerHTML = …` on the contacts book,
369
+ * cockpit's in-place patcher on its automation tables, a 30-second poll behind
370
+ * both. A component that snapshotted its rows once would hold a list of detached
371
+ * <tr>s after the first repaint and quietly filter nothing, and its header
372
+ * controls would be gone with the <thead> that carried them.
373
+ */
374
+ function snapshot(inst) {
375
+ const body = inst.table.tBodies[0];
376
+ if (!body) return;
377
+ const rows = [...body.rows];
378
+ inst.childrenOf = new Map();
379
+ inst.allRows = [];
380
+ const byKey = new Map();
381
+ for (const row of rows) {
382
+ const parentKey = row.getAttribute("data-row-for");
383
+ if (parentKey === null) {
384
+ inst.allRows.push(row);
385
+ const key = row.getAttribute("data-row-key");
386
+ if (key !== null) byKey.set(key, row);
387
+ }
388
+ }
389
+ for (const row of rows) {
390
+ const parentKey = row.getAttribute("data-row-for");
391
+ if (parentKey === null) continue;
392
+ const parent = byKey.get(parentKey);
393
+ // An orphan detail row — parent filtered out by the page itself, or a stale
394
+ // key — is left as ordinary content rather than dropped. Removing a row
395
+ // because its key did not resolve would delete data over a typo.
396
+ if (!parent) { inst.allRows.push(row); continue; }
397
+ if (!inst.childrenOf.has(parent)) inst.childrenOf.set(parent, []);
398
+ inst.childrenOf.get(parent).push(row);
399
+ }
400
+ // The <thead> may have been replaced too, so the column objects must be
401
+ // re-read and the controls re-injected onto the cells that exist now.
402
+ const fresh = columnsOf(inst.table);
403
+ if (fresh.length) {
404
+ // Carry the CONTROLS across, not just the inputs. columnsOf() builds new
405
+ // column objects, and paintHeader() writes the sort glyph and the active-filter
406
+ // mark through col.sortBtn / col.filterWrap — drop those and the header stops
407
+ // reporting the state it is actually in. Measured: aria-sort said "ascending"
408
+ // while the button still showed the neutral glyph.
409
+ for (const col of fresh) {
410
+ const prev = inst.columns.find((c) => c.key === col.key);
411
+ if (!prev) continue;
412
+ col.filterInput = prev.filterInput;
413
+ col.sortBtn = prev.sortBtn;
414
+ col.filterWrap = prev.filterWrap;
415
+ }
416
+ inst.columns = fresh;
417
+ if (!inst.table.querySelector(".tbl-tools")) buildHeaderControls(inst);
418
+ else refreshPickOptions(inst);
419
+ }
420
+ }
421
+
422
+ /*
423
+ * A `pick` list is derived from the column's own cells, so it has to be rebuilt
424
+ * when those cells change — a re-render that introduces a new relationship would
425
+ * otherwise leave a dropdown that cannot offer it, and the reader would conclude
426
+ * the value does not exist. Rebuilt only when the set actually differs, so an
427
+ * open dropdown is not torn out from under the pointer on every poll.
428
+ */
429
+ function refreshPickOptions(inst) {
430
+ for (const col of inst.columns) {
431
+ if (col.filter !== "pick" || !col.filterWrap) continue;
432
+ const seen = new Map();
433
+ for (const row of inst.allRows) {
434
+ const raw = cellValue(row, col.index);
435
+ if (raw) seen.set(raw.toLowerCase(), raw);
436
+ }
437
+ const wanted = [...seen.entries()].sort((a, b) => a[1].localeCompare(b[1]));
438
+ const panel = col.filterWrap.querySelector(".tbl-filter-panel");
439
+ if (!panel) continue;
440
+ const current = [...panel.querySelectorAll(".dropdown-item")].slice(1).map((b) => b.textContent);
441
+ if (current.length === wanted.length && current.every((v, i) => v === wanted[i][1])) continue;
442
+ for (const b of [...panel.querySelectorAll(".dropdown-item")].slice(1)) b.remove();
443
+ for (const [lower, label] of wanted) {
444
+ const b = document.createElement("button");
445
+ b.type = "button"; b.className = "dropdown-item"; b.textContent = label;
446
+ b.addEventListener("click", () => {
447
+ inst.view.filters[col.key] = lower;
448
+ col.filterWrap.open = false;
449
+ save(inst); applyTableView(inst.table);
450
+ });
451
+ panel.appendChild(b);
452
+ }
453
+ }
454
+ }
455
+
456
+ function enhance(table) {
457
+ if (instances.has(table)) return;
458
+ const columns = columnsOf(table);
459
+ if (!columns.length) return;
460
+ const body = table.tBodies[0];
461
+ if (!body) return;
462
+
463
+ const inst = {
464
+ table, columns,
465
+ id: table.getAttribute("data-table-id") || "",
466
+ allRows: [],
467
+ childrenOf: new Map(),
468
+ lastWritten: [],
469
+ defaultSortKey: table.getAttribute("data-sort-key") || columns[0].key,
470
+ defaultDir: table.getAttribute("data-sort-dir") === "desc" ? -1 : 1,
471
+ };
472
+ instances.set(table, inst);
473
+ inst.view = restore(inst);
474
+
475
+ // Before the scroll wrapper, never inside it — `.tablewrap` scrolls sideways,
476
+ // and a search box in there slides out of reach on exactly the wide tables
477
+ // that need one. Same reasoning as the pager's anchor, opposite side.
478
+ const anchor = table.closest(".tablewrap") || table;
479
+
480
+ const toolbar = document.createElement("div");
481
+ toolbar.className = "tbl-toolbar";
482
+ const search = document.createElement("input");
483
+ search.type = "search";
484
+ search.className = "tbl-search";
485
+ search.placeholder = "search this table…";
486
+ search.setAttribute("aria-label", "search this table");
487
+ search.setAttribute("data-1p-ignore", "");
488
+ search.value = inst.view.search || "";
489
+ search.addEventListener("input", () => {
490
+ inst.view.search = search.value.trim().toLowerCase();
491
+ save(inst); applyTableView(table);
492
+ });
493
+ toolbar.appendChild(search);
494
+ inst.searchInput = search;
495
+
496
+ const bar = document.createElement("div");
497
+ bar.className = "tbl-view";
498
+ bar.hidden = true;
499
+ bar.addEventListener("click", (event) => {
500
+ if (!event.target.closest("[data-view-reset]")) return;
501
+ inst.view = defaults(inst);
502
+ if (inst.searchInput) inst.searchInput.value = "";
503
+ for (const col of inst.columns) if (col.filterInput) col.filterInput.value = "";
504
+ save(inst); applyTableView(table);
505
+ });
506
+ inst.bar = bar;
507
+
508
+ anchor.before(toolbar);
509
+ anchor.before(bar);
510
+
511
+ // snapshot() builds the header controls itself when they are absent, and it
512
+ // must run FIRST: a `pick` column's option list is derived from the rows, so
513
+ // building the controls against an empty set yields a dropdown offering only
514
+ // "(any)".
515
+ snapshot(inst);
516
+ for (const col of inst.columns) {
517
+ if (col.filterInput) col.filterInput.value = inst.view.filters[col.key] || "";
518
+ }
519
+ applyTableView(table);
520
+
521
+ /*
522
+ * WATCHING THE TABLE, NOT THE TBODY — a renderer is entitled to replace the
523
+ * tbody node rather than fill it, and an observer bound to the old one would
524
+ * be left watching a detached element. Same reasoning as pagination.js.
525
+ *
526
+ * The guard is an IDENTITY CHECK against the row order we last wrote, not a
527
+ * flag. applyTableView() moves every row, so without a guard this re-enters
528
+ * forever — and a synchronous flag cannot close it, because MutationObserver
529
+ * delivers asynchronously and the flag is already cleared by then. Measured:
530
+ * the first version hung the page.
531
+ */
532
+ inst.observer = new MutationObserver(() => {
533
+ const body = table.tBodies[0];
534
+ if (!body) return;
535
+ const now = body.rows;
536
+ const last = inst.lastWritten || [];
537
+ if (now.length === last.length && last.every((row, i) => now[i] === row)) return;
538
+ snapshot(inst);
539
+ applyTableView(table);
540
+ });
541
+ inst.observer.observe(table, { childList: true, subtree: true });
542
+ }
543
+
544
+ /**
545
+ * Give every `<table data-table-tools>` a search box, per-column sort and filter
546
+ * controls in its header, and a bar naming whatever is currently in force.
547
+ *
548
+ * Markup contract: `<th data-col="key">` on each column that participates.
549
+ * Optional: `data-filter="pick"` for a value list instead of a text box,
550
+ * `data-sort-type="num"`, `data-col-label`, and `data-value` on a `<td>` to sort
551
+ * by something other than what it prints.
552
+ *
553
+ * @param {ParentNode} [root=document]
554
+ */
555
+ export function initTableTools(root = document) {
556
+ for (const table of root.querySelectorAll("table[data-table-tools]")) enhance(table);
557
+ }
@@ -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
+ }
@@ -85,13 +85,13 @@
85
85
  on docs.danieldeusing.de, NOT for a doc opened from disk over file://. -->
86
86
  <link
87
87
  rel="stylesheet"
88
- href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.29.0/dist/danieldeusing-design.min.css"
89
- onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.29.0.min.css'"
88
+ href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.31.0/dist/danieldeusing-design.min.css"
89
+ onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.31.0.min.css'"
90
90
  />
91
91
  <link
92
92
  rel="stylesheet"
93
- href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.29.0/src/fonts.css"
94
- onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.29.0.fonts.css'"
93
+ href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.31.0/src/fonts.css"
94
+ onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.31.0.fonts.css'"
95
95
  />
96
96
 
97
97
  <style>
@@ -330,7 +330,7 @@ flowchart TD
330
330
 
331
331
  <script type="module">
332
332
  import { applyStoredTheme, initThemeSwitcher, initDropdowns, initSelects, initTerminal, initAnimToggle, initBurgerNav, initLsNav, initTableScroll, initDiagramZoom, initMinimap, initTooltips } from
333
- "https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.29.0/runtime/index.js";
333
+ "https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.31.0/runtime/index.js";
334
334
  applyStoredTheme();
335
335
  initThemeSwitcher();
336
336
  initDropdowns();