@danieldeusing/design 0.1.6 → 0.2.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.2.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
2
2
  /* ===== reset.css ===== */
3
3
  /*
4
4
  * danieldeusing-design — minimal reset.
@@ -619,12 +619,135 @@ html.anim-off .cursor-block {
619
619
  }
620
620
  }
621
621
 
622
+ /* ═══════════════════════════════════════════════════════════════════════════════════════════════
623
+ Shared page vocabulary, promoted from the cockpit portal (2026-08-04).
624
+
625
+ Each of these was built once in cockpit/pages/portal.css and then wanted by every other
626
+ surface. They live here so there is ONE definition — the tabs had already drifted into FOUR
627
+ page style blocks before being consolidated, which is exactly the failure this move prevents.
628
+ ═════════════════════════════════════════════════════════════════════════════════════════════ */
629
+
630
+ /* ── tabs ──────────────────────────────────────────────────────────────────────────────────────
631
+ Daniel: "I am always searching the tabs. They are not falling into my eyes directly."
632
+
633
+ They were low-contrast pills at 0.7 opacity, and the SELECTED one differed from the rest only
634
+ by a slightly darker border and 8% background — so the row read as six equal chips and the eye
635
+ had to stop and compare them. This inverts it: the inactive tabs recede to plain muted text
636
+ with no chrome at all, and the active one is the only thing on the row carrying weight, colour
637
+ and a fill. Nothing to compare — one thing is loud. */
638
+ .tabs {
639
+ display: flex; flex-wrap: wrap; gap: 0.15rem; margin: 1.35rem 0 0;
640
+ border-bottom: 2px solid color-mix(in srgb, var(--border) 80%, transparent);
641
+ }
642
+ .tab {
643
+ background: transparent; border: 0; margin-bottom: -2px;
644
+ font: inherit; font-size: 0.86em; cursor: pointer; padding: 0.5rem 0.95rem;
645
+ color: var(--muted-foreground); transition: background 0.12s ease, color 0.12s ease;
646
+ }
647
+ .tab:hover { color: var(--primary); background: color-mix(in srgb, var(--primary) 10%, transparent); }
648
+ /* FILLED, not underlined. An underline plus a colour shift was still one thin rule and a hue
649
+ change on a row of same-weight words — Daniel was still hunting for the row. A solid block of
650
+ --primary with the page background as its text is the highest-contrast pair the theme owns
651
+ (5.6:1 on warm), and it is the only filled thing on the page, so it is found without reading. */
652
+ .tab[aria-selected="true"] { background: var(--primary); color: var(--background); font-weight: 700; }
653
+ .tab:focus-visible { outline: 2px solid color-mix(in srgb, var(--primary) 55%, transparent); outline-offset: -2px; }
654
+ /* 44px on a touch screen — tabs are the primary navigation of these pages, so they are the last
655
+ control that should be hard to hit. */
656
+ @media (pointer: coarse), (max-width: 40rem) {
657
+ .tab { padding: 0.7rem 0.8rem; min-height: 44px; }
658
+ }
659
+ section.doc.tab-panel { margin-top: 1.35rem; }
660
+
661
+ /* ── fold: progressive disclosure whose affordance is always visible ───────────────────────────
662
+ The detail behind a visual has to open by TAP — these pages are read from a phone, where hover
663
+ does not exist — and the control has to be findable WITHOUT hovering, or the detail is simply
664
+ text that was deleted. Hence a permanent ▸ marker and a full-width summary row rather than
665
+ anything that appears on hover.
666
+
667
+ Pair with a `beforeprint` handler that opens every <details>, or a printout silently loses
668
+ whatever the closed ones were holding. */
669
+ details.fold { border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent); }
670
+ details.fold > summary {
671
+ display: flex; align-items: center; gap: 0.5rem; cursor: pointer; list-style: none;
672
+ padding: 0.55rem 0; font-size: 0.85rem; color: var(--muted-foreground);
673
+ }
674
+ details.fold > summary::-webkit-details-marker { display: none; }
675
+ details.fold > summary::before { content: "\25B8"; flex: none; opacity: 0.6; }
676
+ details.fold[open] > summary::before { content: "\25BE"; }
677
+ details.fold > summary:hover { color: var(--primary); }
678
+ details.fold > summary:focus-visible {
679
+ outline: 2px solid color-mix(in srgb, currentColor 45%, transparent); outline-offset: 2px;
680
+ }
681
+ .fold-body { font-size: 0.9rem; padding: 0 0 0.7rem 1.1rem; }
682
+ .fold-body > * + * { margin-top: 0.6rem; }
683
+
684
+ /* ── legend: one rendering of each state, beside the thing it keys ─────────────────────────────
685
+ An item is `display: block`, NOT flex: a gloss contains <strong>/<code>/<em>, and every one of
686
+ those would become its own flex item — which is how a single sentence first rendered as four
687
+ separately-wrapped columns. */
688
+ .legend {
689
+ list-style: none; padding: 0; margin: 0.7rem 0 0;
690
+ display: grid; gap: 0.4rem 1.4rem; font-size: 0.86rem;
691
+ }
692
+ @media (min-width: 52rem) { .legend { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
693
+ .legend > li { line-height: 1.6; }
694
+ .legend > li > :first-child { margin-right: 0.45rem; }
695
+
696
+ /* ── ticker strip: one standard header for every poller ────────────────────────────────────────
697
+ Daniel: "For all pollers / tickers we should make a standard layout ... on top of the pages,
698
+ compact and like dashboard style."
699
+
700
+ Every poller answers the same four questions — what ran, when, when next, what did it count —
701
+ and each page was answering them in its own prose box, so "is anything stalled?" took four
702
+ paragraphs instead of one glance.
703
+
704
+ A real TABLE, not a grid per row. The first version made each row its own `display: grid`,
705
+ which cannot align anything across rows: every row sized its columns to its own content, so
706
+ `next` sat wherever that row's name happened to end and the layout moved when a label changed.
707
+ One table means one set of column widths for the whole strip, and scanning a column is the only
708
+ thing the strip is for. Borderless by design — rules between cells draw a grid nobody reads.
709
+
710
+ Colour comes only from --primary and --destructive, both per-theme. Hardcoded accents miss AA
711
+ on at least one theme (measured 2026-08-04), so a strip inventing its own greens and ambers
712
+ would have added five more. */
713
+ .tickstrip:not(:empty) { border: 1px solid var(--border); background: var(--card); margin: 0 0 1.25rem; }
714
+ .ticktable { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
715
+ .ticktable td { padding: 0.45rem 0.55rem; vertical-align: baseline; border: 0; white-space: nowrap; }
716
+ /* Only the last column may grow, so the four fixed columns keep the same width on every row. */
717
+ .ticktable td.tick-stats { width: 100%; white-space: normal; }
718
+ .tick { border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent); }
719
+ .tick:last-child { border-bottom: 0; }
720
+ /* The glyph carries the state WITHOUT colour as well as with it — two of the four themes are
721
+ near-black and a printout has none, so ● vs ✕ vs ○ has to survive on its own. */
722
+ .tick-dot { font-size: 0.7em; width: 1rem; padding-right: 0 !important; }
723
+ .tick--ok .tick-dot { color: var(--primary); }
724
+ .tick--stale .tick-dot { color: var(--destructive); }
725
+ .tick--never .tick-dot { color: var(--muted-foreground); }
726
+ .tick-name { font-weight: 700; color: var(--primary); }
727
+ .tick-last, .tick-next { font-variant-numeric: tabular-nums; color: var(--muted-foreground); }
728
+ .tick--stale .tick-last { color: var(--destructive); font-weight: 700; }
729
+ .tick--never .tick-last { font-style: italic; }
730
+ /* The NUMBER is what gets scanned; its label is context. Weight and colour on the value alone
731
+ means a row reads as its numbers first and its wording second. */
732
+ .tick-stats { color: var(--muted-foreground); }
733
+ .tick-stats b { font-weight: 700; color: var(--primary); font-variant-numeric: tabular-nums; }
734
+ .tick-sep { font-style: normal; opacity: 0.35; margin: 0 0.5rem; }
735
+ /* A stale row earns the only background tint in the strip — an all-tinted strip is one the eye
736
+ stops reading. */
737
+ .tick--stale { background: color-mix(in srgb, var(--destructive) 8%, transparent); }
738
+ @media (max-width: 40rem) {
739
+ .ticktable, .ticktable tbody, .tick, .ticktable td { display: block; }
740
+ .tick { padding: 0.4rem 0.5rem; }
741
+ .ticktable td { display: inline-block; padding: 0.1rem 0.5rem 0.1rem 0; }
742
+ .ticktable td.tick-stats { display: block; width: auto; }
743
+ }
744
+
622
745
  /* ===== chrome.css ===== */
623
746
  /*
624
747
  * 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.
748
+ * and the responsive site navigation (desktop `ls -l` RAIL, mobile burger with
749
+ * the footer folded in). Extracted from danieldeusing.de and the cockpit portal
750
+ * so every app ships the same chrome without rebuilding it.
628
751
  *
629
752
  * Markup contract (see templates/documentation.html for the full example):
630
753
  *
@@ -633,7 +756,14 @@ html.anim-off .cursor-block {
633
756
  * <a href="/" class="brand"><span class="glow">name</span><span class="cursor-block"></span></a>
634
757
  * <button class="nav-burger" data-nav-toggle aria-controls="site-nav" aria-expanded="false">…☰ svg…</button>
635
758
  * <nav aria-label="site" id="site-nav" class="site-nav">
636
- * <details class="dropdown" id="nav">…ls-panel with .ls-row items…</details>
759
+ * <div class="ls-nav" id="nav">
760
+ * <div class="ls-nav-head">
761
+ * <span class="ls-nav-title">ls -l</span>
762
+ * <button class="ls-nav-hide" data-ls-nav-toggle aria-controls="nav" aria-expanded="true">»</button>
763
+ * </div>
764
+ * <ul class="ls-panel">….ls-row items…</ul>
765
+ * </div>
766
+ * <button class="ls-nav-show" data-ls-nav-toggle aria-controls="nav">« ls -l</button>
637
767
  * <ul class="mobile-nav">…same links as .mobile-item ls-rows…</ul>
638
768
  * <div class="mobile-footer">…footer controls folded in…</div>
639
769
  * </nav>
@@ -641,8 +771,15 @@ html.anim-off .cursor-block {
641
771
  * …
642
772
  * <footer class="status">…status-left + status-right…</footer>
643
773
  *
644
- * Behaviour: pair with `initBurgerNav()` from the runtime (adds the toggle +
645
- * outside-click close). Breakpoint: 48rem.
774
+ * Behaviour: pair with `initBurgerNav()` (mobile burger) and `initLsNav()`
775
+ * (rail show/hide) from the runtime. The rail's SHOWN/HIDDEN state must also be
776
+ * applied before first paint by an inline <head> script — see the rail section
777
+ * below for why the runtime cannot be the one to do it:
778
+ *
779
+ * try { if (localStorage.getItem("ls-nav") === "off")
780
+ * document.documentElement.dataset.lsNav = "off"; } catch {}
781
+ *
782
+ * Breakpoint: 48rem.
646
783
  */
647
784
 
648
785
  /* ── a11y helpers ── */
@@ -682,12 +819,100 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
682
819
  .doc-link { color: var(--muted-foreground); text-decoration: none; transition: color 0.15s ease; }
683
820
  .doc-link:hover { color: var(--primary); }
684
821
 
685
- /* ── ls-listing rows (shared by the desktop dropdown + mobile menu) ── */
822
+ /* ── ls-listing rows (shared by the desktop rail + mobile menu) ── */
686
823
  .ls-row { display: flex; align-items: baseline; gap: 0.9rem; }
687
824
  .ls-perm { color: var(--muted-foreground); font-size: 0.72rem; letter-spacing: 0; }
688
825
  .ls-row--sub .ls-name { padding-left: 1rem; }
689
- .ls-panel { min-width: 17rem; left: auto !important; right: 0; }
690
- #nav { position: relative; }
826
+ /* The pre-rail dropdown form. Kept, and scoped to the dropdown panel, because the
827
+ other surfaces still ship that markup — when they unpin they get this file, and
828
+ their nav should stay right-aligned rather than quietly jumping to the left. The
829
+ rail's own <ul> carries `ls-panel` WITHOUT `dropdown-panel`, so it is unaffected. */
830
+ .dropdown-panel.ls-panel { min-width: 17rem; left: auto !important; right: 0; }
831
+
832
+ /* ── `ls -l` site rail (desktop ≥48rem): sticky, full-height, right-hand panel ──
833
+ Was a dropdown that had to be re-opened on every page. Daniel: "Always make it
834
+ sticky (default shown) right side, keep `ls -l`, but not down arrow, but
835
+ something which tells the user he can hide / minimize."
836
+
837
+ SHOWN BY DEFAULT, and the state lives in localStorage under "ls-nav". It is
838
+ applied by an inline <head> script as `html[data-ls-nav="off"]` BEFORE first
839
+ paint — not by the runtime — because the runtime is a module at the end of
840
+ <body>: a reader who hid the rail would otherwise watch it paint and then jump
841
+ away on every single page load. `initLsNav()` only handles the clicks.
842
+
843
+ The affordance is deliberately NOT a caret. A caret means "a menu drops from
844
+ here", which is the thing this stopped being. Guillemets point the way the
845
+ panel travels — » pushes it off to the right, « pulls it back — and hiding it
846
+ leaves a small labelled tab on the edge rather than nothing, so a hidden rail
847
+ is still discoverable by someone who did not hide it themselves.
848
+
849
+ Width is one token so a consumer can widen it for longer trees. */
850
+ :root { --ls-nav-w: 17rem; }
851
+ .ls-nav-show { display: none; }
852
+
853
+ @media (min-width: 48.0625rem) {
854
+ /* Room is reserved ONLY when a rail is actually in the page, and the default is
855
+ zero. This is what makes the stylesheet safe to publish ahead of a consumer's
856
+ markup: every surface that still ships the old dropdown nav (or no nav at all)
857
+ keeps its full width instead of growing a 17rem gutter with nothing in it —
858
+ which is exactly what they would have got the moment they unpinned.
859
+ It also fails in the right direction: if :has() is unavailable the selector is
860
+ dropped, the inset stays 0, and the rail overlays content rather than the page
861
+ reserving space for a rail that never arrives. */
862
+ html { --ls-nav-inset: 0rem; }
863
+ html:has(.ls-nav):not([data-ls-nav="off"]) { --ls-nav-inset: var(--ls-nav-w); }
864
+
865
+ /* Everything in normal flow shifts left, so content sits against the rail's
866
+ left edge instead of running under it. The fixed footer has to be told
867
+ separately: `inset-inline: 0` ignores the body's padding. */
868
+ /* Deliberately NOT transitioned. Animating padding on <body> reflows the whole
869
+ document every frame, and the pages this ships to render tables of hundreds of
870
+ rows — the rail's own transform is composited and costs nothing, so the panel
871
+ slides while the content reflows once. Animating both looked no better and
872
+ made the containers dashboard stutter on toggle. */
873
+ body { padding-right: var(--ls-nav-inset); }
874
+ footer.status { right: var(--ls-nav-inset); }
875
+
876
+ .ls-nav {
877
+ position: fixed; top: 0; right: 0; bottom: 0; width: var(--ls-nav-w); z-index: 45;
878
+ display: flex; flex-direction: column;
879
+ background: var(--card); border-left: 1px solid var(--border);
880
+ }
881
+ html:not(.anim-off) .ls-nav,
882
+ html:not(.anim-off) .ls-nav-show { transition: transform 0.18s ease; }
883
+ html[data-ls-nav="off"] .ls-nav { transform: translateX(100%); }
884
+
885
+ .ls-nav-head {
886
+ flex: none; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
887
+ padding: 0.6rem 0.5rem 0.6rem 0.85rem; border-bottom: 1px solid var(--border);
888
+ }
889
+ .ls-nav-title { font-weight: 700; font-size: 0.9rem; }
890
+
891
+ /* The list is the only thing that scrolls — the head stays put, so the control
892
+ that hides the rail cannot be scrolled out of reach. */
893
+ .ls-nav .ls-panel {
894
+ flex: 1; min-height: 0; overflow-y: auto;
895
+ list-style: none; margin: 0; padding: 0.5rem 0 1rem;
896
+ }
897
+
898
+ .ls-nav-hide, .ls-nav-show {
899
+ background: none; border: 0; font: inherit; cursor: pointer; line-height: 1;
900
+ color: var(--muted-foreground); padding: 0.4rem 0.55rem;
901
+ }
902
+ .ls-nav-hide:hover, .ls-nav-show:hover { color: var(--primary); }
903
+ .ls-nav-hide:focus-visible, .ls-nav-show:focus-visible {
904
+ outline: 2px solid color-mix(in srgb, var(--primary) 55%, transparent); outline-offset: -2px;
905
+ }
906
+
907
+ /* The tab that survives hiding. Sits on the right edge, mid-height, and only
908
+ exists while the rail is away. */
909
+ html[data-ls-nav="off"] .ls-nav-show {
910
+ display: inline-flex; align-items: center; gap: 0.4rem;
911
+ position: fixed; top: 50%; right: 0; z-index: 45; transform: translateY(-50%);
912
+ background: var(--card); border: 1px solid var(--border); border-right: 0;
913
+ padding: 0.6rem 0.5rem; font-size: 0.8rem;
914
+ }
915
+ }
691
916
 
692
917
  /* ── burger + mobile menu ── */
693
918
  .nav-burger {
@@ -708,7 +933,9 @@ footer.status .dropdown summary, footer.status .anim-toggle { padding: 0.55rem 0
708
933
  padding: 0.4rem 1.25rem 0.9rem; max-height: calc(100vh - 6rem); overflow-y: auto;
709
934
  }
710
935
  .site-nav.open { display: block; }
711
- .site-nav > .dropdown { display: none; } /* desktop ls-dropdown replaced by the flat list */
936
+ /* The desktop rail is a desktop idea: a fixed 17rem column would eat a phone.
937
+ Below the breakpoint the burger owns navigation and the flat list replaces it. */
938
+ .site-nav > .ls-nav, .ls-nav-show { display: none; }
712
939
 
713
940
  .mobile-nav { display: flex; flex-direction: column; }
714
941
  .mobile-nav .mobile-item {
@@ -895,6 +1122,14 @@ span[data-tip], th[data-tip] { border-bottom: 1px dotted var(--muted-foreground)
895
1122
  display: none !important;
896
1123
  }
897
1124
 
1125
+ /* The `ls -l` rail sits inside .site-nav, so the rule above already hides it —
1126
+ but the room it reserved is padding on the BODY, and that would print as a
1127
+ 17rem gutter down the right of every page. Hiding a fixed element does not
1128
+ reclaim the space something else was told to leave for it. */
1129
+ body {
1130
+ padding-right: 0 !important;
1131
+ }
1132
+
898
1133
  [data-tip] {
899
1134
  cursor: auto;
900
1135
  }
@@ -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.2.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;--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)}}.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.2.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
  }