@danieldeusing/design 0.3.0 → 0.3.1

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.
@@ -1,4 +1,4 @@
1
- /*! danieldeusing-design v0.3.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
1
+ /*! danieldeusing-design v0.3.1 | MIT | https://github.com/danieldeusing/danieldeusing-design */
2
2
  /* ===== reset.css ===== */
3
3
  /*
4
4
  * danieldeusing-design — minimal reset.
@@ -740,19 +740,30 @@ details.fold > summary:focus-visible {
740
740
  would have added five more. */
741
741
  .tickstrip:not(:empty) { border: 1px solid var(--border); background: var(--card); margin: 0 0 1.25rem; }
742
742
  .ticktable { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
743
- .ticktable td { padding: 0.45rem 0.55rem; vertical-align: baseline; border: 0; white-space: nowrap; }
743
+ .ticktable td { padding: 0.5rem 0.75rem; vertical-align: baseline; border: 0; white-space: nowrap; }
744
744
  /* Only the last column may grow, so the four fixed columns keep the same width on every row. */
745
745
  .ticktable td.tick-stats { width: 100%; white-space: normal; }
746
746
  .tick { border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent); }
747
747
  .tick:last-child { border-bottom: 0; }
748
- /* The glyph carries the state WITHOUT colour as well as with it — two of the four themes are
749
- near-black and a printout has none, so vs vs has to survive on its own. */
750
- .tick-dot { font-size: 0.7em; width: 1rem; padding-right: 0 !important; }
751
- .tick--ok .tick-dot { color: var(--primary); }
752
- .tick--stale .tick-dot { color: var(--destructive); }
753
- .tick--never .tick-dot { color: var(--muted-foreground); }
754
- .tick-name { font-weight: 700; color: var(--primary); }
748
+ /* State reads THREE ways, so it survives every context: the GLYPH (two themes are near-black and
749
+ a printout has no colour at all), the COLOUR, and a left edge-marker that makes a bad row
750
+ findable by shape alone when the strip is scanned rather than read. */
751
+ .tick-dot {
752
+ font-size: 0.7em; width: 1.5rem; padding-right: 0 !important;
753
+ border-left: 3px solid transparent; padding-left: 0.6rem !important;
754
+ }
755
+ /* --success is per-theme and AA-measured (0.3.0). `ok` used to borrow --primary — the BRAND
756
+ colour — so a healthy poller and the page heading were the same ink, and "everything is fine"
757
+ had to be read rather than seen. */
758
+ .tick--ok .tick-dot { color: var(--success); border-left-color: color-mix(in srgb, var(--success) 45%, transparent); }
759
+ .tick--stale .tick-dot { color: var(--destructive); border-left-color: var(--destructive); }
760
+ .tick--never .tick-dot { color: var(--muted-foreground); border-left-color: color-mix(in srgb, var(--muted-foreground) 35%, transparent); }
761
+ /* The NAME is an identifier, not an accent. Making it --primary put brand colour on every row of
762
+ a strip whose entire job is that the exceptions stand out. */
763
+ .tick-name { font-weight: 700; color: var(--foreground); letter-spacing: 0.01em; }
755
764
  .tick-last, .tick-next { font-variant-numeric: tabular-nums; color: var(--muted-foreground); }
765
+ /* "next 57s" is a prediction; the age beside it is an observation. It should not compete. */
766
+ .tick-next { opacity: 0.72; }
756
767
  .tick--stale .tick-last { color: var(--destructive); font-weight: 700; }
757
768
  .tick--never .tick-last { font-style: italic; }
758
769
  /* The NUMBER is what gets scanned; its label is context. Weight and colour on the value alone
@@ -770,6 +781,51 @@ details.fold > summary:focus-visible {
770
781
  .ticktable td.tick-stats { display: block; width: auto; }
771
782
  }
772
783
 
784
+ /* ── zoomable diagrams ────────────────────────────────────────────────────────────────────────
785
+ An architecture diagram authored for a text column is unreadable at exactly the moment someone
786
+ needs it. Pair with `initDiagramZoom()`: every `.diagram` becomes a real button that opens
787
+ full-screen, fitted to the viewport, then zoomable and pannable.
788
+
789
+ The affordance has to be visible without hovering — this portal is read from a phone — so the
790
+ corner hint is always drawn, not revealed on :hover. */
791
+ .dgm-zoomable { position: relative; cursor: zoom-in; }
792
+ .dgm-zoomable::after {
793
+ content: "⤢"; position: absolute; top: 0.4rem; right: 0.5rem;
794
+ font-size: 0.85rem; line-height: 1; opacity: 0.45; pointer-events: none;
795
+ color: var(--muted-foreground);
796
+ }
797
+ .dgm-zoomable:hover::after, .dgm-zoomable:focus-visible::after { opacity: 1; color: var(--primary); }
798
+ .dgm-zoomable:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
799
+
800
+ html.dgm-locked, html.dgm-locked body { overflow: hidden; }
801
+
802
+ .dgm-overlay {
803
+ display: none; position: fixed; inset: 0; z-index: 90;
804
+ background: color-mix(in srgb, var(--background) 96%, transparent);
805
+ flex-direction: column;
806
+ }
807
+ .dgm-overlay.open { display: flex; }
808
+ .dgm-bar {
809
+ flex: none; display: flex; align-items: center; justify-content: flex-end; gap: 0.4rem;
810
+ padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); background: var(--card);
811
+ }
812
+ .dgm-btn {
813
+ font: inherit; font-size: 0.82rem; cursor: pointer; padding: 0.3rem 0.7rem; min-height: 32px;
814
+ background: transparent; border: 1px solid var(--border); color: var(--foreground);
815
+ }
816
+ .dgm-btn:hover { border-color: var(--primary); color: var(--primary); }
817
+ .dgm-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
818
+ .dgm-close { border-color: color-mix(in srgb, var(--destructive) 55%, transparent); color: var(--destructive); }
819
+ .dgm-close:hover { border-color: var(--destructive); color: var(--destructive); }
820
+ /* The stage clips; the artwork inside it is what moves. Transforming a wrapper rather than the
821
+ svg keeps mermaid's own geometry untouched. */
822
+ .dgm-stage { flex: 1; overflow: hidden; display: grid; place-items: center; cursor: grab; touch-action: none; }
823
+ .dgm-stage.is-grabbing { cursor: grabbing; }
824
+ .dgm-art { transform-origin: center center; will-change: transform; }
825
+ .dgm-art > svg, .dgm-art > img { display: block; max-width: none; max-height: none; }
826
+
827
+ @media print { .dgm-overlay, .dgm-zoomable::after { display: none !important; } }
828
+
773
829
  /* ===== chrome.css ===== */
774
830
  /*
775
831
  * chrome.css — the app chrome kit: sticky header bar, fixed status footer,
@@ -874,8 +930,20 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
874
930
  leaves a small labelled tab on the edge rather than nothing, so a hidden rail
875
931
  is still discoverable by someone who did not hide it themselves.
876
932
 
933
+ THE HEADER AND FOOTER WIN. The rail runs BETWEEN them — it never covers the bar
934
+ or the status line, and both keep their full width. Daniel: "the right sidebar
935
+ then should not overlay footer and header. Footer and header wins."
936
+ That also buys the better toggle behaviour: with the rail starting below the
937
+ bar, `ls -l` sits at ONE fixed spot whether the rail is open or shut, so the
938
+ only thing that changes is which way the arrow points. The reader never has to
939
+ go looking for a control that moved.
940
+
941
+ --ls-nav-top/--ls-nav-bottom are measured off the real chrome by initLsNav()
942
+ and written onto <html>; the values here are fallbacks that keep the layout
943
+ sane if the runtime never arrives.
944
+
877
945
  Width is one token so a consumer can widen it for longer trees. */
878
- :root { --ls-nav-w: 17rem; }
946
+ :root { --ls-nav-w: 17rem; --ls-nav-top: 3rem; --ls-nav-bottom: 2.2rem; }
879
947
  .ls-nav-show { display: none; }
880
948
 
881
949
  @media (min-width: 48.0625rem) {
@@ -890,19 +958,30 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
890
958
  html { --ls-nav-inset: 0rem; }
891
959
  html:has(.ls-nav):not([data-ls-nav="off"]) { --ls-nav-inset: var(--ls-nav-w); }
892
960
 
893
- /* Everything in normal flow shifts left, so content sits against the rail's
894
- left edge instead of running under it. The fixed footer has to be told
895
- separately: `inset-inline: 0` ignores the body's padding. */
896
- /* Deliberately NOT transitioned. Animating padding on <body> reflows the whole
961
+ /* Content shifts left to sit against the rail's edge — but the CHROME does not.
962
+ The header bar and the status footer pull back out to full width with a
963
+ negative margin, so the rail begins below one and ends above the other rather
964
+ than covering either. Applied to the body rather than to a `main` selector
965
+ because the design system cannot know a consumer's content wrapper: every
966
+ block child shifts, and the two that must not are named explicitly.
967
+
968
+ Deliberately NOT transitioned. Animating padding on <body> reflows the whole
897
969
  document every frame, and the pages this ships to render tables of hundreds of
898
970
  rows — the rail's own transform is composited and costs nothing, so the panel
899
- slides while the content reflows once. Animating both looked no better and
900
- made the containers dashboard stutter on toggle. */
971
+ slides while the content reflows once. */
901
972
  body { padding-right: var(--ls-nav-inset); }
902
- footer.status { right: var(--ls-nav-inset); }
903
-
973
+ /* Only the HEADER needs pulling back out. It is `position: sticky`, i.e. still
974
+ in flow, so the body's padding narrows it. The footer is `position: fixed`
975
+ with `inset-inline: 0`, which ignores body padding and is already full width
976
+ — giving it the same negative margin pushed it 272px PAST the viewport edge
977
+ (measured 1099px against an 842px viewport). Fixed and in-flow siblings look
978
+ alike in the markup and do not behave alike here. */
979
+ header.bar { margin-right: calc(-1 * var(--ls-nav-inset)); }
980
+
981
+ /* Between the chrome, never over it. */
904
982
  .ls-nav {
905
- position: fixed; top: 0; right: 0; bottom: 0; width: var(--ls-nav-w); z-index: 45;
983
+ position: fixed; top: var(--ls-nav-top); right: 0; bottom: var(--ls-nav-bottom);
984
+ width: var(--ls-nav-w); z-index: 25;
906
985
  display: flex; flex-direction: column;
907
986
  background: var(--card); border-left: 1px solid var(--border);
908
987
  }
@@ -932,13 +1011,17 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
932
1011
  outline: 2px solid color-mix(in srgb, var(--primary) 55%, transparent); outline-offset: -2px;
933
1012
  }
934
1013
 
935
- /* The tab that survives hiding. Sits on the right edge, mid-height, and only
936
- exists while the rail is away. */
1014
+ /* The tab that survives hiding TOP RIGHT, in the header band, which is where
1015
+ `ls -l` has always lived (it was a header dropdown before it was a rail) and
1016
+ where the rail's own title sits when it is open. An earlier version centred it
1017
+ vertically on the right edge: correct as a drawer handle, wrong here, because
1018
+ it moved the one control the reader is looking for to a place navigation has
1019
+ never been. Same corner open or closed. */
937
1020
  html[data-ls-nav="off"] .ls-nav-show {
938
1021
  display: inline-flex; align-items: center; gap: 0.4rem;
939
- position: fixed; top: 50%; right: 0; z-index: 45; transform: translateY(-50%);
1022
+ position: fixed; top: var(--ls-nav-top); right: 0; z-index: 25;
940
1023
  background: var(--card); border: 1px solid var(--border); border-right: 0;
941
- padding: 0.6rem 0.5rem; font-size: 0.8rem;
1024
+ border-top: 0; padding: 0.6rem 0.85rem; font-size: 0.9rem; font-weight: 700;
942
1025
  }
943
1026
  }
944
1027
 
@@ -1,2 +1,2 @@
1
- /*! danieldeusing-design v0.3.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;--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;--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;--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;--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}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: 0.75rem;line-height: 1.5;overflow-x: hidden;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale}button:not(:disabled),summary,[role="button"]{cursor: pointer}body::after{content: "";position: fixed;inset: 0;z-index: 40;pointer-events: none;background: repeating-linear-gradient( 0deg,rgba(0,0,0,0.25) 0px,rgba(0,0,0,0.25) 1px,transparent 1px,transparent 3px );opacity: var(--scanline-opacity)}::selection{background: var(--primary);color: var(--primary-foreground)}.glow{color: var(--primary);text-shadow: 0 0 8px var(--glow)}.glow-lg{color: var(--primary);text-shadow: 0 0 12px var(--glow)}.prompt{font-size: 0.75rem;font-weight: 700;color: var(--muted-foreground)}.prompt::before{content: "$ ";color: var(--primary)}.comment::before{content: "# ";color: var(--border)}.cursor-block{display: inline-block;width: 0.55em;height: 1em;margin-left: 8px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em);animation: ddd-blink 1.1s step-end infinite}@keyframes ddd-blink{50%{opacity: 0}}.btn-terminal{display: inline-block;background: var(--primary);color: var(--primary-foreground);font-weight: 700;padding: 12px 24px;box-shadow: 0 0 16px var(--glow);transition: box-shadow 0.15s ease,transform 0.15s ease}.btn-terminal::before{content: "> "}.btn-terminal:hover{box-shadow: 0 0 28px var(--glow);transform: translateY(-2px)}.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: 0;box-shadow: 0 0 20px var(--glow-soft)}.dropdown-panel--down{top: calc(100% + 8px);bottom: auto;left: 0;right: auto}.dropdown-item{display: flex;align-items: center;gap: 8px;width: 100%;padding: 5px 14px;text-align: left;font: inherit;line-height: 1.5;background: none;border: 0;color: var(--muted-foreground);cursor: pointer;white-space: nowrap;transition: color 0.15s ease}.dropdown-item:hover{color: var(--primary);background: var(--muted)}.dropdown-item[aria-current="true"],html:not([data-theme]) .dropdown-item[data-theme-value="warm"],html[data-theme="green"] .dropdown-item[data-theme-value="green"],html[data-theme="mono"] .dropdown-item[data-theme-value="mono"],html[data-theme="paper"] .dropdown-item[data-theme-value="paper"],html[data-theme="warm"] .dropdown-item[data-theme-value="warm"]{color: var(--primary);text-shadow: 0 0 8px var(--glow)}.eli5{margin: 1.25em 0;padding: 0.7em 1em;color: var(--foreground);background: color-mix(in srgb,var(--primary) 7%,var(--card));border: 1px solid var(--border);border-left: 3px solid var(--primary);border-radius: var(--radius);font-size: 0.92em;line-height: 1.6}.eli5::before{content: "ELI5";display: inline-flex;align-items: center;justify-content: center;margin-right: 0.6em;vertical-align: 0.08em;font-family: var(--font-mono);font-size: 0.72em;font-weight: 700;line-height: 1;letter-spacing: 0.04em;padding: 0.34em 0.6em;border-radius: 5px;background: #b42318;color: #fff}.eli5-term{font-weight: 700;color: var(--primary)}html.anim-off *,html.anim-off *::before,html.anim-off *::after{animation: none !important}html.anim-off .cursor-block{display: none}@media (prefers-reduced-motion: no-preference){html.term-anim [data-term] .prompt:not(.term-live),html.term-anim [data-term] .prompt:not(.term-live)::before{color: transparent}html.term-anim [data-term] [data-term-out]:not(.term-show){visibility: hidden}html.term-anim [data-term] [data-term-out].term-show{animation: ddd-term-output 0.2s steps(3,end) both}@keyframes ddd-term-output{from{opacity: 0}}html.term-anim [data-term] [data-term-out].term-show>*{animation: ddd-term-output 0.25s steps(3,end) both}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(2){animation-delay: 0.09s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(3){animation-delay: 0.18s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(4){animation-delay: 0.27s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(5){animation-delay: 0.36s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(6){animation-delay: 0.45s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(7){animation-delay: 0.54s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(8){animation-delay: 0.63s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(9){animation-delay: 0.72s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(10){animation-delay: 0.81s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(n + 11){animation-delay: 0.9s}.term-caret{display: inline-block;width: 0.55em;height: 1em;margin-left: 2px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em)}}.tabs{display: flex;flex-wrap: wrap;gap: 0.15rem;margin: 1.35rem 0 0;border-bottom: 2px solid color-mix(in srgb,var(--border) 80%,transparent)}.tab{background: transparent;border: 0;margin-bottom: -2px;font: inherit;font-size: 0.86em;cursor: pointer;padding: 0.5rem 0.95rem;color: var(--muted-foreground);transition: background 0.12s ease,color 0.12s ease}.tab:hover{color: var(--primary);background: color-mix(in srgb,var(--primary) 10%,transparent)}.tab[aria-selected="true"]{background: var(--primary);color: var(--background);font-weight: 700}.tab:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}@media (pointer: coarse),(max-width: 40rem){.tab{padding: 0.7rem 0.8rem;min-height: 44px}}section.doc.tab-panel{margin-top: 1.35rem}details.fold{border-top: 1px solid color-mix(in srgb,currentColor 15%,transparent)}details.fold>summary{display: flex;align-items: center;gap: 0.5rem;cursor: pointer;list-style: none;padding: 0.55rem 0;font-size: 0.85rem;color: var(--muted-foreground)}details.fold>summary::-webkit-details-marker{display: none}details.fold>summary::before{content: "\25B8";flex: none;opacity: 0.6}details.fold[open]>summary::before{content: "\25BE"}details.fold>summary:hover{color: var(--primary)}details.fold>summary:focus-visible{outline: 2px solid color-mix(in srgb,currentColor 45%,transparent);outline-offset: 2px}.fold-body{font-size: 0.9rem;padding: 0 0 0.7rem 1.1rem}.fold-body>* + *{margin-top: 0.6rem}.legend{list-style: none;padding: 0;margin: 0.7rem 0 0;display: grid;gap: 0.4rem 1.4rem;font-size: 0.86rem}@media (min-width: 52rem){.legend{grid-template-columns: repeat(2,minmax(0,1fr))}}.legend>li{line-height: 1.6}.legend>li>:first-child{margin-right: 0.45rem}.tickstrip:not(:empty){border: 1px solid var(--border);background: var(--card);margin: 0 0 1.25rem}.ticktable{width: 100%;border-collapse: collapse;font-size: 0.82rem}.ticktable td{padding: 0.45rem 0.55rem;vertical-align: baseline;border: 0;white-space: nowrap}.ticktable td.tick-stats{width: 100%;white-space: normal}.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: 1rem;padding-right: 0 !important}.tick--ok .tick-dot{color: var(--primary)}.tick--stale .tick-dot{color: var(--destructive)}.tick--never .tick-dot{color: var(--muted-foreground)}.tick-name{font-weight: 700;color: var(--primary)}.tick-last,.tick-next{font-variant-numeric: tabular-nums;color: var(--muted-foreground)}.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}}.skip-link{position: absolute;left: -999px;top: 0;z-index: 100;background: var(--card);border: 1px solid var(--primary);padding: 0.5rem 1rem;color: var(--primary)}.skip-link:focus{left: 0.5rem;top: 0.5rem}.visually-hidden{position: absolute;width: 1px;height: 1px;overflow: hidden;clip: rect(0 0 0 0);white-space: nowrap}header.bar{position: sticky;top: 0;z-index: 30;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.6rem 1.25rem;border-bottom: 1px solid var(--border);background: var(--card)}header.bar .brand{display: inline-flex;align-items: center;font-weight: 700;font-size: 1rem}header.bar .brand,header.bar .brand a{color: inherit;text-decoration: none}header.bar .dropdown summary{padding: 0.6rem 0.4rem}.bar-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}footer.status{position: fixed;inset-inline: 0;bottom: 0;z-index: 50;min-height: 2rem;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.35rem 1.25rem;border-top: 1px solid var(--border);background: var(--card);font-size: 0.7rem}footer.status .dropdown summary,footer.status .anim-toggle{padding: 0.55rem 0.4rem}.status-left{color: var(--muted-foreground);min-width: 0;overflow: hidden;text-overflow: ellipsis;white-space: nowrap}.status-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}.sep{width: 1px;height: 0.9rem;background: var(--border)}.doc-link{color: var(--muted-foreground);text-decoration: none;transition: color 0.15s ease}.doc-link:hover{color: var(--primary)}.ls-row{display: flex;align-items: baseline;gap: 0.9rem}.ls-perm{color: var(--muted-foreground);font-size: 0.72rem;letter-spacing: 0}.ls-row--sub .ls-name{padding-left: 1rem}.dropdown-panel.ls-panel{min-width: 17rem;left: auto !important;right: 0}:root{--ls-nav-w: 17rem}.ls-nav-show{display: none}@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)}footer.status{right: var(--ls-nav-inset)}.ls-nav{position: fixed;top: 0;right: 0;bottom: 0;width: var(--ls-nav-w);z-index: 45;display: flex;flex-direction: column;background: var(--card);border-left: 1px solid var(--border)}html:not(.anim-off) .ls-nav,html:not(.anim-off) .ls-nav-show{transition: transform 0.18s ease}html[data-ls-nav="off"] .ls-nav{transform: translateX(100%)}.ls-nav-head{flex: none;display: flex;align-items: center;justify-content: space-between;gap: 0.5rem;padding: 0.6rem 0.5rem 0.6rem 0.85rem;border-bottom: 1px solid var(--border)}.ls-nav-title{font-weight: 700;font-size: 0.9rem}.ls-nav .ls-panel{flex: 1;min-height: 0;overflow-y: auto;list-style: none;margin: 0;padding: 0.5rem 0 1rem}.ls-nav-hide,.ls-nav-show{background: none;border: 0;font: inherit;cursor: pointer;line-height: 1;color: var(--muted-foreground);padding: 0.4rem 0.55rem}.ls-nav-hide:hover,.ls-nav-show:hover{color: var(--primary)}.ls-nav-hide:focus-visible,.ls-nav-show:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}html[data-ls-nav="off"] .ls-nav-show{display: inline-flex;align-items: center;gap: 0.4rem;position: fixed;top: 50%;right: 0;z-index: 45;transform: translateY(-50%);background: var(--card);border: 1px solid var(--border);border-right: 0;padding: 0.6rem 0.5rem;font-size: 0.8rem}}.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,.ls-nav-show{display: none}.mobile-nav{display: flex;flex-direction: column}.mobile-nav .mobile-item{display: flex;align-items: baseline;gap: 0.9rem;padding: 0.65rem 0;text-decoration: none;color: var(--foreground);font-size: 0.9rem;border-bottom: 1px solid var(--border)}.mobile-nav li:last-child .mobile-item{border-bottom: 0}.mobile-nav .mobile-item:active .ls-name,.mobile-nav .mobile-item:hover .ls-name{color: var(--primary)}footer.status{display: none}.mobile-footer{display: flex;flex-direction: column;gap: 0.55rem;margin-top: 0.8rem;border-top: 1px solid var(--border);padding-top: 0.8rem;font-size: 0.85rem}.mobile-footer .doc-link{padding: 0.2rem 0}.mobile-footer .mobile-theme summary{list-style: none;display: flex;align-items: center;gap: 0.5rem;color: var(--muted-foreground);cursor: pointer;padding: 0.2rem 0}.mobile-footer .mobile-theme summary::-webkit-details-marker{display: none}.mobile-footer .mf-chev{margin-left: auto;opacity: 0.7}.mobile-footer .mf-panel{display: flex;flex-direction: column;margin-top: 0.3rem}.mobile-footer .mf-panel .dropdown-item{text-align: left;padding: 0.4rem 0.6rem}.mobile-footer .anim-toggle{display: inline-flex;gap: 0.5rem;align-items: center;background: none;border: 0;padding: 0.2rem 0;color: var(--muted-foreground);font: inherit;cursor: pointer}}[data-tip]{cursor: help}span[data-tip],th[data-tip]{border-bottom: 1px dotted var(--muted-foreground)}#ddtip{position: fixed;z-index: 9999;display: none;max-width: 340px;background: var(--background);color: var(--foreground);border: 1px solid var(--muted-foreground);border-radius: 4px;padding: 7px 10px;font-size: 0.72rem;line-height: 1.45;font-weight: 400;text-align: left;white-space: normal;pointer-events: none;box-shadow: 0 4px 14px rgba(0,0,0,0.25)}@media print{@page{margin: 14mm 12mm}:root,html,html[data-theme]{--background: #ffffff;--foreground: #111111;--card: #ffffff;--card-foreground: #111111;--popover: #ffffff;--popover-foreground: #111111;--primary: #000000;--primary-foreground: #ffffff;--secondary: #ffffff;--secondary-foreground: #111111;--muted: #ffffff;--muted-foreground: #444444;--accent: #000000;--accent-foreground: #ffffff;--destructive: #7a1c1c;--border: #9a9a9a;--input: #9a9a9a;--ring: #000000;--glow: transparent;--glow-soft: transparent;--scanline-opacity: 0}html{zoom: 1 !important;font-size: 16px;scroll-behavior: auto}body{background: #ffffff;color: var(--foreground);font-size: 12px;line-height: 1.45;overflow: visible}body::after{display: none !important}*,*::before,*::after{animation: none !important;transition: none !important;text-shadow: none !important;box-shadow: none !important}html.term-anim [data-term] [data-term-out],html.term-anim [data-term] [data-term-out]:not(.term-show),html.term-anim [data-term] [data-term-out]>*{visibility: visible !important;opacity: 1 !important}html.term-anim [data-term] .prompt:not(.term-live){color: var(--muted-foreground) !important}html.term-anim [data-term] .prompt:not(.term-live)::before{color: var(--primary) !important}header.bar,footer.status,.site-nav,.nav-burger,.mobile-nav,.mobile-footer,.skip-link,.dropdown,.dropdown-panel,.anim-toggle,.cursor-block,.term-caret,.ascii-rule,.toc,#ddtip{display: none !important}body{padding-right: 0 !important}[data-tip]{cursor: auto}span[data-tip],th[data-tip]{border-bottom: 0}.wrap{max-width: none !important;margin: 0 !important;padding: 0 !important}.layout{display: block !important}.content{min-width: 0}h1,.title{font-size: 1.5rem !important}h2{font-size: 1.15rem !important}h3,.sub{font-size: 1rem !important}h4{font-size: 0.9rem !important}.prompt{font-size: 0.7rem}h1,h2,h3,h4,.prompt,.toc-label{break-after: avoid-page;break-inside: avoid}p,li,blockquote{orphans: 3;widows: 3}pre,figure,blockquote,tr,img,.eli5,.card-terminal{break-inside: avoid}svg{break-inside: auto}thead{display: table-header-group}.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.3.1 | MIT | https://github.com/danieldeusing/danieldeusing-design */
2
+ *,*::before,*::after{box-sizing: border-box}*{margin: 0}html{-webkit-text-size-adjust: 100%;text-size-adjust: 100%;tab-size: 4}body{min-height: 100vh;min-height: 100dvh;line-height: 1.5}img,picture,video,canvas,svg{display: block;max-width: 100%}input,button,textarea,select{font: inherit;color: inherit}p,h1,h2,h3,h4,h5,h6{overflow-wrap: break-word}ul,ol{list-style: none;padding: 0}a{color: inherit;text-decoration: none}table{border-collapse: collapse}:root{--background: #f5efe2;--foreground: #43352a;--card: #efe7d4;--card-foreground: #43352a;--popover: #efe7d4;--popover-foreground: #43352a;--primary: #8a4516;--primary-foreground: #f8f3e8;--secondary: #ece3cf;--secondary-foreground: #43352a;--muted: #ece3cf;--muted-foreground: #71614e;--accent: #8a4516;--accent-foreground: #f8f3e8;--destructive: #a02c2c;--success: #1a6b2e;--warning: #845600;--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;--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;--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;--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}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: 0.75rem;line-height: 1.5;overflow-x: hidden;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale}button:not(:disabled),summary,[role="button"]{cursor: pointer}body::after{content: "";position: fixed;inset: 0;z-index: 40;pointer-events: none;background: repeating-linear-gradient( 0deg,rgba(0,0,0,0.25) 0px,rgba(0,0,0,0.25) 1px,transparent 1px,transparent 3px );opacity: var(--scanline-opacity)}::selection{background: var(--primary);color: var(--primary-foreground)}.glow{color: var(--primary);text-shadow: 0 0 8px var(--glow)}.glow-lg{color: var(--primary);text-shadow: 0 0 12px var(--glow)}.prompt{font-size: 0.75rem;font-weight: 700;color: var(--muted-foreground)}.prompt::before{content: "$ ";color: var(--primary)}.comment::before{content: "# ";color: var(--border)}.cursor-block{display: inline-block;width: 0.55em;height: 1em;margin-left: 8px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em);animation: ddd-blink 1.1s step-end infinite}@keyframes ddd-blink{50%{opacity: 0}}.btn-terminal{display: inline-block;background: var(--primary);color: var(--primary-foreground);font-weight: 700;padding: 12px 24px;box-shadow: 0 0 16px var(--glow);transition: box-shadow 0.15s ease,transform 0.15s ease}.btn-terminal::before{content: "> "}.btn-terminal:hover{box-shadow: 0 0 28px var(--glow);transform: translateY(-2px)}.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: 0;box-shadow: 0 0 20px var(--glow-soft)}.dropdown-panel--down{top: calc(100% + 8px);bottom: auto;left: 0;right: auto}.dropdown-item{display: flex;align-items: center;gap: 8px;width: 100%;padding: 5px 14px;text-align: left;font: inherit;line-height: 1.5;background: none;border: 0;color: var(--muted-foreground);cursor: pointer;white-space: nowrap;transition: color 0.15s ease}.dropdown-item:hover{color: var(--primary);background: var(--muted)}.dropdown-item[aria-current="true"],html:not([data-theme]) .dropdown-item[data-theme-value="warm"],html[data-theme="green"] .dropdown-item[data-theme-value="green"],html[data-theme="mono"] .dropdown-item[data-theme-value="mono"],html[data-theme="paper"] .dropdown-item[data-theme-value="paper"],html[data-theme="warm"] .dropdown-item[data-theme-value="warm"]{color: var(--primary);text-shadow: 0 0 8px var(--glow)}.eli5{margin: 1.25em 0;padding: 0.7em 1em;color: var(--foreground);background: color-mix(in srgb,var(--primary) 7%,var(--card));border: 1px solid var(--border);border-left: 3px solid var(--primary);border-radius: var(--radius);font-size: 0.92em;line-height: 1.6}.eli5::before{content: "ELI5";display: inline-flex;align-items: center;justify-content: center;margin-right: 0.6em;vertical-align: 0.08em;font-family: var(--font-mono);font-size: 0.72em;font-weight: 700;line-height: 1;letter-spacing: 0.04em;padding: 0.34em 0.6em;border-radius: 5px;background: #b42318;color: #fff}.eli5-term{font-weight: 700;color: var(--primary)}html.anim-off *,html.anim-off *::before,html.anim-off *::after{animation: none !important}html.anim-off .cursor-block{display: none}@media (prefers-reduced-motion: no-preference){html.term-anim [data-term] .prompt:not(.term-live),html.term-anim [data-term] .prompt:not(.term-live)::before{color: transparent}html.term-anim [data-term] [data-term-out]:not(.term-show){visibility: hidden}html.term-anim [data-term] [data-term-out].term-show{animation: ddd-term-output 0.2s steps(3,end) both}@keyframes ddd-term-output{from{opacity: 0}}html.term-anim [data-term] [data-term-out].term-show>*{animation: ddd-term-output 0.25s steps(3,end) both}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(2){animation-delay: 0.09s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(3){animation-delay: 0.18s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(4){animation-delay: 0.27s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(5){animation-delay: 0.36s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(6){animation-delay: 0.45s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(7){animation-delay: 0.54s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(8){animation-delay: 0.63s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(9){animation-delay: 0.72s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(10){animation-delay: 0.81s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(n + 11){animation-delay: 0.9s}.term-caret{display: inline-block;width: 0.55em;height: 1em;margin-left: 2px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em)}}.tabs{display: flex;flex-wrap: wrap;gap: 0.15rem;margin: 1.35rem 0 0;border-bottom: 2px solid color-mix(in srgb,var(--border) 80%,transparent)}.tab{background: transparent;border: 0;margin-bottom: -2px;font: inherit;font-size: 0.86em;cursor: pointer;padding: 0.5rem 0.95rem;color: var(--muted-foreground);transition: background 0.12s ease,color 0.12s ease}.tab:hover{color: var(--primary);background: color-mix(in srgb,var(--primary) 10%,transparent)}.tab[aria-selected="true"]{background: var(--primary);color: var(--background);font-weight: 700}.tab:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}@media (pointer: coarse),(max-width: 40rem){.tab{padding: 0.7rem 0.8rem;min-height: 44px}}section.doc.tab-panel{margin-top: 1.35rem}details.fold{border-top: 1px solid color-mix(in srgb,currentColor 15%,transparent)}details.fold>summary{display: flex;align-items: center;gap: 0.5rem;cursor: pointer;list-style: none;padding: 0.55rem 0;font-size: 0.85rem;color: var(--muted-foreground)}details.fold>summary::-webkit-details-marker{display: none}details.fold>summary::before{content: "\25B8";flex: none;opacity: 0.6}details.fold[open]>summary::before{content: "\25BE"}details.fold>summary:hover{color: var(--primary)}details.fold>summary:focus-visible{outline: 2px solid color-mix(in srgb,currentColor 45%,transparent);outline-offset: 2px}.fold-body{font-size: 0.9rem;padding: 0 0 0.7rem 1.1rem}.fold-body>* + *{margin-top: 0.6rem}.legend{list-style: none;padding: 0;margin: 0.7rem 0 0;display: grid;gap: 0.4rem 1.4rem;font-size: 0.86rem}@media (min-width: 52rem){.legend{grid-template-columns: repeat(2,minmax(0,1fr))}}.legend>li{line-height: 1.6}.legend>li>:first-child{margin-right: 0.45rem}.tickstrip:not(:empty){border: 1px solid var(--border);background: var(--card);margin: 0 0 1.25rem}.ticktable{width: 100%;border-collapse: collapse;font-size: 0.82rem}.ticktable td{padding: 0.5rem 0.75rem;vertical-align: baseline;border: 0;white-space: nowrap}.ticktable td.tick-stats{width: 100%;white-space: normal}.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}}.dgm-zoomable{position: relative;cursor: zoom-in}.dgm-zoomable::after{content: "⤢";position: absolute;top: 0.4rem;right: 0.5rem;font-size: 0.85rem;line-height: 1;opacity: 0.45;pointer-events: none;color: var(--muted-foreground)}.dgm-zoomable:hover::after,.dgm-zoomable:focus-visible::after{opacity: 1;color: var(--primary)}.dgm-zoomable:focus-visible{outline: 2px solid var(--primary);outline-offset: 3px}html.dgm-locked,html.dgm-locked body{overflow: hidden}.dgm-overlay{display: none;position: fixed;inset: 0;z-index: 90;background: color-mix(in srgb,var(--background) 96%,transparent);flex-direction: column}.dgm-overlay.open{display: flex}.dgm-bar{flex: none;display: flex;align-items: center;justify-content: flex-end;gap: 0.4rem;padding: 0.5rem 0.75rem;border-bottom: 1px solid var(--border);background: var(--card)}.dgm-btn{font: inherit;font-size: 0.82rem;cursor: pointer;padding: 0.3rem 0.7rem;min-height: 32px;background: transparent;border: 1px solid var(--border);color: var(--foreground)}.dgm-btn:hover{border-color: var(--primary);color: var(--primary)}.dgm-btn:focus-visible{outline: 2px solid var(--primary);outline-offset: 1px}.dgm-close{border-color: color-mix(in srgb,var(--destructive) 55%,transparent);color: var(--destructive)}.dgm-close:hover{border-color: var(--destructive);color: var(--destructive)}.dgm-stage{flex: 1;overflow: hidden;display: grid;place-items: center;cursor: grab;touch-action: none}.dgm-stage.is-grabbing{cursor: grabbing}.dgm-art{transform-origin: center center;will-change: transform}.dgm-art>svg,.dgm-art>img{display: block;max-width: none;max-height: none}@media print{.dgm-overlay,.dgm-zoomable::after{display: none !important}}.skip-link{position: absolute;left: -999px;top: 0;z-index: 100;background: var(--card);border: 1px solid var(--primary);padding: 0.5rem 1rem;color: var(--primary)}.skip-link:focus{left: 0.5rem;top: 0.5rem}.visually-hidden{position: absolute;width: 1px;height: 1px;overflow: hidden;clip: rect(0 0 0 0);white-space: nowrap}header.bar{position: sticky;top: 0;z-index: 30;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.6rem 1.25rem;border-bottom: 1px solid var(--border);background: var(--card)}header.bar .brand{display: inline-flex;align-items: center;font-weight: 700;font-size: 1rem}header.bar .brand,header.bar .brand a{color: inherit;text-decoration: none}header.bar .dropdown summary{padding: 0.6rem 0.4rem}.bar-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}footer.status{position: fixed;inset-inline: 0;bottom: 0;z-index: 50;min-height: 2rem;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.35rem 1.25rem;border-top: 1px solid var(--border);background: var(--card);font-size: 0.7rem}footer.status .dropdown summary,footer.status .anim-toggle{padding: 0.55rem 0.4rem}.status-left{color: var(--muted-foreground);min-width: 0;overflow: hidden;text-overflow: ellipsis;white-space: nowrap}.status-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}.sep{width: 1px;height: 0.9rem;background: var(--border)}.doc-link{color: var(--muted-foreground);text-decoration: none;transition: color 0.15s ease}.doc-link:hover{color: var(--primary)}.ls-row{display: flex;align-items: baseline;gap: 0.9rem}.ls-perm{color: var(--muted-foreground);font-size: 0.72rem;letter-spacing: 0}.ls-row--sub .ls-name{padding-left: 1rem}.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}.ls-nav-show{display: none}@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{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;background: var(--card);border-left: 1px solid var(--border)}html:not(.anim-off) .ls-nav,html:not(.anim-off) .ls-nav-show{transition: transform 0.18s ease}html[data-ls-nav="off"] .ls-nav{transform: translateX(100%)}.ls-nav-head{flex: none;display: flex;align-items: center;justify-content: space-between;gap: 0.5rem;padding: 0.6rem 0.5rem 0.6rem 0.85rem;border-bottom: 1px solid var(--border)}.ls-nav-title{font-weight: 700;font-size: 0.9rem}.ls-nav .ls-panel{flex: 1;min-height: 0;overflow-y: auto;list-style: none;margin: 0;padding: 0.5rem 0 1rem}.ls-nav-hide,.ls-nav-show{background: none;border: 0;font: inherit;cursor: pointer;line-height: 1;color: var(--muted-foreground);padding: 0.4rem 0.55rem}.ls-nav-hide:hover,.ls-nav-show:hover{color: var(--primary)}.ls-nav-hide:focus-visible,.ls-nav-show:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}html[data-ls-nav="off"] .ls-nav-show{display: inline-flex;align-items: center;gap: 0.4rem;position: fixed;top: var(--ls-nav-top);right: 0;z-index: 25;background: var(--card);border: 1px solid var(--border);border-right: 0;border-top: 0;padding: 0.6rem 0.85rem;font-size: 0.9rem;font-weight: 700}}.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,.ls-nav-show{display: none}.mobile-nav{display: flex;flex-direction: column}.mobile-nav .mobile-item{display: flex;align-items: baseline;gap: 0.9rem;padding: 0.65rem 0;text-decoration: none;color: var(--foreground);font-size: 0.9rem;border-bottom: 1px solid var(--border)}.mobile-nav li:last-child .mobile-item{border-bottom: 0}.mobile-nav .mobile-item:active .ls-name,.mobile-nav .mobile-item:hover .ls-name{color: var(--primary)}footer.status{display: none}.mobile-footer{display: flex;flex-direction: column;gap: 0.55rem;margin-top: 0.8rem;border-top: 1px solid var(--border);padding-top: 0.8rem;font-size: 0.85rem}.mobile-footer .doc-link{padding: 0.2rem 0}.mobile-footer .mobile-theme summary{list-style: none;display: flex;align-items: center;gap: 0.5rem;color: var(--muted-foreground);cursor: pointer;padding: 0.2rem 0}.mobile-footer .mobile-theme summary::-webkit-details-marker{display: none}.mobile-footer .mf-chev{margin-left: auto;opacity: 0.7}.mobile-footer .mf-panel{display: flex;flex-direction: column;margin-top: 0.3rem}.mobile-footer .mf-panel .dropdown-item{text-align: left;padding: 0.4rem 0.6rem}.mobile-footer .anim-toggle{display: inline-flex;gap: 0.5rem;align-items: center;background: none;border: 0;padding: 0.2rem 0;color: var(--muted-foreground);font: inherit;cursor: pointer}}[data-tip]{cursor: help}span[data-tip],th[data-tip]{border-bottom: 1px dotted var(--muted-foreground)}#ddtip{position: fixed;z-index: 9999;display: none;max-width: 340px;background: var(--background);color: var(--foreground);border: 1px solid var(--muted-foreground);border-radius: 4px;padding: 7px 10px;font-size: 0.72rem;line-height: 1.45;font-weight: 400;text-align: left;white-space: normal;pointer-events: none;box-shadow: 0 4px 14px rgba(0,0,0,0.25)}@media print{@page{margin: 14mm 12mm}:root,html,html[data-theme]{--background: #ffffff;--foreground: #111111;--card: #ffffff;--card-foreground: #111111;--popover: #ffffff;--popover-foreground: #111111;--primary: #000000;--primary-foreground: #ffffff;--secondary: #ffffff;--secondary-foreground: #111111;--muted: #ffffff;--muted-foreground: #444444;--accent: #000000;--accent-foreground: #ffffff;--destructive: #7a1c1c;--border: #9a9a9a;--input: #9a9a9a;--ring: #000000;--glow: transparent;--glow-soft: transparent;--scanline-opacity: 0}html{zoom: 1 !important;font-size: 16px;scroll-behavior: auto}body{background: #ffffff;color: var(--foreground);font-size: 12px;line-height: 1.45;overflow: visible}body::after{display: none !important}*,*::before,*::after{animation: none !important;transition: none !important;text-shadow: none !important;box-shadow: none !important}html.term-anim [data-term] [data-term-out],html.term-anim [data-term] [data-term-out]:not(.term-show),html.term-anim [data-term] [data-term-out]>*{visibility: visible !important;opacity: 1 !important}html.term-anim [data-term] .prompt:not(.term-live){color: var(--muted-foreground) !important}html.term-anim [data-term] .prompt:not(.term-live)::before{color: var(--primary) !important}header.bar,footer.status,.site-nav,.nav-burger,.mobile-nav,.mobile-footer,.skip-link,.dropdown,.dropdown-panel,.anim-toggle,.cursor-block,.term-caret,.ascii-rule,.toc,#ddtip{display: none !important}body{padding-right: 0 !important}[data-tip]{cursor: auto}span[data-tip],th[data-tip]{border-bottom: 0}.wrap{max-width: none !important;margin: 0 !important;padding: 0 !important}.layout{display: block !important}.content{min-width: 0}h1,.title{font-size: 1.5rem !important}h2{font-size: 1.15rem !important}h3,.sub{font-size: 1rem !important}h4{font-size: 0.9rem !important}.prompt{font-size: 0.7rem}h1,h2,h3,h4,.prompt,.toc-label{break-after: avoid-page;break-inside: avoid}p,li,blockquote{orphans: 3;widows: 3}pre,figure,blockquote,tr,img,.eli5,.card-terminal{break-inside: avoid}svg{break-inside: auto}thead{display: table-header-group}.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.0",
3
+ "version": "0.3.1",
4
4
  "description": "The danieldeusing terminal design system — framework-agnostic CSS tokens, components, and a vanilla-JS runtime for the CRT/JetBrains-Mono look used across danieldeusing.de, seedr, and briefs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,156 @@
1
+ /*
2
+ * diagramzoom.js — open any `.diagram` full-screen, then zoom and pan it.
3
+ *
4
+ * Markup contract: whatever `.diagram` wraps (a mermaid <svg>, an <img>). No
5
+ * per-diagram attributes — a diagram is zoomable by virtue of being a diagram,
6
+ * which is the point: an architecture diagram that has to fit a text column is
7
+ * unreadable at exactly the moment someone needs it.
8
+ *
9
+ * Behaviour: click (or Enter/Space — the wrapper becomes a real button, so this
10
+ * works from the keyboard and is announced) opens an overlay with the diagram
11
+ * scaled to FIT the viewport. Wheel zooms about the pointer, drag pans, +/-/0
12
+ * and the on-screen controls do the same. Escape or a click on the backdrop
13
+ * closes and returns focus to the diagram that was opened.
14
+ *
15
+ * The SVG is CLONED into the overlay rather than moved: mermaid holds references
16
+ * to the nodes it rendered and re-runs against them (a folded or tabbed diagram
17
+ * is redrawn when it becomes visible), so moving the original out of the document
18
+ * and back is how you get a diagram that silently stops updating.
19
+ */
20
+ const FIT_MARGIN = 0.92; // leave a little air around a fitted diagram
21
+ const MIN_SCALE = 0.2;
22
+ const MAX_SCALE = 12;
23
+
24
+ export function initDiagramZoom(selector = ".diagram") {
25
+ const diagrams = Array.from(document.querySelectorAll(selector));
26
+ if (!diagrams.length) return;
27
+
28
+ let overlay, stage, art, closeBtn, opener;
29
+ let scale = 1, tx = 0, ty = 0, dragging = false, lastX = 0, lastY = 0;
30
+
31
+ const apply = () => { art.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`; };
32
+ const zoomTo = (next, cx, cy) => {
33
+ const clamped = Math.min(MAX_SCALE, Math.max(MIN_SCALE, next));
34
+ if (clamped === scale) return;
35
+ // keep the point under the cursor stationary
36
+ const rect = stage.getBoundingClientRect();
37
+ const px = (cx ?? rect.left + rect.width / 2) - rect.left - rect.width / 2;
38
+ const py = (cy ?? rect.top + rect.height / 2) - rect.top - rect.height / 2;
39
+ const ratio = clamped / scale;
40
+ tx = px - (px - tx) * ratio;
41
+ ty = py - (py - ty) * ratio;
42
+ scale = clamped;
43
+ apply();
44
+ };
45
+
46
+ const build = () => {
47
+ overlay = document.createElement("div");
48
+ overlay.className = "dgm-overlay";
49
+ overlay.setAttribute("role", "dialog");
50
+ overlay.setAttribute("aria-modal", "true");
51
+ overlay.setAttribute("aria-label", "diagram, zoomable");
52
+ overlay.innerHTML =
53
+ '<div class="dgm-bar">' +
54
+ '<button type="button" class="dgm-btn" data-dgm="out" aria-label="zoom out">&minus;</button>' +
55
+ '<button type="button" class="dgm-btn" data-dgm="reset" aria-label="fit to screen">fit</button>' +
56
+ '<button type="button" class="dgm-btn" data-dgm="in" aria-label="zoom in">+</button>' +
57
+ '<button type="button" class="dgm-btn dgm-close" data-dgm="close" aria-label="close">close &times;</button>' +
58
+ "</div><div class=\"dgm-stage\"><div class=\"dgm-art\"></div></div>";
59
+ document.body.appendChild(overlay);
60
+ stage = overlay.querySelector(".dgm-stage");
61
+ art = overlay.querySelector(".dgm-art");
62
+ closeBtn = overlay.querySelector(".dgm-close");
63
+
64
+ overlay.addEventListener("click", (e) => {
65
+ const act = e.target.closest("[data-dgm]")?.dataset.dgm;
66
+ if (act === "close" || e.target === stage || e.target === overlay) return close();
67
+ if (act === "in") return zoomTo(scale * 1.3);
68
+ if (act === "out") return zoomTo(scale / 1.3);
69
+ if (act === "reset") return fit();
70
+ });
71
+ stage.addEventListener("wheel", (e) => {
72
+ e.preventDefault();
73
+ zoomTo(scale * (e.deltaY < 0 ? 1.12 : 1 / 1.12), e.clientX, e.clientY);
74
+ }, { passive: false });
75
+ stage.addEventListener("pointerdown", (e) => {
76
+ dragging = true; lastX = e.clientX; lastY = e.clientY;
77
+ stage.setPointerCapture(e.pointerId); stage.classList.add("is-grabbing");
78
+ });
79
+ stage.addEventListener("pointermove", (e) => {
80
+ if (!dragging) return;
81
+ tx += e.clientX - lastX; ty += e.clientY - lastY;
82
+ lastX = e.clientX; lastY = e.clientY; apply();
83
+ });
84
+ const endDrag = () => { dragging = false; stage.classList.remove("is-grabbing"); };
85
+ stage.addEventListener("pointerup", endDrag);
86
+ stage.addEventListener("pointercancel", endDrag);
87
+
88
+ document.addEventListener("keydown", (e) => {
89
+ if (!overlay.classList.contains("open")) return;
90
+ if (e.key === "Escape") { e.preventDefault(); close(); }
91
+ else if (e.key === "+" || e.key === "=") zoomTo(scale * 1.3);
92
+ else if (e.key === "-") zoomTo(scale / 1.3);
93
+ else if (e.key === "0") fit();
94
+ });
95
+ };
96
+
97
+ const fit = () => {
98
+ const node = art.firstElementChild;
99
+ if (!node) return;
100
+ // Measure the artwork at 1x, not through the current transform.
101
+ const prev = art.style.transform;
102
+ art.style.transform = "none";
103
+ const box = node.getBoundingClientRect();
104
+ art.style.transform = prev;
105
+ const s = stage.getBoundingClientRect();
106
+ scale = box.width && box.height
107
+ ? Math.min((s.width * FIT_MARGIN) / box.width, (s.height * FIT_MARGIN) / box.height)
108
+ : 1;
109
+ tx = ty = 0;
110
+ apply();
111
+ };
112
+
113
+ const close = () => {
114
+ overlay.classList.remove("open");
115
+ document.documentElement.classList.remove("dgm-locked");
116
+ art.replaceChildren();
117
+ opener?.focus();
118
+ };
119
+
120
+ const open = (source) => {
121
+ if (!overlay) build();
122
+ const node = source.querySelector("svg, img, canvas");
123
+ if (!node) return;
124
+ opener = source;
125
+ // Measure the ORIGINAL while it is still laid out. The clone needs a definite
126
+ // pixel size: a mermaid svg is sized by `width="100%"` plus an inline
127
+ // max-width, and both resolve against a parent — dropping them to "let it
128
+ // fill" gives an svg with only a viewBox, which collapses to 0x0 in an
129
+ // auto-sized box and makes fit() scale nothing (measured exactly that).
130
+ const box = node.getBoundingClientRect();
131
+ const copy = node.cloneNode(true);
132
+ copy.style.maxWidth = copy.style.maxHeight = "none";
133
+ if (box.width && box.height) {
134
+ copy.setAttribute("width", box.width);
135
+ copy.setAttribute("height", box.height);
136
+ copy.style.width = `${box.width}px`;
137
+ copy.style.height = `${box.height}px`;
138
+ }
139
+ art.replaceChildren(copy);
140
+ overlay.classList.add("open");
141
+ document.documentElement.classList.add("dgm-locked");
142
+ requestAnimationFrame(fit);
143
+ closeBtn.focus();
144
+ };
145
+
146
+ for (const d of diagrams) {
147
+ d.classList.add("dgm-zoomable");
148
+ d.setAttribute("role", "button");
149
+ d.setAttribute("tabindex", "0");
150
+ d.setAttribute("aria-label", "open diagram, zoomable");
151
+ d.addEventListener("click", () => open(d));
152
+ d.addEventListener("keydown", (e) => {
153
+ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(d); }
154
+ });
155
+ }
156
+ }
package/runtime/index.js CHANGED
@@ -17,4 +17,5 @@ export * from "./anim.js";
17
17
  export * from "./terminal.js";
18
18
  export * from "./nav.js";
19
19
  export * from "./lsnav.js";
20
+ export * from "./diagramzoom.js";
20
21
  export * from "./tooltip.js";
package/runtime/lsnav.js CHANGED
@@ -28,11 +28,37 @@
28
28
  */
29
29
  const KEY = "ls-nav";
30
30
 
31
+ /*
32
+ * The rail runs BETWEEN the chrome — it must not cover the header bar or the
33
+ * status footer. Their heights are a consumer's business and change with the
34
+ * viewport, so they are measured here and published as --ls-nav-top /
35
+ * --ls-nav-bottom rather than guessed at in CSS. chrome.css carries fallbacks,
36
+ * so a page that never runs this still lays out sensibly.
37
+ *
38
+ * Keeping `ls -l` at one fixed spot is the point: the rail's head and the tab
39
+ * that replaces it share --ls-nav-top, so opening and closing changes the arrow
40
+ * and nothing else moves.
41
+ */
42
+ function measureChrome() {
43
+ const root = document.documentElement;
44
+ const bar = document.querySelector("header.bar");
45
+ const status = document.querySelector("footer.status");
46
+ // A hidden footer (mobile folds it into the burger) contributes nothing.
47
+ const visible = (el) => el && el.getBoundingClientRect().height > 0;
48
+ if (visible(bar)) root.style.setProperty("--ls-nav-top", `${bar.getBoundingClientRect().height}px`);
49
+ root.style.setProperty("--ls-nav-bottom", visible(status) ? `${status.getBoundingClientRect().height}px` : "0px");
50
+ }
51
+
31
52
  export function initLsNav() {
32
53
  const root = document.documentElement;
33
54
  const toggles = document.querySelectorAll("[data-ls-nav-toggle]");
34
55
  if (!toggles.length) return;
35
56
 
57
+ measureChrome();
58
+ addEventListener("resize", measureChrome);
59
+ // The bar reflows when webfonts land, which changes its height after first paint.
60
+ if (document.fonts?.ready) document.fonts.ready.then(measureChrome).catch(() => {});
61
+
36
62
  const shown = () => root.dataset.lsNav !== "off";
37
63
  const sync = () => {
38
64
  for (const button of toggles) button.setAttribute("aria-expanded", String(shown()));
package/src/chrome.css CHANGED
@@ -101,8 +101,20 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
101
101
  leaves a small labelled tab on the edge rather than nothing, so a hidden rail
102
102
  is still discoverable by someone who did not hide it themselves.
103
103
 
104
+ THE HEADER AND FOOTER WIN. The rail runs BETWEEN them — it never covers the bar
105
+ or the status line, and both keep their full width. Daniel: "the right sidebar
106
+ then should not overlay footer and header. Footer and header wins."
107
+ That also buys the better toggle behaviour: with the rail starting below the
108
+ bar, `ls -l` sits at ONE fixed spot whether the rail is open or shut, so the
109
+ only thing that changes is which way the arrow points. The reader never has to
110
+ go looking for a control that moved.
111
+
112
+ --ls-nav-top/--ls-nav-bottom are measured off the real chrome by initLsNav()
113
+ and written onto <html>; the values here are fallbacks that keep the layout
114
+ sane if the runtime never arrives.
115
+
104
116
  Width is one token so a consumer can widen it for longer trees. */
105
- :root { --ls-nav-w: 17rem; }
117
+ :root { --ls-nav-w: 17rem; --ls-nav-top: 3rem; --ls-nav-bottom: 2.2rem; }
106
118
  .ls-nav-show { display: none; }
107
119
 
108
120
  @media (min-width: 48.0625rem) {
@@ -117,19 +129,30 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
117
129
  html { --ls-nav-inset: 0rem; }
118
130
  html:has(.ls-nav):not([data-ls-nav="off"]) { --ls-nav-inset: var(--ls-nav-w); }
119
131
 
120
- /* Everything in normal flow shifts left, so content sits against the rail's
121
- left edge instead of running under it. The fixed footer has to be told
122
- separately: `inset-inline: 0` ignores the body's padding. */
123
- /* Deliberately NOT transitioned. Animating padding on <body> reflows the whole
132
+ /* Content shifts left to sit against the rail's edge — but the CHROME does not.
133
+ The header bar and the status footer pull back out to full width with a
134
+ negative margin, so the rail begins below one and ends above the other rather
135
+ than covering either. Applied to the body rather than to a `main` selector
136
+ because the design system cannot know a consumer's content wrapper: every
137
+ block child shifts, and the two that must not are named explicitly.
138
+
139
+ Deliberately NOT transitioned. Animating padding on <body> reflows the whole
124
140
  document every frame, and the pages this ships to render tables of hundreds of
125
141
  rows — the rail's own transform is composited and costs nothing, so the panel
126
- slides while the content reflows once. Animating both looked no better and
127
- made the containers dashboard stutter on toggle. */
142
+ slides while the content reflows once. */
128
143
  body { padding-right: var(--ls-nav-inset); }
129
- footer.status { right: var(--ls-nav-inset); }
130
-
144
+ /* Only the HEADER needs pulling back out. It is `position: sticky`, i.e. still
145
+ in flow, so the body's padding narrows it. The footer is `position: fixed`
146
+ with `inset-inline: 0`, which ignores body padding and is already full width
147
+ — giving it the same negative margin pushed it 272px PAST the viewport edge
148
+ (measured 1099px against an 842px viewport). Fixed and in-flow siblings look
149
+ alike in the markup and do not behave alike here. */
150
+ header.bar { margin-right: calc(-1 * var(--ls-nav-inset)); }
151
+
152
+ /* Between the chrome, never over it. */
131
153
  .ls-nav {
132
- position: fixed; top: 0; right: 0; bottom: 0; width: var(--ls-nav-w); z-index: 45;
154
+ position: fixed; top: var(--ls-nav-top); right: 0; bottom: var(--ls-nav-bottom);
155
+ width: var(--ls-nav-w); z-index: 25;
133
156
  display: flex; flex-direction: column;
134
157
  background: var(--card); border-left: 1px solid var(--border);
135
158
  }
@@ -159,13 +182,17 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
159
182
  outline: 2px solid color-mix(in srgb, var(--primary) 55%, transparent); outline-offset: -2px;
160
183
  }
161
184
 
162
- /* The tab that survives hiding. Sits on the right edge, mid-height, and only
163
- exists while the rail is away. */
185
+ /* The tab that survives hiding TOP RIGHT, in the header band, which is where
186
+ `ls -l` has always lived (it was a header dropdown before it was a rail) and
187
+ where the rail's own title sits when it is open. An earlier version centred it
188
+ vertically on the right edge: correct as a drawer handle, wrong here, because
189
+ it moved the one control the reader is looking for to a place navigation has
190
+ never been. Same corner open or closed. */
164
191
  html[data-ls-nav="off"] .ls-nav-show {
165
192
  display: inline-flex; align-items: center; gap: 0.4rem;
166
- position: fixed; top: 50%; right: 0; z-index: 45; transform: translateY(-50%);
193
+ position: fixed; top: var(--ls-nav-top); right: 0; z-index: 25;
167
194
  background: var(--card); border: 1px solid var(--border); border-right: 0;
168
- padding: 0.6rem 0.5rem; font-size: 0.8rem;
195
+ border-top: 0; padding: 0.6rem 0.85rem; font-size: 0.9rem; font-weight: 700;
169
196
  }
170
197
  }
171
198
 
@@ -446,19 +446,30 @@ details.fold > summary:focus-visible {
446
446
  would have added five more. */
447
447
  .tickstrip:not(:empty) { border: 1px solid var(--border); background: var(--card); margin: 0 0 1.25rem; }
448
448
  .ticktable { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
449
- .ticktable td { padding: 0.45rem 0.55rem; vertical-align: baseline; border: 0; white-space: nowrap; }
449
+ .ticktable td { padding: 0.5rem 0.75rem; vertical-align: baseline; border: 0; white-space: nowrap; }
450
450
  /* Only the last column may grow, so the four fixed columns keep the same width on every row. */
451
451
  .ticktable td.tick-stats { width: 100%; white-space: normal; }
452
452
  .tick { border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent); }
453
453
  .tick:last-child { border-bottom: 0; }
454
- /* The glyph carries the state WITHOUT colour as well as with it — two of the four themes are
455
- near-black and a printout has none, so vs vs has to survive on its own. */
456
- .tick-dot { font-size: 0.7em; width: 1rem; padding-right: 0 !important; }
457
- .tick--ok .tick-dot { color: var(--primary); }
458
- .tick--stale .tick-dot { color: var(--destructive); }
459
- .tick--never .tick-dot { color: var(--muted-foreground); }
460
- .tick-name { font-weight: 700; color: var(--primary); }
454
+ /* State reads THREE ways, so it survives every context: the GLYPH (two themes are near-black and
455
+ a printout has no colour at all), the COLOUR, and a left edge-marker that makes a bad row
456
+ findable by shape alone when the strip is scanned rather than read. */
457
+ .tick-dot {
458
+ font-size: 0.7em; width: 1.5rem; padding-right: 0 !important;
459
+ border-left: 3px solid transparent; padding-left: 0.6rem !important;
460
+ }
461
+ /* --success is per-theme and AA-measured (0.3.0). `ok` used to borrow --primary — the BRAND
462
+ colour — so a healthy poller and the page heading were the same ink, and "everything is fine"
463
+ had to be read rather than seen. */
464
+ .tick--ok .tick-dot { color: var(--success); border-left-color: color-mix(in srgb, var(--success) 45%, transparent); }
465
+ .tick--stale .tick-dot { color: var(--destructive); border-left-color: var(--destructive); }
466
+ .tick--never .tick-dot { color: var(--muted-foreground); border-left-color: color-mix(in srgb, var(--muted-foreground) 35%, transparent); }
467
+ /* The NAME is an identifier, not an accent. Making it --primary put brand colour on every row of
468
+ a strip whose entire job is that the exceptions stand out. */
469
+ .tick-name { font-weight: 700; color: var(--foreground); letter-spacing: 0.01em; }
461
470
  .tick-last, .tick-next { font-variant-numeric: tabular-nums; color: var(--muted-foreground); }
471
+ /* "next 57s" is a prediction; the age beside it is an observation. It should not compete. */
472
+ .tick-next { opacity: 0.72; }
462
473
  .tick--stale .tick-last { color: var(--destructive); font-weight: 700; }
463
474
  .tick--never .tick-last { font-style: italic; }
464
475
  /* The NUMBER is what gets scanned; its label is context. Weight and colour on the value alone
@@ -475,3 +486,48 @@ details.fold > summary:focus-visible {
475
486
  .ticktable td { display: inline-block; padding: 0.1rem 0.5rem 0.1rem 0; }
476
487
  .ticktable td.tick-stats { display: block; width: auto; }
477
488
  }
489
+
490
+ /* ── zoomable diagrams ────────────────────────────────────────────────────────────────────────
491
+ An architecture diagram authored for a text column is unreadable at exactly the moment someone
492
+ needs it. Pair with `initDiagramZoom()`: every `.diagram` becomes a real button that opens
493
+ full-screen, fitted to the viewport, then zoomable and pannable.
494
+
495
+ The affordance has to be visible without hovering — this portal is read from a phone — so the
496
+ corner hint is always drawn, not revealed on :hover. */
497
+ .dgm-zoomable { position: relative; cursor: zoom-in; }
498
+ .dgm-zoomable::after {
499
+ content: "⤢"; position: absolute; top: 0.4rem; right: 0.5rem;
500
+ font-size: 0.85rem; line-height: 1; opacity: 0.45; pointer-events: none;
501
+ color: var(--muted-foreground);
502
+ }
503
+ .dgm-zoomable:hover::after, .dgm-zoomable:focus-visible::after { opacity: 1; color: var(--primary); }
504
+ .dgm-zoomable:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
505
+
506
+ html.dgm-locked, html.dgm-locked body { overflow: hidden; }
507
+
508
+ .dgm-overlay {
509
+ display: none; position: fixed; inset: 0; z-index: 90;
510
+ background: color-mix(in srgb, var(--background) 96%, transparent);
511
+ flex-direction: column;
512
+ }
513
+ .dgm-overlay.open { display: flex; }
514
+ .dgm-bar {
515
+ flex: none; display: flex; align-items: center; justify-content: flex-end; gap: 0.4rem;
516
+ padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); background: var(--card);
517
+ }
518
+ .dgm-btn {
519
+ font: inherit; font-size: 0.82rem; cursor: pointer; padding: 0.3rem 0.7rem; min-height: 32px;
520
+ background: transparent; border: 1px solid var(--border); color: var(--foreground);
521
+ }
522
+ .dgm-btn:hover { border-color: var(--primary); color: var(--primary); }
523
+ .dgm-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
524
+ .dgm-close { border-color: color-mix(in srgb, var(--destructive) 55%, transparent); color: var(--destructive); }
525
+ .dgm-close:hover { border-color: var(--destructive); color: var(--destructive); }
526
+ /* The stage clips; the artwork inside it is what moves. Transforming a wrapper rather than the
527
+ svg keeps mermaid's own geometry untouched. */
528
+ .dgm-stage { flex: 1; overflow: hidden; display: grid; place-items: center; cursor: grab; touch-action: none; }
529
+ .dgm-stage.is-grabbing { cursor: grabbing; }
530
+ .dgm-art { transform-origin: center center; will-change: transform; }
531
+ .dgm-art > svg, .dgm-art > img { display: block; max-width: none; max-height: none; }
532
+
533
+ @media print { .dgm-overlay, .dgm-zoomable::after { display: none !important; } }