@danieldeusing/design 0.1.6 → 0.3.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.
@@ -1,4 +1,4 @@
1
- /*! danieldeusing-design v0.1.6 | MIT | https://github.com/danieldeusing/danieldeusing-design */
1
+ /*! danieldeusing-design v0.3.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
2
2
  /* ===== reset.css ===== */
3
3
  /*
4
4
  * danieldeusing-design — minimal reset.
@@ -87,6 +87,26 @@ table {
87
87
  * These are plain CSS custom properties — they work in any framework, any
88
88
  * browser, with or without a build step. Load this file first; base.css and
89
89
  * components.css resolve against the variables declared here.
90
+ *
91
+ * ── WHY EVERY ACCENT IS PER-THEME, AND WHY A LITERAL IS NEVER OK ──
92
+ * A status colour has to clear WCAG AA 4.5:1 against every surface it can land
93
+ * on — --background, --card AND --muted. Do that arithmetic across the four
94
+ * themes and the two admissible luminance bands DO NOT OVERLAP:
95
+ *
96
+ * light themes: darkest surface is warm's --muted #ece3cf -> accent L <= 0.1328
97
+ * dark themes: lightest surface is green's --muted #071509 -> accent L >= 0.2021
98
+ * (mono's #111111 wants accent L >= 0.2002)
99
+ *
100
+ * A gap of 0.0692 with nothing in it. So NO single hex can serve four themes:
101
+ * the two near-black themes need a light tint, the two light themes a dark one,
102
+ * and every compromise fails both. That is arithmetic, not taste — which is why
103
+ * --destructive, --success and --warning are each declared four times.
104
+ *
105
+ * Cockpit carried twelve hardcoded accents that predate these tokens; every one
106
+ * missed AA on at least one theme and four sat inside the gap, clearing neither
107
+ * side. Adding a status colour? Add a TOKEN here with its measured worst-case
108
+ * ratio in a trailing comment. Do not tune a literal at the call site — that
109
+ * forks the palette and fixes exactly one theme.
90
110
  */
91
111
 
92
112
  :root {
@@ -105,6 +125,8 @@ table {
105
125
  --accent: #8a4516;
106
126
  --accent-foreground: #f8f3e8;
107
127
  --destructive: #a02c2c;
128
+ --success: #1a6b2e; /* 5.16:1 worst surface */
129
+ --warning: #845600; /* 4.97:1 worst surface */
108
130
  --border: #d9cdb6;
109
131
  --input: #d9cdb6;
110
132
  --ring: #8a4516;
@@ -135,6 +157,8 @@ html[data-theme="green"] {
135
157
  --accent: #33ff66;
136
158
  --accent-foreground: #02160a;
137
159
  --destructive: #ff5c5c;
160
+ --success: #4ade80; /* 10.76:1 worst surface */
161
+ --warning: #f5b32b; /* 10.15:1 worst surface */
138
162
  --border: #1c4a2c;
139
163
  --input: #1c4a2c;
140
164
  --ring: #33ff66;
@@ -160,6 +184,8 @@ html[data-theme="mono"] {
160
184
  --accent: #ffffff;
161
185
  --accent-foreground: #050505;
162
186
  --destructive: #ff5c5c;
187
+ --success: #4ade80; /* 10.84:1 worst surface */
188
+ --warning: #f5b32b; /* 10.22:1 worst surface */
163
189
  --border: #333333;
164
190
  --input: #333333;
165
191
  --ring: #ffffff;
@@ -185,6 +211,8 @@ html[data-theme="paper"] {
185
211
  --accent: #000000;
186
212
  --accent-foreground: #fafafa;
187
213
  --destructive: #b3261e;
214
+ --success: #1a7031; /* 5.36:1 worst surface */
215
+ --warning: #8f5d00; /* 4.89:1 worst surface */
188
216
  --border: #d4d4d4;
189
217
  --input: #d4d4d4;
190
218
  --ring: #000000;
@@ -619,12 +647,135 @@ html.anim-off .cursor-block {
619
647
  }
620
648
  }
621
649
 
650
+ /* ═══════════════════════════════════════════════════════════════════════════════════════════════
651
+ Shared page vocabulary, promoted from the cockpit portal (2026-08-04).
652
+
653
+ Each of these was built once in cockpit/pages/portal.css and then wanted by every other
654
+ surface. They live here so there is ONE definition — the tabs had already drifted into FOUR
655
+ page style blocks before being consolidated, which is exactly the failure this move prevents.
656
+ ═════════════════════════════════════════════════════════════════════════════════════════════ */
657
+
658
+ /* ── tabs ──────────────────────────────────────────────────────────────────────────────────────
659
+ Daniel: "I am always searching the tabs. They are not falling into my eyes directly."
660
+
661
+ They were low-contrast pills at 0.7 opacity, and the SELECTED one differed from the rest only
662
+ by a slightly darker border and 8% background — so the row read as six equal chips and the eye
663
+ had to stop and compare them. This inverts it: the inactive tabs recede to plain muted text
664
+ with no chrome at all, and the active one is the only thing on the row carrying weight, colour
665
+ and a fill. Nothing to compare — one thing is loud. */
666
+ .tabs {
667
+ display: flex; flex-wrap: wrap; gap: 0.15rem; margin: 1.35rem 0 0;
668
+ border-bottom: 2px solid color-mix(in srgb, var(--border) 80%, transparent);
669
+ }
670
+ .tab {
671
+ background: transparent; border: 0; margin-bottom: -2px;
672
+ font: inherit; font-size: 0.86em; cursor: pointer; padding: 0.5rem 0.95rem;
673
+ color: var(--muted-foreground); transition: background 0.12s ease, color 0.12s ease;
674
+ }
675
+ .tab:hover { color: var(--primary); background: color-mix(in srgb, var(--primary) 10%, transparent); }
676
+ /* FILLED, not underlined. An underline plus a colour shift was still one thin rule and a hue
677
+ change on a row of same-weight words — Daniel was still hunting for the row. A solid block of
678
+ --primary with the page background as its text is the highest-contrast pair the theme owns
679
+ (5.6:1 on warm), and it is the only filled thing on the page, so it is found without reading. */
680
+ .tab[aria-selected="true"] { background: var(--primary); color: var(--background); font-weight: 700; }
681
+ .tab:focus-visible { outline: 2px solid color-mix(in srgb, var(--primary) 55%, transparent); outline-offset: -2px; }
682
+ /* 44px on a touch screen — tabs are the primary navigation of these pages, so they are the last
683
+ control that should be hard to hit. */
684
+ @media (pointer: coarse), (max-width: 40rem) {
685
+ .tab { padding: 0.7rem 0.8rem; min-height: 44px; }
686
+ }
687
+ section.doc.tab-panel { margin-top: 1.35rem; }
688
+
689
+ /* ── fold: progressive disclosure whose affordance is always visible ───────────────────────────
690
+ The detail behind a visual has to open by TAP — these pages are read from a phone, where hover
691
+ does not exist — and the control has to be findable WITHOUT hovering, or the detail is simply
692
+ text that was deleted. Hence a permanent ▸ marker and a full-width summary row rather than
693
+ anything that appears on hover.
694
+
695
+ Pair with a `beforeprint` handler that opens every <details>, or a printout silently loses
696
+ whatever the closed ones were holding. */
697
+ details.fold { border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent); }
698
+ details.fold > summary {
699
+ display: flex; align-items: center; gap: 0.5rem; cursor: pointer; list-style: none;
700
+ padding: 0.55rem 0; font-size: 0.85rem; color: var(--muted-foreground);
701
+ }
702
+ details.fold > summary::-webkit-details-marker { display: none; }
703
+ details.fold > summary::before { content: "\25B8"; flex: none; opacity: 0.6; }
704
+ details.fold[open] > summary::before { content: "\25BE"; }
705
+ details.fold > summary:hover { color: var(--primary); }
706
+ details.fold > summary:focus-visible {
707
+ outline: 2px solid color-mix(in srgb, currentColor 45%, transparent); outline-offset: 2px;
708
+ }
709
+ .fold-body { font-size: 0.9rem; padding: 0 0 0.7rem 1.1rem; }
710
+ .fold-body > * + * { margin-top: 0.6rem; }
711
+
712
+ /* ── legend: one rendering of each state, beside the thing it keys ─────────────────────────────
713
+ An item is `display: block`, NOT flex: a gloss contains <strong>/<code>/<em>, and every one of
714
+ those would become its own flex item — which is how a single sentence first rendered as four
715
+ separately-wrapped columns. */
716
+ .legend {
717
+ list-style: none; padding: 0; margin: 0.7rem 0 0;
718
+ display: grid; gap: 0.4rem 1.4rem; font-size: 0.86rem;
719
+ }
720
+ @media (min-width: 52rem) { .legend { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
721
+ .legend > li { line-height: 1.6; }
722
+ .legend > li > :first-child { margin-right: 0.45rem; }
723
+
724
+ /* ── ticker strip: one standard header for every poller ────────────────────────────────────────
725
+ Daniel: "For all pollers / tickers we should make a standard layout ... on top of the pages,
726
+ compact and like dashboard style."
727
+
728
+ Every poller answers the same four questions — what ran, when, when next, what did it count —
729
+ and each page was answering them in its own prose box, so "is anything stalled?" took four
730
+ paragraphs instead of one glance.
731
+
732
+ A real TABLE, not a grid per row. The first version made each row its own `display: grid`,
733
+ which cannot align anything across rows: every row sized its columns to its own content, so
734
+ `next` sat wherever that row's name happened to end and the layout moved when a label changed.
735
+ One table means one set of column widths for the whole strip, and scanning a column is the only
736
+ thing the strip is for. Borderless by design — rules between cells draw a grid nobody reads.
737
+
738
+ Colour comes only from --primary and --destructive, both per-theme. Hardcoded accents miss AA
739
+ on at least one theme (measured 2026-08-04), so a strip inventing its own greens and ambers
740
+ would have added five more. */
741
+ .tickstrip:not(:empty) { border: 1px solid var(--border); background: var(--card); margin: 0 0 1.25rem; }
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; }
744
+ /* Only the last column may grow, so the four fixed columns keep the same width on every row. */
745
+ .ticktable td.tick-stats { width: 100%; white-space: normal; }
746
+ .tick { border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent); }
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); }
755
+ .tick-last, .tick-next { font-variant-numeric: tabular-nums; color: var(--muted-foreground); }
756
+ .tick--stale .tick-last { color: var(--destructive); font-weight: 700; }
757
+ .tick--never .tick-last { font-style: italic; }
758
+ /* The NUMBER is what gets scanned; its label is context. Weight and colour on the value alone
759
+ means a row reads as its numbers first and its wording second. */
760
+ .tick-stats { color: var(--muted-foreground); }
761
+ .tick-stats b { font-weight: 700; color: var(--primary); font-variant-numeric: tabular-nums; }
762
+ .tick-sep { font-style: normal; opacity: 0.35; margin: 0 0.5rem; }
763
+ /* A stale row earns the only background tint in the strip — an all-tinted strip is one the eye
764
+ stops reading. */
765
+ .tick--stale { background: color-mix(in srgb, var(--destructive) 8%, transparent); }
766
+ @media (max-width: 40rem) {
767
+ .ticktable, .ticktable tbody, .tick, .ticktable td { display: block; }
768
+ .tick { padding: 0.4rem 0.5rem; }
769
+ .ticktable td { display: inline-block; padding: 0.1rem 0.5rem 0.1rem 0; }
770
+ .ticktable td.tick-stats { display: block; width: auto; }
771
+ }
772
+
622
773
  /* ===== chrome.css ===== */
623
774
  /*
624
775
  * chrome.css — the app chrome kit: sticky header bar, fixed status footer,
625
- * and the responsive site navigation (desktop `ls -l` dropdown, mobile
626
- * burger with the footer folded in). Extracted from danieldeusing.de and
627
- * the cockpit portal so every app ships the same chrome without rebuilding it.
776
+ * and the responsive site navigation (desktop `ls -l` RAIL, mobile burger with
777
+ * the footer folded in). Extracted from danieldeusing.de and the cockpit portal
778
+ * so every app ships the same chrome without rebuilding it.
628
779
  *
629
780
  * Markup contract (see templates/documentation.html for the full example):
630
781
  *
@@ -633,7 +784,14 @@ html.anim-off .cursor-block {
633
784
  * <a href="/" class="brand"><span class="glow">name</span><span class="cursor-block"></span></a>
634
785
  * <button class="nav-burger" data-nav-toggle aria-controls="site-nav" aria-expanded="false">…☰ svg…</button>
635
786
  * <nav aria-label="site" id="site-nav" class="site-nav">
636
- * <details class="dropdown" id="nav">…ls-panel with .ls-row items…</details>
787
+ * <div class="ls-nav" id="nav">
788
+ * <div class="ls-nav-head">
789
+ * <span class="ls-nav-title">ls -l</span>
790
+ * <button class="ls-nav-hide" data-ls-nav-toggle aria-controls="nav" aria-expanded="true">»</button>
791
+ * </div>
792
+ * <ul class="ls-panel">….ls-row items…</ul>
793
+ * </div>
794
+ * <button class="ls-nav-show" data-ls-nav-toggle aria-controls="nav">« ls -l</button>
637
795
  * <ul class="mobile-nav">…same links as .mobile-item ls-rows…</ul>
638
796
  * <div class="mobile-footer">…footer controls folded in…</div>
639
797
  * </nav>
@@ -641,8 +799,15 @@ html.anim-off .cursor-block {
641
799
  * …
642
800
  * <footer class="status">…status-left + status-right…</footer>
643
801
  *
644
- * Behaviour: pair with `initBurgerNav()` from the runtime (adds the toggle +
645
- * outside-click close). Breakpoint: 48rem.
802
+ * Behaviour: pair with `initBurgerNav()` (mobile burger) and `initLsNav()`
803
+ * (rail show/hide) from the runtime. The rail's SHOWN/HIDDEN state must also be
804
+ * applied before first paint by an inline <head> script — see the rail section
805
+ * below for why the runtime cannot be the one to do it:
806
+ *
807
+ * try { if (localStorage.getItem("ls-nav") === "off")
808
+ * document.documentElement.dataset.lsNav = "off"; } catch {}
809
+ *
810
+ * Breakpoint: 48rem.
646
811
  */
647
812
 
648
813
  /* ── a11y helpers ── */
@@ -682,12 +847,100 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
682
847
  .doc-link { color: var(--muted-foreground); text-decoration: none; transition: color 0.15s ease; }
683
848
  .doc-link:hover { color: var(--primary); }
684
849
 
685
- /* ── ls-listing rows (shared by the desktop dropdown + mobile menu) ── */
850
+ /* ── ls-listing rows (shared by the desktop rail + mobile menu) ── */
686
851
  .ls-row { display: flex; align-items: baseline; gap: 0.9rem; }
687
852
  .ls-perm { color: var(--muted-foreground); font-size: 0.72rem; letter-spacing: 0; }
688
853
  .ls-row--sub .ls-name { padding-left: 1rem; }
689
- .ls-panel { min-width: 17rem; left: auto !important; right: 0; }
690
- #nav { position: relative; }
854
+ /* The pre-rail dropdown form. Kept, and scoped to the dropdown panel, because the
855
+ other surfaces still ship that markup — when they unpin they get this file, and
856
+ their nav should stay right-aligned rather than quietly jumping to the left. The
857
+ rail's own <ul> carries `ls-panel` WITHOUT `dropdown-panel`, so it is unaffected. */
858
+ .dropdown-panel.ls-panel { min-width: 17rem; left: auto !important; right: 0; }
859
+
860
+ /* ── `ls -l` site rail (desktop ≥48rem): sticky, full-height, right-hand panel ──
861
+ Was a dropdown that had to be re-opened on every page. Daniel: "Always make it
862
+ sticky (default shown) right side, keep `ls -l`, but not down arrow, but
863
+ something which tells the user he can hide / minimize."
864
+
865
+ SHOWN BY DEFAULT, and the state lives in localStorage under "ls-nav". It is
866
+ applied by an inline <head> script as `html[data-ls-nav="off"]` BEFORE first
867
+ paint — not by the runtime — because the runtime is a module at the end of
868
+ <body>: a reader who hid the rail would otherwise watch it paint and then jump
869
+ away on every single page load. `initLsNav()` only handles the clicks.
870
+
871
+ The affordance is deliberately NOT a caret. A caret means "a menu drops from
872
+ here", which is the thing this stopped being. Guillemets point the way the
873
+ panel travels — » pushes it off to the right, « pulls it back — and hiding it
874
+ leaves a small labelled tab on the edge rather than nothing, so a hidden rail
875
+ is still discoverable by someone who did not hide it themselves.
876
+
877
+ Width is one token so a consumer can widen it for longer trees. */
878
+ :root { --ls-nav-w: 17rem; }
879
+ .ls-nav-show { display: none; }
880
+
881
+ @media (min-width: 48.0625rem) {
882
+ /* Room is reserved ONLY when a rail is actually in the page, and the default is
883
+ zero. This is what makes the stylesheet safe to publish ahead of a consumer's
884
+ markup: every surface that still ships the old dropdown nav (or no nav at all)
885
+ keeps its full width instead of growing a 17rem gutter with nothing in it —
886
+ which is exactly what they would have got the moment they unpinned.
887
+ It also fails in the right direction: if :has() is unavailable the selector is
888
+ dropped, the inset stays 0, and the rail overlays content rather than the page
889
+ reserving space for a rail that never arrives. */
890
+ html { --ls-nav-inset: 0rem; }
891
+ html:has(.ls-nav):not([data-ls-nav="off"]) { --ls-nav-inset: var(--ls-nav-w); }
892
+
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
897
+ document every frame, and the pages this ships to render tables of hundreds of
898
+ 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. */
901
+ body { padding-right: var(--ls-nav-inset); }
902
+ footer.status { right: var(--ls-nav-inset); }
903
+
904
+ .ls-nav {
905
+ position: fixed; top: 0; right: 0; bottom: 0; width: var(--ls-nav-w); z-index: 45;
906
+ display: flex; flex-direction: column;
907
+ background: var(--card); border-left: 1px solid var(--border);
908
+ }
909
+ html:not(.anim-off) .ls-nav,
910
+ html:not(.anim-off) .ls-nav-show { transition: transform 0.18s ease; }
911
+ html[data-ls-nav="off"] .ls-nav { transform: translateX(100%); }
912
+
913
+ .ls-nav-head {
914
+ flex: none; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
915
+ padding: 0.6rem 0.5rem 0.6rem 0.85rem; border-bottom: 1px solid var(--border);
916
+ }
917
+ .ls-nav-title { font-weight: 700; font-size: 0.9rem; }
918
+
919
+ /* The list is the only thing that scrolls — the head stays put, so the control
920
+ that hides the rail cannot be scrolled out of reach. */
921
+ .ls-nav .ls-panel {
922
+ flex: 1; min-height: 0; overflow-y: auto;
923
+ list-style: none; margin: 0; padding: 0.5rem 0 1rem;
924
+ }
925
+
926
+ .ls-nav-hide, .ls-nav-show {
927
+ background: none; border: 0; font: inherit; cursor: pointer; line-height: 1;
928
+ color: var(--muted-foreground); padding: 0.4rem 0.55rem;
929
+ }
930
+ .ls-nav-hide:hover, .ls-nav-show:hover { color: var(--primary); }
931
+ .ls-nav-hide:focus-visible, .ls-nav-show:focus-visible {
932
+ outline: 2px solid color-mix(in srgb, var(--primary) 55%, transparent); outline-offset: -2px;
933
+ }
934
+
935
+ /* The tab that survives hiding. Sits on the right edge, mid-height, and only
936
+ exists while the rail is away. */
937
+ html[data-ls-nav="off"] .ls-nav-show {
938
+ display: inline-flex; align-items: center; gap: 0.4rem;
939
+ position: fixed; top: 50%; right: 0; z-index: 45; transform: translateY(-50%);
940
+ background: var(--card); border: 1px solid var(--border); border-right: 0;
941
+ padding: 0.6rem 0.5rem; font-size: 0.8rem;
942
+ }
943
+ }
691
944
 
692
945
  /* ── burger + mobile menu ── */
693
946
  .nav-burger {
@@ -708,7 +961,9 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
708
961
  padding: 0.4rem 1.25rem 0.9rem; max-height: calc(100vh - 6rem); overflow-y: auto;
709
962
  }
710
963
  .site-nav.open { display: block; }
711
- .site-nav > .dropdown { display: none; } /* desktop ls-dropdown replaced by the flat list */
964
+ /* The desktop rail is a desktop idea: a fixed 17rem column would eat a phone.
965
+ Below the breakpoint the burger owns navigation and the flat list replaces it. */
966
+ .site-nav > .ls-nav, .ls-nav-show { display: none; }
712
967
 
713
968
  .mobile-nav { display: flex; flex-direction: column; }
714
969
  .mobile-nav .mobile-item {
@@ -895,6 +1150,14 @@ span[data-tip], th[data-tip] { border-bottom: 1px dotted var(--muted-foreground)
895
1150
  display: none !important;
896
1151
  }
897
1152
 
1153
+ /* The `ls -l` rail sits inside .site-nav, so the rule above already hides it —
1154
+ but the room it reserved is padding on the BODY, and that would print as a
1155
+ 17rem gutter down the right of every page. Hiding a fixed element does not
1156
+ reclaim the space something else was told to leave for it. */
1157
+ body {
1158
+ padding-right: 0 !important;
1159
+ }
1160
+
898
1161
  [data-tip] {
899
1162
  cursor: auto;
900
1163
  }
@@ -1,2 +1,2 @@
1
- /*! danieldeusing-design v0.1.6 | 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;--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;--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;--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;--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)}}.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}.ls-panel{min-width: 17rem;left: auto !important;right: 0}#nav{position: relative}.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>.dropdown{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}[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.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}}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@danieldeusing/design",
3
- "version": "0.1.6",
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.",
3
+ "version": "0.3.0",
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",
7
7
  "author": "Daniel Deusing",
package/runtime/index.js CHANGED
@@ -16,4 +16,5 @@ export * from "./dropdown.js";
16
16
  export * from "./anim.js";
17
17
  export * from "./terminal.js";
18
18
  export * from "./nav.js";
19
+ export * from "./lsnav.js";
19
20
  export * from "./tooltip.js";
@@ -0,0 +1,56 @@
1
+ /*
2
+ * lsnav.js — show/hide for the `ls -l` site rail (src/chrome.css).
3
+ *
4
+ * Markup contract:
5
+ * <div class="ls-nav" id="nav">
6
+ * <div class="ls-nav-head">
7
+ * <span class="ls-nav-title">ls -l</span>
8
+ * <button class="ls-nav-hide" data-ls-nav-toggle aria-controls="nav" aria-expanded="true">»</button>
9
+ * </div>
10
+ * <ul class="ls-panel">…</ul>
11
+ * </div>
12
+ * <button class="ls-nav-show" data-ls-nav-toggle aria-controls="nav">« ls -l</button>
13
+ *
14
+ * THIS MODULE DOES NOT APPLY THE INITIAL STATE, and that is deliberate. It is a
15
+ * module at the end of <body>, so anything it does happens after first paint: a
16
+ * reader who hid the rail would watch it paint and then jump away on every page
17
+ * load. The state is applied by an inline <head> script instead —
18
+ *
19
+ * try { if (localStorage.getItem("ls-nav") === "off")
20
+ * document.documentElement.dataset.lsNav = "off"; } catch {}
21
+ *
22
+ * — and this module only reads that, wires the clicks, and writes back. Shown is
23
+ * the default, so an absent key and a failed read both land on "shown", which is
24
+ * the state that is correct when in doubt.
25
+ *
26
+ * Every [data-ls-nav-toggle] toggles, so the hide button inside the rail and the
27
+ * tab that replaces it share one handler and one aria-expanded truth.
28
+ */
29
+ const KEY = "ls-nav";
30
+
31
+ export function initLsNav() {
32
+ const root = document.documentElement;
33
+ const toggles = document.querySelectorAll("[data-ls-nav-toggle]");
34
+ if (!toggles.length) return;
35
+
36
+ const shown = () => root.dataset.lsNav !== "off";
37
+ const sync = () => {
38
+ for (const button of toggles) button.setAttribute("aria-expanded", String(shown()));
39
+ };
40
+
41
+ sync(); // the inline script set the state; the buttons have not been told yet
42
+
43
+ for (const button of toggles) {
44
+ button.addEventListener("click", () => {
45
+ const next = !shown();
46
+ root.dataset.lsNav = next ? "on" : "off";
47
+ // A rail the reader hid should stay hidden on the next page. Wrapped
48
+ // because a locked-down browser throws on write and a nav that cannot
49
+ // remember is still a working nav.
50
+ try {
51
+ localStorage.setItem(KEY, next ? "on" : "off");
52
+ } catch {}
53
+ sync();
54
+ });
55
+ }
56
+ }
package/src/chrome.css CHANGED
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  * chrome.css — the app chrome kit: sticky header bar, fixed status footer,
3
- * and the responsive site navigation (desktop `ls -l` dropdown, mobile
4
- * burger with the footer folded in). Extracted from danieldeusing.de and
5
- * the cockpit portal so every app ships the same chrome without rebuilding it.
3
+ * and the responsive site navigation (desktop `ls -l` RAIL, mobile burger with
4
+ * the footer folded in). Extracted from danieldeusing.de and the cockpit portal
5
+ * so every app ships the same chrome without rebuilding it.
6
6
  *
7
7
  * Markup contract (see templates/documentation.html for the full example):
8
8
  *
@@ -11,7 +11,14 @@
11
11
  * <a href="/" class="brand"><span class="glow">name</span><span class="cursor-block"></span></a>
12
12
  * <button class="nav-burger" data-nav-toggle aria-controls="site-nav" aria-expanded="false">…☰ svg…</button>
13
13
  * <nav aria-label="site" id="site-nav" class="site-nav">
14
- * <details class="dropdown" id="nav">…ls-panel with .ls-row items…</details>
14
+ * <div class="ls-nav" id="nav">
15
+ * <div class="ls-nav-head">
16
+ * <span class="ls-nav-title">ls -l</span>
17
+ * <button class="ls-nav-hide" data-ls-nav-toggle aria-controls="nav" aria-expanded="true">»</button>
18
+ * </div>
19
+ * <ul class="ls-panel">….ls-row items…</ul>
20
+ * </div>
21
+ * <button class="ls-nav-show" data-ls-nav-toggle aria-controls="nav">« ls -l</button>
15
22
  * <ul class="mobile-nav">…same links as .mobile-item ls-rows…</ul>
16
23
  * <div class="mobile-footer">…footer controls folded in…</div>
17
24
  * </nav>
@@ -19,8 +26,15 @@
19
26
  * …
20
27
  * <footer class="status">…status-left + status-right…</footer>
21
28
  *
22
- * Behaviour: pair with `initBurgerNav()` from the runtime (adds the toggle +
23
- * outside-click close). Breakpoint: 48rem.
29
+ * Behaviour: pair with `initBurgerNav()` (mobile burger) and `initLsNav()`
30
+ * (rail show/hide) from the runtime. The rail's SHOWN/HIDDEN state must also be
31
+ * applied before first paint by an inline <head> script — see the rail section
32
+ * below for why the runtime cannot be the one to do it:
33
+ *
34
+ * try { if (localStorage.getItem("ls-nav") === "off")
35
+ * document.documentElement.dataset.lsNav = "off"; } catch {}
36
+ *
37
+ * Breakpoint: 48rem.
24
38
  */
25
39
 
26
40
  /* ── a11y helpers ── */
@@ -60,12 +74,100 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
60
74
  .doc-link { color: var(--muted-foreground); text-decoration: none; transition: color 0.15s ease; }
61
75
  .doc-link:hover { color: var(--primary); }
62
76
 
63
- /* ── ls-listing rows (shared by the desktop dropdown + mobile menu) ── */
77
+ /* ── ls-listing rows (shared by the desktop rail + mobile menu) ── */
64
78
  .ls-row { display: flex; align-items: baseline; gap: 0.9rem; }
65
79
  .ls-perm { color: var(--muted-foreground); font-size: 0.72rem; letter-spacing: 0; }
66
80
  .ls-row--sub .ls-name { padding-left: 1rem; }
67
- .ls-panel { min-width: 17rem; left: auto !important; right: 0; }
68
- #nav { position: relative; }
81
+ /* The pre-rail dropdown form. Kept, and scoped to the dropdown panel, because the
82
+ other surfaces still ship that markup — when they unpin they get this file, and
83
+ their nav should stay right-aligned rather than quietly jumping to the left. The
84
+ rail's own <ul> carries `ls-panel` WITHOUT `dropdown-panel`, so it is unaffected. */
85
+ .dropdown-panel.ls-panel { min-width: 17rem; left: auto !important; right: 0; }
86
+
87
+ /* ── `ls -l` site rail (desktop ≥48rem): sticky, full-height, right-hand panel ──
88
+ Was a dropdown that had to be re-opened on every page. Daniel: "Always make it
89
+ sticky (default shown) right side, keep `ls -l`, but not down arrow, but
90
+ something which tells the user he can hide / minimize."
91
+
92
+ SHOWN BY DEFAULT, and the state lives in localStorage under "ls-nav". It is
93
+ applied by an inline <head> script as `html[data-ls-nav="off"]` BEFORE first
94
+ paint — not by the runtime — because the runtime is a module at the end of
95
+ <body>: a reader who hid the rail would otherwise watch it paint and then jump
96
+ away on every single page load. `initLsNav()` only handles the clicks.
97
+
98
+ The affordance is deliberately NOT a caret. A caret means "a menu drops from
99
+ here", which is the thing this stopped being. Guillemets point the way the
100
+ panel travels — » pushes it off to the right, « pulls it back — and hiding it
101
+ leaves a small labelled tab on the edge rather than nothing, so a hidden rail
102
+ is still discoverable by someone who did not hide it themselves.
103
+
104
+ Width is one token so a consumer can widen it for longer trees. */
105
+ :root { --ls-nav-w: 17rem; }
106
+ .ls-nav-show { display: none; }
107
+
108
+ @media (min-width: 48.0625rem) {
109
+ /* Room is reserved ONLY when a rail is actually in the page, and the default is
110
+ zero. This is what makes the stylesheet safe to publish ahead of a consumer's
111
+ markup: every surface that still ships the old dropdown nav (or no nav at all)
112
+ keeps its full width instead of growing a 17rem gutter with nothing in it —
113
+ which is exactly what they would have got the moment they unpinned.
114
+ It also fails in the right direction: if :has() is unavailable the selector is
115
+ dropped, the inset stays 0, and the rail overlays content rather than the page
116
+ reserving space for a rail that never arrives. */
117
+ html { --ls-nav-inset: 0rem; }
118
+ html:has(.ls-nav):not([data-ls-nav="off"]) { --ls-nav-inset: var(--ls-nav-w); }
119
+
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
124
+ document every frame, and the pages this ships to render tables of hundreds of
125
+ 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. */
128
+ body { padding-right: var(--ls-nav-inset); }
129
+ footer.status { right: var(--ls-nav-inset); }
130
+
131
+ .ls-nav {
132
+ position: fixed; top: 0; right: 0; bottom: 0; width: var(--ls-nav-w); z-index: 45;
133
+ display: flex; flex-direction: column;
134
+ background: var(--card); border-left: 1px solid var(--border);
135
+ }
136
+ html:not(.anim-off) .ls-nav,
137
+ html:not(.anim-off) .ls-nav-show { transition: transform 0.18s ease; }
138
+ html[data-ls-nav="off"] .ls-nav { transform: translateX(100%); }
139
+
140
+ .ls-nav-head {
141
+ flex: none; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
142
+ padding: 0.6rem 0.5rem 0.6rem 0.85rem; border-bottom: 1px solid var(--border);
143
+ }
144
+ .ls-nav-title { font-weight: 700; font-size: 0.9rem; }
145
+
146
+ /* The list is the only thing that scrolls — the head stays put, so the control
147
+ that hides the rail cannot be scrolled out of reach. */
148
+ .ls-nav .ls-panel {
149
+ flex: 1; min-height: 0; overflow-y: auto;
150
+ list-style: none; margin: 0; padding: 0.5rem 0 1rem;
151
+ }
152
+
153
+ .ls-nav-hide, .ls-nav-show {
154
+ background: none; border: 0; font: inherit; cursor: pointer; line-height: 1;
155
+ color: var(--muted-foreground); padding: 0.4rem 0.55rem;
156
+ }
157
+ .ls-nav-hide:hover, .ls-nav-show:hover { color: var(--primary); }
158
+ .ls-nav-hide:focus-visible, .ls-nav-show:focus-visible {
159
+ outline: 2px solid color-mix(in srgb, var(--primary) 55%, transparent); outline-offset: -2px;
160
+ }
161
+
162
+ /* The tab that survives hiding. Sits on the right edge, mid-height, and only
163
+ exists while the rail is away. */
164
+ html[data-ls-nav="off"] .ls-nav-show {
165
+ display: inline-flex; align-items: center; gap: 0.4rem;
166
+ position: fixed; top: 50%; right: 0; z-index: 45; transform: translateY(-50%);
167
+ background: var(--card); border: 1px solid var(--border); border-right: 0;
168
+ padding: 0.6rem 0.5rem; font-size: 0.8rem;
169
+ }
170
+ }
69
171
 
70
172
  /* ── burger + mobile menu ── */
71
173
  .nav-burger {
@@ -86,7 +188,9 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
86
188
  padding: 0.4rem 1.25rem 0.9rem; max-height: calc(100vh - 6rem); overflow-y: auto;
87
189
  }
88
190
  .site-nav.open { display: block; }
89
- .site-nav > .dropdown { display: none; } /* desktop ls-dropdown replaced by the flat list */
191
+ /* The desktop rail is a desktop idea: a fixed 17rem column would eat a phone.
192
+ Below the breakpoint the burger owns navigation and the flat list replaces it. */
193
+ .site-nav > .ls-nav, .ls-nav-show { display: none; }
90
194
 
91
195
  .mobile-nav { display: flex; flex-direction: column; }
92
196
  .mobile-nav .mobile-item {
@@ -352,3 +352,126 @@ html.anim-off .cursor-block {
352
352
  transform: translateY(0.12em);
353
353
  }
354
354
  }
355
+
356
+ /* ═══════════════════════════════════════════════════════════════════════════════════════════════
357
+ Shared page vocabulary, promoted from the cockpit portal (2026-08-04).
358
+
359
+ Each of these was built once in cockpit/pages/portal.css and then wanted by every other
360
+ surface. They live here so there is ONE definition — the tabs had already drifted into FOUR
361
+ page style blocks before being consolidated, which is exactly the failure this move prevents.
362
+ ═════════════════════════════════════════════════════════════════════════════════════════════ */
363
+
364
+ /* ── tabs ──────────────────────────────────────────────────────────────────────────────────────
365
+ Daniel: "I am always searching the tabs. They are not falling into my eyes directly."
366
+
367
+ They were low-contrast pills at 0.7 opacity, and the SELECTED one differed from the rest only
368
+ by a slightly darker border and 8% background — so the row read as six equal chips and the eye
369
+ had to stop and compare them. This inverts it: the inactive tabs recede to plain muted text
370
+ with no chrome at all, and the active one is the only thing on the row carrying weight, colour
371
+ and a fill. Nothing to compare — one thing is loud. */
372
+ .tabs {
373
+ display: flex; flex-wrap: wrap; gap: 0.15rem; margin: 1.35rem 0 0;
374
+ border-bottom: 2px solid color-mix(in srgb, var(--border) 80%, transparent);
375
+ }
376
+ .tab {
377
+ background: transparent; border: 0; margin-bottom: -2px;
378
+ font: inherit; font-size: 0.86em; cursor: pointer; padding: 0.5rem 0.95rem;
379
+ color: var(--muted-foreground); transition: background 0.12s ease, color 0.12s ease;
380
+ }
381
+ .tab:hover { color: var(--primary); background: color-mix(in srgb, var(--primary) 10%, transparent); }
382
+ /* FILLED, not underlined. An underline plus a colour shift was still one thin rule and a hue
383
+ change on a row of same-weight words — Daniel was still hunting for the row. A solid block of
384
+ --primary with the page background as its text is the highest-contrast pair the theme owns
385
+ (5.6:1 on warm), and it is the only filled thing on the page, so it is found without reading. */
386
+ .tab[aria-selected="true"] { background: var(--primary); color: var(--background); font-weight: 700; }
387
+ .tab:focus-visible { outline: 2px solid color-mix(in srgb, var(--primary) 55%, transparent); outline-offset: -2px; }
388
+ /* 44px on a touch screen — tabs are the primary navigation of these pages, so they are the last
389
+ control that should be hard to hit. */
390
+ @media (pointer: coarse), (max-width: 40rem) {
391
+ .tab { padding: 0.7rem 0.8rem; min-height: 44px; }
392
+ }
393
+ section.doc.tab-panel { margin-top: 1.35rem; }
394
+
395
+ /* ── fold: progressive disclosure whose affordance is always visible ───────────────────────────
396
+ The detail behind a visual has to open by TAP — these pages are read from a phone, where hover
397
+ does not exist — and the control has to be findable WITHOUT hovering, or the detail is simply
398
+ text that was deleted. Hence a permanent ▸ marker and a full-width summary row rather than
399
+ anything that appears on hover.
400
+
401
+ Pair with a `beforeprint` handler that opens every <details>, or a printout silently loses
402
+ whatever the closed ones were holding. */
403
+ details.fold { border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent); }
404
+ details.fold > summary {
405
+ display: flex; align-items: center; gap: 0.5rem; cursor: pointer; list-style: none;
406
+ padding: 0.55rem 0; font-size: 0.85rem; color: var(--muted-foreground);
407
+ }
408
+ details.fold > summary::-webkit-details-marker { display: none; }
409
+ details.fold > summary::before { content: "\25B8"; flex: none; opacity: 0.6; }
410
+ details.fold[open] > summary::before { content: "\25BE"; }
411
+ details.fold > summary:hover { color: var(--primary); }
412
+ details.fold > summary:focus-visible {
413
+ outline: 2px solid color-mix(in srgb, currentColor 45%, transparent); outline-offset: 2px;
414
+ }
415
+ .fold-body { font-size: 0.9rem; padding: 0 0 0.7rem 1.1rem; }
416
+ .fold-body > * + * { margin-top: 0.6rem; }
417
+
418
+ /* ── legend: one rendering of each state, beside the thing it keys ─────────────────────────────
419
+ An item is `display: block`, NOT flex: a gloss contains <strong>/<code>/<em>, and every one of
420
+ those would become its own flex item — which is how a single sentence first rendered as four
421
+ separately-wrapped columns. */
422
+ .legend {
423
+ list-style: none; padding: 0; margin: 0.7rem 0 0;
424
+ display: grid; gap: 0.4rem 1.4rem; font-size: 0.86rem;
425
+ }
426
+ @media (min-width: 52rem) { .legend { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
427
+ .legend > li { line-height: 1.6; }
428
+ .legend > li > :first-child { margin-right: 0.45rem; }
429
+
430
+ /* ── ticker strip: one standard header for every poller ────────────────────────────────────────
431
+ Daniel: "For all pollers / tickers we should make a standard layout ... on top of the pages,
432
+ compact and like dashboard style."
433
+
434
+ Every poller answers the same four questions — what ran, when, when next, what did it count —
435
+ and each page was answering them in its own prose box, so "is anything stalled?" took four
436
+ paragraphs instead of one glance.
437
+
438
+ A real TABLE, not a grid per row. The first version made each row its own `display: grid`,
439
+ which cannot align anything across rows: every row sized its columns to its own content, so
440
+ `next` sat wherever that row's name happened to end and the layout moved when a label changed.
441
+ One table means one set of column widths for the whole strip, and scanning a column is the only
442
+ thing the strip is for. Borderless by design — rules between cells draw a grid nobody reads.
443
+
444
+ Colour comes only from --primary and --destructive, both per-theme. Hardcoded accents miss AA
445
+ on at least one theme (measured 2026-08-04), so a strip inventing its own greens and ambers
446
+ would have added five more. */
447
+ .tickstrip:not(:empty) { border: 1px solid var(--border); background: var(--card); margin: 0 0 1.25rem; }
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; }
450
+ /* Only the last column may grow, so the four fixed columns keep the same width on every row. */
451
+ .ticktable td.tick-stats { width: 100%; white-space: normal; }
452
+ .tick { border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent); }
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); }
461
+ .tick-last, .tick-next { font-variant-numeric: tabular-nums; color: var(--muted-foreground); }
462
+ .tick--stale .tick-last { color: var(--destructive); font-weight: 700; }
463
+ .tick--never .tick-last { font-style: italic; }
464
+ /* The NUMBER is what gets scanned; its label is context. Weight and colour on the value alone
465
+ means a row reads as its numbers first and its wording second. */
466
+ .tick-stats { color: var(--muted-foreground); }
467
+ .tick-stats b { font-weight: 700; color: var(--primary); font-variant-numeric: tabular-nums; }
468
+ .tick-sep { font-style: normal; opacity: 0.35; margin: 0 0.5rem; }
469
+ /* A stale row earns the only background tint in the strip — an all-tinted strip is one the eye
470
+ stops reading. */
471
+ .tick--stale { background: color-mix(in srgb, var(--destructive) 8%, transparent); }
472
+ @media (max-width: 40rem) {
473
+ .ticktable, .ticktable tbody, .tick, .ticktable td { display: block; }
474
+ .tick { padding: 0.4rem 0.5rem; }
475
+ .ticktable td { display: inline-block; padding: 0.1rem 0.5rem 0.1rem 0; }
476
+ .ticktable td.tick-stats { display: block; width: auto; }
477
+ }
package/src/print.css CHANGED
@@ -131,6 +131,14 @@
131
131
  display: none !important;
132
132
  }
133
133
 
134
+ /* The `ls -l` rail sits inside .site-nav, so the rule above already hides it —
135
+ but the room it reserved is padding on the BODY, and that would print as a
136
+ 17rem gutter down the right of every page. Hiding a fixed element does not
137
+ reclaim the space something else was told to leave for it. */
138
+ body {
139
+ padding-right: 0 !important;
140
+ }
141
+
134
142
  [data-tip] {
135
143
  cursor: auto;
136
144
  }
package/src/tokens.css CHANGED
@@ -9,6 +9,26 @@
9
9
  * These are plain CSS custom properties — they work in any framework, any
10
10
  * browser, with or without a build step. Load this file first; base.css and
11
11
  * components.css resolve against the variables declared here.
12
+ *
13
+ * ── WHY EVERY ACCENT IS PER-THEME, AND WHY A LITERAL IS NEVER OK ──
14
+ * A status colour has to clear WCAG AA 4.5:1 against every surface it can land
15
+ * on — --background, --card AND --muted. Do that arithmetic across the four
16
+ * themes and the two admissible luminance bands DO NOT OVERLAP:
17
+ *
18
+ * light themes: darkest surface is warm's --muted #ece3cf -> accent L <= 0.1328
19
+ * dark themes: lightest surface is green's --muted #071509 -> accent L >= 0.2021
20
+ * (mono's #111111 wants accent L >= 0.2002)
21
+ *
22
+ * A gap of 0.0692 with nothing in it. So NO single hex can serve four themes:
23
+ * the two near-black themes need a light tint, the two light themes a dark one,
24
+ * and every compromise fails both. That is arithmetic, not taste — which is why
25
+ * --destructive, --success and --warning are each declared four times.
26
+ *
27
+ * Cockpit carried twelve hardcoded accents that predate these tokens; every one
28
+ * missed AA on at least one theme and four sat inside the gap, clearing neither
29
+ * side. Adding a status colour? Add a TOKEN here with its measured worst-case
30
+ * ratio in a trailing comment. Do not tune a literal at the call site — that
31
+ * forks the palette and fixes exactly one theme.
12
32
  */
13
33
 
14
34
  :root {
@@ -27,6 +47,8 @@
27
47
  --accent: #8a4516;
28
48
  --accent-foreground: #f8f3e8;
29
49
  --destructive: #a02c2c;
50
+ --success: #1a6b2e; /* 5.16:1 worst surface */
51
+ --warning: #845600; /* 4.97:1 worst surface */
30
52
  --border: #d9cdb6;
31
53
  --input: #d9cdb6;
32
54
  --ring: #8a4516;
@@ -57,6 +79,8 @@ html[data-theme="green"] {
57
79
  --accent: #33ff66;
58
80
  --accent-foreground: #02160a;
59
81
  --destructive: #ff5c5c;
82
+ --success: #4ade80; /* 10.76:1 worst surface */
83
+ --warning: #f5b32b; /* 10.15:1 worst surface */
60
84
  --border: #1c4a2c;
61
85
  --input: #1c4a2c;
62
86
  --ring: #33ff66;
@@ -82,6 +106,8 @@ html[data-theme="mono"] {
82
106
  --accent: #ffffff;
83
107
  --accent-foreground: #050505;
84
108
  --destructive: #ff5c5c;
109
+ --success: #4ade80; /* 10.84:1 worst surface */
110
+ --warning: #f5b32b; /* 10.22:1 worst surface */
85
111
  --border: #333333;
86
112
  --input: #333333;
87
113
  --ring: #ffffff;
@@ -107,6 +133,8 @@ html[data-theme="paper"] {
107
133
  --accent: #000000;
108
134
  --accent-foreground: #fafafa;
109
135
  --destructive: #b3261e;
136
+ --success: #1a7031; /* 5.36:1 worst surface */
137
+ --warning: #8f5d00; /* 4.89:1 worst surface */
110
138
  --border: #d4d4d4;
111
139
  --input: #d4d4d4;
112
140
  --ring: #000000;
@@ -18,6 +18,8 @@
18
18
  "accent": "#8a4516",
19
19
  "accent-foreground": "#f8f3e8",
20
20
  "destructive": "#a02c2c",
21
+ "success": "#1a6b2e",
22
+ "warning": "#845600",
21
23
  "border": "#d9cdb6",
22
24
  "input": "#d9cdb6",
23
25
  "ring": "#8a4516",
@@ -43,6 +45,8 @@
43
45
  "accent": "#33ff66",
44
46
  "accent-foreground": "#02160a",
45
47
  "destructive": "#ff5c5c",
48
+ "success": "#4ade80",
49
+ "warning": "#f5b32b",
46
50
  "border": "#1c4a2c",
47
51
  "input": "#1c4a2c",
48
52
  "ring": "#33ff66",
@@ -66,6 +70,8 @@
66
70
  "accent": "#ffffff",
67
71
  "accent-foreground": "#050505",
68
72
  "destructive": "#ff5c5c",
73
+ "success": "#4ade80",
74
+ "warning": "#f5b32b",
69
75
  "border": "#333333",
70
76
  "input": "#333333",
71
77
  "ring": "#ffffff",
@@ -89,6 +95,8 @@
89
95
  "accent": "#000000",
90
96
  "accent-foreground": "#fafafa",
91
97
  "destructive": "#b3261e",
98
+ "success": "#1a7031",
99
+ "warning": "#8f5d00",
92
100
  "border": "#d4d4d4",
93
101
  "input": "#d4d4d4",
94
102
  "ring": "#000000",