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