@lepid-labs/styles 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,11 @@
2
2
  @import "./tokens.css";
3
3
  @import "./base.css";
4
4
  @import "./components/card.css";
5
+ @import "./components/status-card.css";
5
6
  @import "./components/link.css";
6
7
  @import "./components/button.css";
8
+ @import "./components/button-hold.css";
9
+ @import "./components/button-group.css";
7
10
  @import "./components/form.css";
8
11
  @import "./components/badge.css";
9
12
  @import "./components/alert.css";
@@ -11,5 +14,6 @@
11
14
  @import "./components/tabs.css";
12
15
  @import "./components/table.css";
13
16
  @import "./components/progress.css";
17
+ @import "./components/stepper.css";
14
18
  @import "./components/muted.css";
15
19
  @import "./components/pre.css";
@@ -62,6 +62,12 @@
62
62
  --ld-radius-lg: 1rem; /* cards and large containers */
63
63
  --ld-blur: 12px;
64
64
  --ld-transition: 0.3s;
65
+ --ld-hold-duration: 300ms; /* (extra) hold-to-confirm commitment window */
66
+ /* (extra) sliding indicator — tabs underline, button-group pill. The edge
67
+ * nearer the target leads; the far edge trails on a tight, near-critically-damped spring that barely overshoots
68
+ * inward, so the indicator stretches across the gap and snaps back to shape. */
69
+ --ld-slide-lead: 0.2s cubic-bezier(0.2, 0.9, 0.3, 1);
70
+ --ld-slide-trail: 0.45s linear(0, 0.044, 0.148, 0.279, 0.415, 0.544, 0.657, 0.753, 0.83, 0.889, 0.934, 0.966, 0.988, 1.002, 1.01, 1.014, 1.015, 1.015, 1.013, 1.011, 1.009, 1.007, 1.005, 1.004, 1);
65
71
 
66
72
  /* spacing (8px rhythm) */
67
73
  --ld-space-1: 0.25rem;
@@ -15,3 +15,28 @@
15
15
  :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-badge--success { border-color: var(--ld-success); color: var(--ld-success); }
16
16
  :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-badge--warning { border-color: var(--ld-warning); color: var(--ld-warning); }
17
17
  :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-badge--danger { border-color: var(--ld-danger); color: var(--ld-danger); }
18
+
19
+ /* Compact size — dashboard pills inside .ld-status-card__meta and row trails. */
20
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-badge--sm {
21
+ padding: 0.125rem 0.375rem;
22
+ font-size: 9px;
23
+ letter-spacing: 0.06em;
24
+ }
25
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-badge[href] { text-decoration: none; }
26
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-badge[href]:hover { filter: brightness(1.1); }
27
+
28
+ /* Pulse — the one solid, glowing call to action (work is waiting on you). */
29
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-badge--pulse {
30
+ background: var(--ld-warning);
31
+ border-color: transparent;
32
+ color: var(--ld-bg);
33
+ box-shadow: 0 0 8px color-mix(in srgb, var(--ld-warning) 60%, transparent);
34
+ animation: ld-nb-badge-pulse 2s ease-in-out infinite;
35
+ }
36
+ @keyframes ld-nb-badge-pulse {
37
+ 0%, 100% { box-shadow: 0 0 6px color-mix(in srgb, var(--ld-warning) 50%, transparent); }
38
+ 50% { box-shadow: 0 0 16px color-mix(in srgb, var(--ld-warning) 90%, transparent); }
39
+ }
40
+ @media (prefers-reduced-motion: reduce) {
41
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-badge--pulse { animation: none; }
42
+ }
@@ -0,0 +1,128 @@
1
+ /* neon-butterfly — button group: one glass track, chromeless segments; the
2
+ * pressed segment takes the lilac edge and glow. */
3
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group {
4
+ display: inline-flex;
5
+ align-items: stretch;
6
+ gap: 2px;
7
+ max-width: 100%;
8
+ padding: 3px;
9
+ background: var(--ld-surface-glass);
10
+ border: 1px solid var(--ld-border);
11
+ border-radius: var(--ld-radius);
12
+ }
13
+ /* --block: fill the row, equal-width segments */
14
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group--block {
15
+ display: flex;
16
+ }
17
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group--block > .ld-btn {
18
+ flex: 1 1 0;
19
+ }
20
+
21
+ /* segments: the track is the chrome, so the button drops its own */
22
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn {
23
+ justify-content: center;
24
+ background: transparent;
25
+ border-color: transparent;
26
+ border-radius: calc(var(--ld-radius) - 3px);
27
+ color: var(--ld-faint);
28
+ white-space: nowrap;
29
+ transition:
30
+ background var(--ld-transition),
31
+ border-color var(--ld-transition),
32
+ color var(--ld-transition);
33
+ }
34
+ /* variants tint the label only */
35
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn--primary {
36
+ color: var(--ld-primary);
37
+ }
38
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn--accent {
39
+ color: var(--ld-accent);
40
+ }
41
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn--danger {
42
+ color: var(--ld-danger);
43
+ }
44
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn:hover,
45
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn:focus-visible {
46
+ background: var(--ld-surface-sunken);
47
+ border-color: transparent;
48
+ color: var(--ld-on-surface);
49
+ animation: none;
50
+ }
51
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn:focus-visible {
52
+ /* an outline, not the border: it must stay visible on the pressed segment */
53
+ outline: 1px solid var(--ld-primary);
54
+ outline-offset: 1px;
55
+ }
56
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn--primary:hover,
57
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn--primary:focus-visible {
58
+ color: var(--ld-primary);
59
+ }
60
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn--accent:hover,
61
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn--accent:focus-visible {
62
+ color: var(--ld-accent);
63
+ }
64
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn--danger:hover,
65
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn--danger:focus-visible {
66
+ color: var(--ld-danger);
67
+ }
68
+
69
+ /* pressed segment (toggle / segmented use) */
70
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn--active,
71
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn[aria-pressed="true"] {
72
+ background: var(--ld-surface-sunken);
73
+ border-color: var(--ld-primary-border);
74
+ color: var(--ld-primary);
75
+ text-shadow: 0 0 8px var(--ld-primary-glow);
76
+ }
77
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn:disabled {
78
+ background: transparent;
79
+ color: var(--ld-faint);
80
+ }
81
+
82
+ /* Sliding pill: with exactly one pressed segment, its chrome moves to one pill
83
+ * on the track, pinned to the segment by anchor positioning, so a change of
84
+ * selection travels instead of blinking. Its inline edges transition
85
+ * separately — with data-ld-dir ("forward" | "back", set by ButtonGroup) the
86
+ * edge nearer the target leads and the far edge trails, stretching the pill
87
+ * across the gap; without it the pill just slides. Several pressed segments
88
+ * (multi-toggle), or no anchor positioning, keep the per-segment chrome above. */
89
+ @supports (anchor-name: --ld-a) and (anchor-scope: --ld-a) {
90
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group {
91
+ position: relative;
92
+ isolation: isolate;
93
+ anchor-scope: --ld-btn-group-active;
94
+ }
95
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group:not(:has(> :is(.ld-btn--active, .ld-btn[aria-pressed="true"]) ~ :is(.ld-btn--active, .ld-btn[aria-pressed="true"]))) > :is(.ld-btn--active, .ld-btn[aria-pressed="true"]) {
96
+ anchor-name: --ld-btn-group-active;
97
+ background: transparent;
98
+ border-color: transparent;
99
+ }
100
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group:where(:has(> :is(.ld-btn--active, .ld-btn[aria-pressed="true"])):not(:has(> :is(.ld-btn--active, .ld-btn[aria-pressed="true"]) ~ :is(.ld-btn--active, .ld-btn[aria-pressed="true"]))))::before {
101
+ content: "";
102
+ position: absolute;
103
+ z-index: -1;
104
+ position-anchor: --ld-btn-group-active;
105
+ inset-block: anchor(top) anchor(bottom);
106
+ inset-inline: anchor(start) anchor(end);
107
+ background: var(--ld-surface-sunken);
108
+ border: 1px solid var(--ld-primary-border);
109
+ border-radius: calc(var(--ld-radius) - 3px);
110
+ box-shadow: 0 0 10px -2px var(--ld-primary-glow);
111
+ pointer-events: none;
112
+ transition: inset-inline-start var(--ld-slide-lead), inset-inline-end var(--ld-slide-lead);
113
+ }
114
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group[data-ld-dir="forward"]::before {
115
+ transition: inset-inline-start var(--ld-slide-trail), inset-inline-end var(--ld-slide-lead);
116
+ }
117
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group[data-ld-dir="back"]::before {
118
+ transition: inset-inline-start var(--ld-slide-lead), inset-inline-end var(--ld-slide-trail);
119
+ }
120
+ }
121
+
122
+ @media (prefers-reduced-motion: reduce) {
123
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group > .ld-btn,
124
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group::before,
125
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn-group[data-ld-dir]::before {
126
+ transition: none;
127
+ }
128
+ }
@@ -0,0 +1,112 @@
1
+ /* neon-butterfly — hold-to-confirm button: lime ring (the activity signal), glow on commit
2
+ *
3
+ * Contract: .ld-btn--hold composes with .ld-btn and any variant. The consumer
4
+ * writes one number, --ld-hold in [0, 1]; the ring fill is pure CSS.
5
+ * data-ld-hold="holding" | "fired" carries the behavioural state.
6
+ */
7
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn--hold {
8
+ --ld-hold: 0;
9
+ --ld-hold-color: var(--ld-on-surface); /* neutral on its own */
10
+ position: relative;
11
+ gap: var(--ld-space-3);
12
+ padding: var(--ld-space-2) var(--ld-space-3) var(--ld-space-2) var(--ld-space-2);
13
+ text-align: left;
14
+ user-select: none;
15
+ -webkit-user-select: none;
16
+ touch-action: none; /* a touch hold must not turn into a scroll */
17
+ }
18
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn--hold.ld-btn--primary {
19
+ --ld-hold-color: var(--ld-primary);
20
+ }
21
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn--hold.ld-btn--accent {
22
+ --ld-hold-color: var(--ld-accent);
23
+ }
24
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn--hold.ld-btn--danger {
25
+ --ld-hold-color: var(--ld-accent);
26
+ }
27
+
28
+ /* ring: the icon well, wrapped by a conic fill masked to a 3px stroke */
29
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn__ring {
30
+ position: relative;
31
+ display: inline-grid;
32
+ place-items: center;
33
+ flex: none;
34
+ width: 2.25rem;
35
+ height: 2.25rem;
36
+ border-radius: 50%;
37
+ background: var(--ld-surface-sunken);
38
+ color: var(--ld-hold-color);
39
+ }
40
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn__ring::before {
41
+ content: "";
42
+ position: absolute;
43
+ inset: 0;
44
+ border-radius: 50%;
45
+ background: conic-gradient(var(--ld-hold-color) calc(var(--ld-hold) * 360deg), var(--ld-border-lit) 0);
46
+ -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2.5px));
47
+ mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2.5px));
48
+ }
49
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn__ring > svg {
50
+ width: 1rem;
51
+ height: 1rem;
52
+ }
53
+
54
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn__body {
55
+ display: flex;
56
+ flex-direction: column;
57
+ align-items: flex-start;
58
+ gap: 0.125rem;
59
+ line-height: 1.2;
60
+ }
61
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn__label {
62
+ font-weight: var(--ld-font-weight-bold);
63
+ }
64
+ /* the hint and meter restate the ring, so state is never color-only */
65
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn__hint {
66
+ font-size: 11px;
67
+ font-weight: var(--ld-font-weight);
68
+ letter-spacing: normal;
69
+ text-transform: none;
70
+ color: var(--ld-faint);
71
+ }
72
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn__meter {
73
+ margin-left: auto;
74
+ min-width: 3.5ch;
75
+ text-align: right;
76
+ font-family: var(--ld-font-mono);
77
+ font-variant-numeric: tabular-nums;
78
+ font-size: 12px;
79
+ color: var(--ld-faint);
80
+ }
81
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn--hold[data-ld-hold="holding"] .ld-btn__meter {
82
+ color: var(--ld-hold-color);
83
+ }
84
+
85
+ /* fired: the post-commit flash */
86
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn--hold[data-ld-hold="fired"] {
87
+ border-color: var(--ld-hold-color);
88
+ animation: ld-nb-hold-fired 0.6s ease-out;
89
+ }
90
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn--hold[data-ld-hold="fired"] .ld-btn__ring {
91
+ box-shadow: 0 0 16px color-mix(in srgb, var(--ld-hold-color) 55%, transparent);
92
+ }
93
+ @keyframes ld-nb-hold-fired {
94
+ from {
95
+ box-shadow: 0 0 28px color-mix(in srgb, var(--ld-hold-color) 70%, transparent);
96
+ }
97
+ to {
98
+ box-shadow: 0 0 12px color-mix(in srgb, var(--ld-hold-color) 35%, transparent);
99
+ }
100
+ }
101
+
102
+ /* reduced motion: the ring still fills (it is the state, not decoration);
103
+ * only the flash and glow go */
104
+ @media (prefers-reduced-motion: reduce) {
105
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn--hold[data-ld-hold="fired"] {
106
+ animation: none;
107
+ box-shadow: none;
108
+ }
109
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-btn--hold[data-ld-hold="fired"] .ld-btn__ring {
110
+ box-shadow: none;
111
+ }
112
+ }
@@ -0,0 +1,156 @@
1
+ /* neon-butterfly — status card: a dense dashboard card whose left edge, ring, and
2
+ * flash carry state (generalized from pulse's repo card).
3
+ * .ld-status-card--<tone> left edge in the semantic color
4
+ * [data-ld-attention="<tone>"] outline + glow ring calling for action
5
+ * [data-ld-changed] replays the change flash while present */
6
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card {
7
+ --ld-status-attention: var(--ld-warning);
8
+ position: relative;
9
+ display: flex;
10
+ flex-direction: column;
11
+ padding: 0;
12
+ background: var(--ld-surface-glass);
13
+ border: 1px solid var(--ld-border);
14
+ border-left: 3px solid transparent;
15
+ border-radius: var(--ld-radius);
16
+ backdrop-filter: blur(var(--ld-blur));
17
+ -webkit-backdrop-filter: blur(var(--ld-blur));
18
+ color: var(--ld-on-surface);
19
+ overflow: hidden;
20
+ transition: background var(--ld-transition), box-shadow var(--ld-transition);
21
+ }
22
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card:hover { box-shadow: 0 0 12px var(--ld-primary-glow); }
23
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card--info { border-left-color: var(--ld-info); }
24
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card--success { border-left-color: var(--ld-success); }
25
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card--warning { border-left-color: var(--ld-warning); }
26
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card--danger { border-left-color: var(--ld-danger); }
27
+
28
+ /* attention ring — the tone rides on the attribute value */
29
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card[data-ld-attention] {
30
+ outline: 1px solid color-mix(in srgb, var(--ld-status-attention) 70%, transparent);
31
+ box-shadow: 0 0 14px color-mix(in srgb, var(--ld-status-attention) 35%, transparent);
32
+ }
33
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card[data-ld-attention]:hover {
34
+ box-shadow: 0 0 18px color-mix(in srgb, var(--ld-status-attention) 55%, transparent);
35
+ }
36
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card[data-ld-attention="info"] { --ld-status-attention: var(--ld-info); }
37
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card[data-ld-attention="success"] { --ld-status-attention: var(--ld-success); }
38
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card[data-ld-attention="danger"] { --ld-status-attention: var(--ld-danger); }
39
+
40
+ /* change flash — fades from a primary tint back to the resting glass */
41
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card[data-ld-changed] { animation: ld-nb-status-flash 30s ease-out forwards; }
42
+ @keyframes ld-nb-status-flash {
43
+ from { background: color-mix(in srgb, var(--ld-primary) 20%, var(--ld-surface)); }
44
+ to { background: var(--ld-surface-glass); }
45
+ }
46
+
47
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__watermark {
48
+ position: absolute;
49
+ top: 0;
50
+ left: -0.25rem;
51
+ width: 2rem;
52
+ height: 2rem;
53
+ color: var(--ld-on-surface);
54
+ opacity: 0.12;
55
+ pointer-events: none;
56
+ rotate: -25deg;
57
+ }
58
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__watermark > svg { width: 100%; height: 100%; }
59
+
60
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__head {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: space-between;
64
+ gap: var(--ld-space-2);
65
+ padding: 0.625rem 0.75rem 0.5rem 0.875rem;
66
+ }
67
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__title {
68
+ min-width: 0;
69
+ overflow: hidden;
70
+ font-size: var(--ld-text-sm);
71
+ font-weight: var(--ld-font-weight-bold);
72
+ white-space: nowrap;
73
+ text-overflow: ellipsis;
74
+ color: inherit;
75
+ text-decoration: none;
76
+ transition: color var(--ld-transition);
77
+ }
78
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__title[href]:hover { color: var(--ld-primary); }
79
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__prefix { color: var(--ld-faint); }
80
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__prefix::after { content: " // "; opacity: 0.5; }
81
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__meta {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 0.375rem;
85
+ flex-shrink: 0;
86
+ }
87
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__note {
88
+ font-size: 11px;
89
+ color: var(--ld-faint);
90
+ white-space: nowrap;
91
+ }
92
+
93
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__rows {
94
+ list-style: none;
95
+ margin: 0;
96
+ padding: 0;
97
+ border-top: 1px solid var(--ld-border);
98
+ }
99
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__row {
100
+ display: grid;
101
+ grid-template-columns: auto 1fr auto;
102
+ align-items: baseline;
103
+ gap: 0.375rem;
104
+ padding: 0.3rem 0.75rem 0.3rem 0.875rem;
105
+ font-size: 12px;
106
+ color: inherit;
107
+ text-decoration: none;
108
+ transition: background 0.1s;
109
+ }
110
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__row[href]:hover { background: color-mix(in srgb, var(--ld-primary) 8%, transparent); }
111
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__row-lead {
112
+ min-width: 1.5rem;
113
+ font-size: 11px;
114
+ font-weight: var(--ld-font-weight-bold);
115
+ color: var(--ld-faint);
116
+ text-align: right;
117
+ font-variant-numeric: tabular-nums;
118
+ }
119
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__row-main {
120
+ min-width: 0;
121
+ overflow: hidden;
122
+ white-space: nowrap;
123
+ text-overflow: ellipsis;
124
+ color: var(--ld-on-surface);
125
+ }
126
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__row-trail {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: var(--ld-space-1);
130
+ flex-shrink: 0;
131
+ font-size: 10px;
132
+ color: var(--ld-faint);
133
+ }
134
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__footer {
135
+ display: block;
136
+ padding: 0.3rem 0.875rem;
137
+ border-top: 1px solid var(--ld-border);
138
+ font-size: 11px;
139
+ color: var(--ld-faint);
140
+ text-decoration: none;
141
+ transition: color var(--ld-transition);
142
+ }
143
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__footer[href]:hover { color: var(--ld-accent); }
144
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card__empty {
145
+ margin: 0;
146
+ padding: 0.375rem 0.875rem 0.5rem;
147
+ border-top: 1px solid var(--ld-border);
148
+ font-size: 12px;
149
+ color: var(--ld-faint);
150
+ opacity: 0.7;
151
+ }
152
+
153
+ @media (prefers-reduced-motion: reduce) {
154
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card { transition: none; }
155
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-status-card[data-ld-changed] { animation: none; }
156
+ }
@@ -0,0 +1,107 @@
1
+ /* neon-butterfly — stepper: a milestone rail with labelled step nodes. Lilac
2
+ * rail and complete nodes; the current node is the lime activity signal.
3
+ * .ld-stepper__step--complete solid lilac node, navy icon
4
+ * .ld-stepper__step--current surface node, 2px lime border, lime glow
5
+ * .ld-stepper__step--upcoming surface node, hairline border, faint icon
6
+ * The fill follows the state modifiers — no progress value to compute. */
7
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper {
8
+ display: flex;
9
+ align-items: flex-start;
10
+ list-style: none;
11
+ margin: 0;
12
+ padding: 0;
13
+ }
14
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step {
15
+ position: relative;
16
+ flex: 1 1 0;
17
+ min-width: 0;
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ gap: var(--ld-space-2);
22
+ text-align: center;
23
+ }
24
+ /* rail segment: from this node's centre to the next node's centre; the track is
25
+ * ::before, the fill ::after. Segments meet under the nodes, so the rail reads
26
+ * as one bar and the fill ends exactly at the current node. */
27
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step::before,
28
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step::after {
29
+ content: "";
30
+ position: absolute;
31
+ top: 0.75rem;
32
+ left: 50%;
33
+ width: 100%;
34
+ height: 0.5rem;
35
+ border-radius: 1rem;
36
+ }
37
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step:last-child::before,
38
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step:last-child::after { display: none; }
39
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step::before {
40
+ background: var(--ld-surface);
41
+ box-shadow: inset 0 0 0 1px var(--ld-border);
42
+ }
43
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step::after {
44
+ width: 0;
45
+ background: var(--ld-primary);
46
+ box-shadow: 0 0 8px var(--ld-primary-glow);
47
+ transition: width var(--ld-transition);
48
+ }
49
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step--complete::after { width: 100%; }
50
+
51
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__node {
52
+ position: relative;
53
+ z-index: 1;
54
+ display: inline-flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ width: 2rem;
58
+ height: 2rem;
59
+ border-radius: 50%;
60
+ box-sizing: border-box;
61
+ font-family: var(--ld-font-mono);
62
+ font-size: 12px;
63
+ font-weight: var(--ld-font-weight-bold);
64
+ line-height: 1;
65
+ transition: background var(--ld-transition), color var(--ld-transition), box-shadow var(--ld-transition);
66
+ }
67
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__node > svg { width: 1rem; height: 1rem; }
68
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step--complete .ld-stepper__node {
69
+ background: var(--ld-primary);
70
+ color: var(--ld-on-primary);
71
+ box-shadow: 0 0 8px var(--ld-primary-glow);
72
+ }
73
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step--current .ld-stepper__node {
74
+ background: var(--ld-surface);
75
+ border: 2px solid var(--ld-accent);
76
+ color: var(--ld-accent);
77
+ box-shadow: 0 0 12px var(--ld-accent-glow);
78
+ }
79
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step--upcoming .ld-stepper__node {
80
+ background: var(--ld-surface);
81
+ border: 1px solid var(--ld-border);
82
+ color: var(--ld-faint);
83
+ }
84
+
85
+ /* label voice: uppercase tracked mono, like every other label here */
86
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__label {
87
+ font-family: var(--ld-font-mono);
88
+ font-size: 11px;
89
+ font-weight: var(--ld-font-weight-bold);
90
+ text-transform: uppercase;
91
+ letter-spacing: 0.1em;
92
+ line-height: 1.3;
93
+ color: var(--ld-faint);
94
+ transition: color var(--ld-transition), text-shadow var(--ld-transition);
95
+ }
96
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step--complete .ld-stepper__label { color: var(--ld-on-surface); }
97
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step--current .ld-stepper__label {
98
+ color: var(--ld-accent);
99
+ text-shadow: 0 0 8px var(--ld-accent-glow);
100
+ }
101
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step--upcoming .ld-stepper__label { color: var(--ld-faint); }
102
+
103
+ @media (prefers-reduced-motion: reduce) {
104
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__step::after,
105
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__node,
106
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-stepper__label { transition: none; }
107
+ }
@@ -31,3 +31,45 @@
31
31
  :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-tabpanel {
32
32
  padding: var(--ld-space-3) 0;
33
33
  }
34
+
35
+ /* Sliding indicator: one lit bar on the tablist, pinned to the active tab by
36
+ * anchor positioning, takes over the underline. Its inline edges transition
37
+ * separately — with data-ld-dir ("forward" | "back", set by the Tabs
38
+ * component) the edge nearer the target leads and the far edge trails, so the
39
+ * light reaches for the tab; without it the bar just slides. Browsers without
40
+ * anchor positioning keep the static underline above. */
41
+ @supports (anchor-name: --ld-a) and (anchor-scope: --ld-a) {
42
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-tabs {
43
+ position: relative;
44
+ anchor-scope: --ld-tab-active;
45
+ }
46
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-tabs :is(.ld-tab--active, .ld-tab[aria-selected="true"]) {
47
+ anchor-name: --ld-tab-active;
48
+ border-bottom-color: transparent;
49
+ }
50
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-tabs:where(:has(.ld-tab--active, .ld-tab[aria-selected="true"]))::after {
51
+ content: "";
52
+ position: absolute;
53
+ position-anchor: --ld-tab-active;
54
+ inset-block-end: anchor(bottom);
55
+ inset-inline: anchor(start) anchor(end);
56
+ block-size: 2px;
57
+ background: var(--ld-primary);
58
+ box-shadow: 0 0 10px var(--ld-primary-glow), 0 0 2px var(--ld-primary);
59
+ pointer-events: none;
60
+ transition: inset-inline-start var(--ld-slide-lead), inset-inline-end var(--ld-slide-lead);
61
+ }
62
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-tabs[data-ld-dir="forward"]::after {
63
+ transition: inset-inline-start var(--ld-slide-trail), inset-inline-end var(--ld-slide-lead);
64
+ }
65
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-tabs[data-ld-dir="back"]::after {
66
+ transition: inset-inline-start var(--ld-slide-lead), inset-inline-end var(--ld-slide-trail);
67
+ }
68
+ }
69
+
70
+ @media (prefers-reduced-motion: reduce) {
71
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-tabs::after,
72
+ :where([data-ld-style="neon-butterfly"], [data-ld-style="neon-butterfly"] *).ld-tabs[data-ld-dir]::after {
73
+ transition: none;
74
+ }
75
+ }
@@ -50,17 +50,47 @@ appear only on interaction or state. Never use pure white or pure black.
50
50
  Class prefix `ld-`. Variants use BEM-ish modifiers (`ld-btn--accent`).
51
51
 
52
52
  - **Card** `.ld-card` — frosted glass panel.
53
+ - **Status card** `.ld-status-card` — dense dashboard card (from pulse's repo
54
+ card): 3px left edge takes a semantic `--<tone>`, `data-ld-attention="<tone>"`
55
+ adds an outline + glow ring, `data-ld-changed` replays a 30s tint flash.
56
+ Parts: `__watermark`, `__head`, `__prefix`, `__title`, `__meta`, `__note`,
57
+ `__rows`, `__row` (`-lead`/`-main`/`-trail`), `__footer`, `__empty`.
53
58
  - **NavLink** `.ld-link` — the switchboard link: `>` chevron turns lime on hover.
54
59
  - **Button** `.ld-btn` — `--primary`, `--accent`, `--danger` variants; `--sm`
55
60
  compact size (badge-scaled) for inline/table-row actions.
61
+ - **Hold button** `.ld-btn--hold` (+ `__ring`, `__body`, `__label`, `__hint`,
62
+ `__meter`) — hold-to-confirm: a 3px conic ring around the icon well fills
63
+ as `--ld-hold` goes 0→1. `--danger` paints it **lime** — this theme reads
64
+ the ring as an activity signal, not a warning; `--primary` lilac,
65
+ `--accent` lime. `data-ld-hold="fired"` lights the border and glows
66
+ (dropped under reduced motion; the fill stays). Window:
67
+ `--ld-hold-duration` 250 ms — terminal-quick.
68
+ - **Button group** `.ld-btn-group` (+ `--block`) — one long track with
69
+ `.ld-btn` segments lined up inside; the track owns the chrome and the
70
+ segments drop theirs. Glass track, hairline border, faint mono labels.
71
+ The pressed segment (`aria-pressed="true"` / `.ld-btn--active`) takes the
72
+ lilac edge and a lilac text glow; no hover pulse inside the track.
73
+ With one pressed segment the chrome is a single sliding pill: the edge
74
+ nearer the new segment leads, the far edge trails on a spring that squashes ~7% and settles
75
+ (`--ld-slide-lead` / `--ld-slide-trail`, direction from `data-ld-dir`).
76
+ Variants tint the label only; `--block` fills the row with equal-width
77
+ segments. Use `role="group"` + `aria-label`.
56
78
  - **Form** `.ld-input`, `.ld-textarea`, `.ld-select`, `.ld-label`, `.ld-field`,
57
79
  `.ld-checkbox`, `.ld-radio`, `.ld-switch`, `.ld-choice` — checked states glow lime.
58
- - **Badge** `.ld-badge` + semantic modifiers.
80
+ - **Badge** `.ld-badge` + semantic modifiers; `--sm` is the 9px dashboard
81
+ pill, `--pulse` the one solid amber call to action with a breathing glow.
59
82
  - **Alert** `.ld-alert` — left accent bar carries the semantic color.
60
83
  - **Dialog** `.ld-dialog` — native `<dialog>`, lilac border + glow, blurred backdrop.
61
- - **Tabs** `.ld-tabs`/`.ld-tab`/`.ld-tabpanel` — active tab underlined in lilac with text glow.
84
+ - **Tabs** `.ld-tabs`/`.ld-tab`/`.ld-tabpanel` — active tab underlined in lilac with text glow; the underline is one lit bar that stretches to the new tab and snaps back (same slide tokens).
62
85
  - **Table** `.ld-table` — lilac header rule, glass row hover.
63
86
  - **Progress** `.ld-progress`, **Spinner** `.ld-spinner` — glowing lilac indicators.
87
+ - **Stepper** `.ld-stepper` (+ `__step`, `__node`, `__label`) — milestone
88
+ rail: glowing lilac fill on a surface track, 2rem nodes, uppercase mono
89
+ labels. `__step--complete` is a solid lilac node; `--current` is the
90
+ activity signal — 2px lime border, lime icon and glow, lime label;
91
+ `--upcoming` a hairline surface node in faint. The rail is per-step
92
+ (`::before` track, `::after` fill), so the fill follows the modifiers and
93
+ `aria-current="step"` marks the active node.
64
94
  - **Muted text** `.ld-muted` — faint secondary/empty-state text;
65
95
  `color: var(--ld-faint)` only, no italic — this theme's mono/uppercase
66
96
  terminal voice never reaches for a literary flourish.