@cawalch/porchlight 0.1.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.
Files changed (47) hide show
  1. package/README.md +28 -0
  2. package/dist/porchlight.css +3765 -0
  3. package/dist/porchlight.min.css +1 -0
  4. package/package.json +59 -0
  5. package/porchlight.css +62 -0
  6. package/src/00-layer-order.css +22 -0
  7. package/src/01-reset.css +53 -0
  8. package/src/02-tokens.css +254 -0
  9. package/src/03-themes.css +79 -0
  10. package/src/04-base.css +78 -0
  11. package/src/05-layout.css +209 -0
  12. package/src/06-components/accordion.css +161 -0
  13. package/src/06-components/alert.css +102 -0
  14. package/src/06-components/avatar.css +112 -0
  15. package/src/06-components/badge.css +73 -0
  16. package/src/06-components/breadcrumb.css +111 -0
  17. package/src/06-components/button.css +180 -0
  18. package/src/06-components/card.css +186 -0
  19. package/src/06-components/chip.css +146 -0
  20. package/src/06-components/command-palette.css +201 -0
  21. package/src/06-components/data-table.css +380 -0
  22. package/src/06-components/dialog.css +148 -0
  23. package/src/06-components/drawer.css +137 -0
  24. package/src/06-components/dropdown.css +180 -0
  25. package/src/06-components/empty-state.css +85 -0
  26. package/src/06-components/field.css +125 -0
  27. package/src/06-components/file-upload.css +104 -0
  28. package/src/06-components/nav.css +185 -0
  29. package/src/06-components/pagination.css +106 -0
  30. package/src/06-components/popover-menu.css +146 -0
  31. package/src/06-components/progress.css +77 -0
  32. package/src/06-components/reveal.css +73 -0
  33. package/src/06-components/scroll-progress.css +73 -0
  34. package/src/06-components/segmented.css +113 -0
  35. package/src/06-components/skeleton.css +73 -0
  36. package/src/06-components/stat.css +107 -0
  37. package/src/06-components/stepper.css +172 -0
  38. package/src/06-components/switch.css +138 -0
  39. package/src/06-components/tabs.css +164 -0
  40. package/src/06-components/tag-input.css +77 -0
  41. package/src/06-components/textarea-auto.css +77 -0
  42. package/src/06-components/timeline.css +129 -0
  43. package/src/06-components/toast.css +175 -0
  44. package/src/06-components/toolbar.css +87 -0
  45. package/src/06-components/tooltip.css +104 -0
  46. package/src/07-utilities.css +77 -0
  47. package/src/08-enhancements.css +129 -0
@@ -0,0 +1,180 @@
1
+ /*
2
+ * Porchlight - dropdown component
3
+ * ===========================================================================
4
+ * A custom select replacement using the Popover API + anchor positioning.
5
+ * Unlike native <select>, this supports icons, descriptions, multi-select,
6
+ * and fully custom option rendering.
7
+ *
8
+ * The trigger uses popovertarget to open declaratively. The menu is anchored
9
+ * to the trigger. Options are <button> or <a> elements for keyboard access.
10
+ *
11
+ * Structure:
12
+ * <div class="c-dropdown">
13
+ * <button class="c-dropdown__trigger" popovertarget="dd-1">
14
+ * Choose... <svg class="c-dropdown__chevron">...</svg>
15
+ * </button>
16
+ * <div popover class="c-dropdown__menu" id="dd-1">
17
+ * <button class="c-dropdown__option" aria-selected="true">Option A</button>
18
+ * <button class="c-dropdown__option">Option B</button>
19
+ * </div>
20
+ * </div>
21
+ */
22
+ @layer porchlight.components {
23
+ @scope (.c-dropdown) {
24
+ :scope {
25
+ --c-dropdown-anchor: --pl-dropdown-anchor;
26
+ --c-dropdown-min-inline: 12rem;
27
+ --c-dropdown-max-block: 20rem;
28
+
29
+ display: inline-block;
30
+ position: relative;
31
+ }
32
+
33
+ .c-dropdown__trigger {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ gap: var(--pl-space-2);
37
+ inline-size: 100%;
38
+ min-block-size: var(--pl-control-block-size);
39
+ padding-inline: var(--pl-control-padding-inline);
40
+ border: 1px solid var(--pl-color-border);
41
+ border-radius: var(--pl-control-radius);
42
+ background: var(--pl-color-surface);
43
+ color: var(--pl-color-text);
44
+ font: inherit;
45
+ font-size: var(--pl-text-sm);
46
+ text-align: start;
47
+ cursor: pointer;
48
+ anchor-name: var(--c-dropdown-anchor);
49
+ transition: border-color var(--pl-duration-1) var(--pl-ease-standard);
50
+ }
51
+
52
+ .c-dropdown__trigger:hover {
53
+ border-color: var(--pl-color-accent);
54
+ }
55
+
56
+ .c-dropdown__trigger:focus-visible {
57
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
58
+ outline-offset: var(--pl-focus-offset);
59
+ }
60
+
61
+ .c-dropdown__chevron {
62
+ inline-size: 1rem;
63
+ block-size: 1rem;
64
+ margin-inline-start: auto;
65
+ color: var(--pl-color-text-muted);
66
+ flex-shrink: 0;
67
+ transition: rotate var(--pl-duration-2) var(--pl-ease-standard);
68
+ }
69
+
70
+ .c-dropdown__trigger:has(~ .c-dropdown__menu:popover-open)
71
+ .c-dropdown__chevron {
72
+ rotate: 180deg;
73
+ }
74
+
75
+ .c-dropdown__menu {
76
+ margin: 0;
77
+ padding: var(--pl-space-2);
78
+ min-inline-size: var(--c-dropdown-min-inline);
79
+ max-block-size: var(--c-dropdown-max-block);
80
+ overflow-y: auto;
81
+
82
+ /* Floating surface: softened border + Liquid Glass. */
83
+ border: 1px solid color-mix(
84
+ in oklab,
85
+ var(--pl-color-border),
86
+ transparent 30%
87
+ );
88
+ border-radius: var(--pl-radius-xl);
89
+ background: light-dark(
90
+ oklch(100% 0 0deg / 96%),
91
+ oklch(17% 0.04 250deg / 94%)
92
+ );
93
+ backdrop-filter: blur(var(--pl-backdrop-blur)) saturate(var(--pl-backdrop-saturate));
94
+ -webkit-backdrop-filter: blur(var(--pl-backdrop-blur)) saturate(var(--pl-backdrop-saturate));
95
+ box-shadow: var(--pl-shadow-3);
96
+
97
+ /* Asymmetric motion. */
98
+ opacity: 0;
99
+ transform: translateY(-4px);
100
+ transition:
101
+ opacity var(--pl-duration-enter) var(--pl-ease-decelerate),
102
+ transform var(--pl-duration-enter) var(--pl-ease-decelerate),
103
+ overlay var(--pl-duration-exit) var(--pl-ease-accelerate) allow-discrete,
104
+ display var(--pl-duration-exit) var(--pl-ease-accelerate) allow-discrete;
105
+ }
106
+
107
+ @supports (position-anchor: --x) {
108
+ .c-dropdown__menu {
109
+ inset: auto;
110
+ position: absolute;
111
+ position-anchor: var(--c-dropdown-anchor);
112
+ position-area: block-end span-inline-end;
113
+ position-try-fallbacks: flip-block;
114
+ margin-block-start: var(--pl-space-1);
115
+ }
116
+ }
117
+
118
+ .c-dropdown__menu:popover-open {
119
+ opacity: 1;
120
+ transform: translateY(0);
121
+ }
122
+
123
+ @starting-style {
124
+ .c-dropdown__menu:popover-open {
125
+ opacity: 0;
126
+ transform: translateY(-4px);
127
+ }
128
+ }
129
+ }
130
+
131
+ /* Dropdown options - OUTSIDE @scope (live-state pseudos). */
132
+ :where(.c-dropdown__menu) > :is(a, button, [role="option"]) {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: var(--pl-space-2);
136
+ inline-size: 100%;
137
+ min-block-size: 2.25rem;
138
+ padding-inline: var(--pl-space-3);
139
+ border: 0;
140
+ border-radius: var(--pl-radius-sm);
141
+ background: transparent;
142
+ color: inherit;
143
+ font: inherit;
144
+ font-size: var(--pl-text-sm);
145
+ text-align: start;
146
+ text-decoration: none;
147
+ cursor: pointer;
148
+ }
149
+
150
+ :where(.c-dropdown__menu)
151
+ > :is(a, button, [role="option"]):is(:hover, :focus-visible) {
152
+ background: var(--pl-color-surface-2);
153
+ }
154
+
155
+ :where(.c-dropdown__menu) > :is(a, button, [role="option"]):focus-visible {
156
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
157
+ outline-offset: calc(var(--pl-focus-offset) * -1);
158
+ }
159
+
160
+ /* Selected option: accent color + subtle tint background for clarity. */
161
+ :where(.c-dropdown__menu) > [aria-selected="true"] {
162
+ color: var(--pl-color-accent);
163
+ font-weight: var(--pl-font-weight-semibold);
164
+ background: color-mix(in oklab, var(--pl-color-accent), transparent 92%);
165
+ }
166
+
167
+ @media (forced-colors: active) {
168
+ :where(.c-dropdown__trigger) {
169
+ border-color: ButtonBorder;
170
+ }
171
+
172
+ :where(.c-dropdown__menu) {
173
+ border-color: ButtonBorder;
174
+ }
175
+
176
+ :where(.c-dropdown__menu) > :is(a, button):focus-visible {
177
+ outline-color: Highlight;
178
+ }
179
+ }
180
+ }
@@ -0,0 +1,85 @@
1
+ /*
2
+ * Porchlight - empty state component
3
+ * ===========================================================================
4
+ * The "no data yet" placeholder - what users see in a table, list, or chart
5
+ * before there is anything to show. A centered illustration or icon, a title,
6
+ * a description, and an optional call-to-action. The psychological anchor that
7
+ * turns a blank screen into an invitation.
8
+ *
9
+ * Structure: .c-empty > .c-empty__media (icon/illustration) + .c-empty__title
10
+ * + .c-empty__description + .c-empty__actions. All slots except the title are
11
+ * optional. Token-driven; centered via grid for robust vertical centering.
12
+ *
13
+ * Tone variants via [data-tone]: default (neutral), "danger" (error states),
14
+ * "success" (completed/empty-by-design). The media gets the tone color.
15
+ */
16
+ @layer porchlight.components {
17
+ @scope (.c-empty) {
18
+ :scope {
19
+ --c-empty-pad: var(--pl-space-8);
20
+ --c-empty-tone: var(--pl-color-text-muted);
21
+
22
+ display: grid;
23
+ justify-items: center;
24
+ gap: var(--pl-space-3);
25
+ padding: var(--c-empty-pad);
26
+ text-align: center;
27
+ color: var(--pl-color-text-muted);
28
+ }
29
+
30
+ .c-empty__media {
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ inline-size: var(--c-empty-media-size, 3rem);
35
+ block-size: var(--c-empty-media-size, 3rem);
36
+ color: var(--c-empty-tone);
37
+ opacity: 0.6;
38
+ }
39
+
40
+ .c-empty__media svg,
41
+ .c-empty__media img {
42
+ inline-size: 100%;
43
+ block-size: 100%;
44
+ }
45
+
46
+ .c-empty__title {
47
+ margin: 0;
48
+ font-size: var(--pl-text-lg);
49
+ font-weight: var(--pl-font-weight-semibold);
50
+ color: var(--pl-color-text);
51
+ text-wrap: balance;
52
+ }
53
+
54
+ .c-empty__description {
55
+ max-inline-size: 36ch;
56
+ margin: 0;
57
+ font-size: var(--pl-text-sm);
58
+ text-wrap: pretty;
59
+ }
60
+
61
+ .c-empty__actions {
62
+ display: flex;
63
+ flex-wrap: wrap;
64
+ align-items: center;
65
+ justify-content: center;
66
+ gap: var(--pl-space-2);
67
+ margin-block-start: var(--pl-space-2);
68
+ }
69
+
70
+ /* Tone variants - recolor the media. */
71
+ :scope[data-tone="danger"] {
72
+ --c-empty-tone: var(--pl-color-danger);
73
+ }
74
+
75
+ :scope[data-tone="success"] {
76
+ --c-empty-tone: var(--pl-color-success);
77
+ }
78
+ }
79
+
80
+ @media (forced-colors: active) {
81
+ :where(.c-empty__media) {
82
+ color: CanvasText;
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,125 @@
1
+ /*
2
+ * Porchlight - field component
3
+ * ===========================================================================
4
+ * A labeled form control: label, native input, and hint. Wraps native
5
+ * <input>/<select>/<textarea> so we inherit all their behavior (constraint
6
+ * validation, autofill, IME, autofill). No JS, no custom widgets.
7
+ *
8
+ * State is driven entirely by native pseudos: :focus-visible, :user-invalid,
9
+ * :disabled. Focus draws a token-colored border AND a 2px ring (box-shadow);
10
+ * invalid flips the border to danger and the hint to danger-text. State rules
11
+ * live OUTSIDE @scope - :focus-visible / :user-invalid inside @scope (.c-field)
12
+ * did not reliably apply to the focused control. They stay in the same layer,
13
+ * with :where() for low specificity.
14
+ *
15
+ * Token-driven via --c-field-* aliases. Density comes from --pl-control-*
16
+ * (set [data-density] on an ancestor).
17
+ *
18
+ * Modern CSS: :user-invalid (Baseline 2024) replaces
19
+ * :invalid:not(:placeholder-shown). accent-color themes native checkbox/radio/color.
20
+ */
21
+ @layer porchlight.components {
22
+ @scope (.c-field) {
23
+ :scope {
24
+ --c-field-border: var(--pl-color-border);
25
+ --c-field-bg: var(--pl-color-surface);
26
+ --c-field-fg: var(--pl-color-text);
27
+
28
+ display: grid;
29
+ gap: var(--pl-space-2);
30
+ color: var(--c-field-fg);
31
+ }
32
+
33
+ .c-field__label {
34
+ font-size: var(--pl-text-sm);
35
+ font-weight: var(--pl-font-weight-semibold);
36
+
37
+ /* Logical "for" association: clicking the label focuses the input it wraps. */
38
+ }
39
+
40
+ .c-field__control {
41
+ /* appearance: none - we fully restyle the control; the UA's native
42
+ input chrome (including its :focus border ring) would otherwise win
43
+ over our border-color/box-shadow and freeze the border at the UA gray. */
44
+ appearance: none;
45
+ inline-size: 100%;
46
+ min-block-size: var(--pl-control-block-size);
47
+
48
+ /* Vertical padding is token-derived so the control height stays within
49
+ --pl-control-block-size when combined with line-height + border.
50
+ padding-block 0.375rem × 2 + border 1px × 2 + line-height 1.5 × 1rem =
51
+ 12px + 2px + 24px = 38px < 40px (min-block-size). */
52
+ padding-block: var(--pl-space-1);
53
+ padding-inline: var(--pl-control-padding-inline);
54
+ border: var(--pl-control-border-width) solid var(--c-field-border);
55
+ border-radius: var(--pl-control-radius);
56
+ background: var(--c-field-bg);
57
+ color: var(--c-field-fg);
58
+ transition:
59
+ border-color var(--pl-duration-1) var(--pl-ease-standard),
60
+ box-shadow var(--pl-duration-1) var(--pl-ease-standard);
61
+ }
62
+
63
+ select.c-field__control {
64
+ padding-inline-end: var(--pl-space-8);
65
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
66
+ background-repeat: no-repeat;
67
+ background-position: right var(--pl-space-3) center;
68
+ background-size: 1rem;
69
+ }
70
+
71
+ .c-field__control::placeholder {
72
+ color: var(--pl-color-text-muted);
73
+ }
74
+
75
+ .c-field__hint {
76
+ color: var(--pl-color-text-muted);
77
+ font-size: var(--pl-text-sm);
78
+ }
79
+ }
80
+
81
+ /* State rules OUTSIDE @scope. Live-state pseudos (:focus-visible,
82
+ :user-invalid) inside @scope (.c-field) do not reliably apply to the
83
+ focused control - @scope evaluates subtree membership but state pseudos
84
+ need direct element matching. Keeping them in the same layer, just
85
+ unscoped, with a :where() prefix for low specificity. */
86
+
87
+ /* Focus indicator: a single crisp 2px ring in the focus color, plus a
88
+ genuinely BLURRED outer glow. The border is NOT recolored - recoloring it
89
+ produced a “double blue stroke” (solid recolored border + a translucent
90
+ ring stacked beside it). The ring is the focus indicator; the border
91
+ stays as the neutral field edge. (The previous `0 0 0 4px` had no blur,
92
+ so it was a hard translucent ring, not the intended glow.) */
93
+ :where(.c-field) .c-field__control:focus-visible {
94
+ outline: none;
95
+ border-color: var(--pl-focus-color);
96
+ box-shadow: 0 0 0 4px
97
+ color-mix(in oklab, var(--pl-focus-color), transparent var(--pl-focus-glow-opacity));
98
+ }
99
+
100
+ /* Invalid: same single-ring + glow treatment, in danger. The hint still
101
+ flips to danger-text via :has() on the wrapper (below). */
102
+ :where(.c-field) .c-field__control:user-invalid {
103
+ border-color: var(--pl-color-danger);
104
+ box-shadow: 0 0 0 4px
105
+ color-mix(in oklab, var(--pl-color-danger), transparent var(--pl-focus-glow-opacity));
106
+ }
107
+
108
+ :where(.c-field):has(.c-field__control:user-invalid) .c-field__hint {
109
+ color: var(--pl-color-danger-text);
110
+ }
111
+
112
+ :where(.c-field):has(.c-field__control:disabled) {
113
+ opacity: var(--pl-opacity-disabled);
114
+ }
115
+
116
+ :where(.c-field):has(.c-field__control:disabled) .c-field__control {
117
+ cursor: not-allowed;
118
+ }
119
+
120
+ @media (forced-colors: active) {
121
+ :where(.c-field) .c-field__control {
122
+ border-color: ButtonBorder;
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,104 @@
1
+ /*
2
+ * Porchlight - file upload component
3
+ * ===========================================================================
4
+ * A drag-and-drop file upload zone. The native <input type="file"> is
5
+ * visually hidden; the zone is the click target. Uses :has() for the
6
+ * drag-over state (set [data-dragging] via JS) and disabled state.
7
+ *
8
+ * Structure:
9
+ * <label class="c-file-upload">
10
+ * <input type="file" class="c-file-upload__input" multiple />
11
+ * <span class="c-file-upload__zone">
12
+ * <svg class="c-file-upload__icon">...</svg>
13
+ * <span class="c-file-upload__text">
14
+ * <strong>Drag files here</strong> or click to browse
15
+ * </span>
16
+ * </span>
17
+ * </label>
18
+ */
19
+ @layer porchlight.components {
20
+ @scope (.c-file-upload) {
21
+ :scope {
22
+ --c-upload-pad: var(--pl-space-6);
23
+ --c-upload-gap: var(--pl-space-3);
24
+ --c-upload-border: 2px dashed var(--pl-color-border);
25
+ --c-upload-radius: var(--pl-radius-lg);
26
+
27
+ display: block;
28
+ cursor: pointer;
29
+ }
30
+
31
+ /* Visually hide the native input. */
32
+ .c-file-upload__input {
33
+ position: absolute;
34
+ inline-size: 1px;
35
+ block-size: 1px;
36
+ padding: 0;
37
+ margin: -1px;
38
+ overflow: hidden;
39
+ clip: rect(0, 0, 0, 0);
40
+ white-space: nowrap;
41
+ border: 0;
42
+ }
43
+
44
+ .c-file-upload__zone {
45
+ display: flex;
46
+ flex-direction: column;
47
+ align-items: center;
48
+ justify-content: center;
49
+ gap: var(--c-upload-gap);
50
+ padding: var(--c-upload-pad);
51
+ border: var(--c-upload-border);
52
+ border-radius: var(--c-upload-radius);
53
+ background: var(--pl-color-surface-2);
54
+ text-align: center;
55
+ transition:
56
+ border-color var(--pl-duration-2) var(--pl-ease-standard),
57
+ background-color var(--pl-duration-2) var(--pl-ease-standard);
58
+ }
59
+
60
+ /* Hover/drag-over: accent border + subtle tint. */
61
+ :scope:hover .c-file-upload__zone,
62
+ :scope[data-dragging] .c-file-upload__zone {
63
+ border-color: var(--pl-color-accent);
64
+ background: color-mix(in oklab, var(--pl-color-accent), transparent 92%);
65
+ }
66
+
67
+ /* Focus ring (when the hidden input is focused). */
68
+ :scope:has(input:focus-visible) .c-file-upload__zone {
69
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
70
+ outline-offset: var(--pl-focus-offset);
71
+ }
72
+
73
+ /* Disabled. */
74
+ :scope:has(input:disabled) {
75
+ cursor: not-allowed;
76
+
77
+ & .c-file-upload__zone {
78
+ opacity: var(--pl-opacity-disabled);
79
+ }
80
+ }
81
+
82
+ .c-file-upload__icon {
83
+ inline-size: 2rem;
84
+ block-size: 2rem;
85
+ color: var(--pl-color-text-muted);
86
+ }
87
+
88
+ .c-file-upload__text {
89
+ font-size: var(--pl-text-sm);
90
+ color: var(--pl-color-text-muted);
91
+ }
92
+
93
+ .c-file-upload__text strong {
94
+ color: var(--pl-color-text);
95
+ }
96
+ }
97
+
98
+ @media (forced-colors: active) {
99
+ :where(.c-file-upload__zone) {
100
+ border-color: ButtonBorder;
101
+ background: Canvas;
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,185 @@
1
+ /*
2
+ * Porchlight - nav (sidebar navigation) component
3
+ * ===========================================================================
4
+ * A vertical navigation list for sidebars. Icon + label items with active
5
+ * state via aria-current="page". Collapsible sub-sections via native
6
+ * <details> for zero-JS expand/collapse. Designed to compose with
7
+ * .l-app-shell__sidebar.
8
+ *
9
+ * Structure:
10
+ * <nav class="c-nav" aria-label="Main">
11
+ * <a class="c-nav__item" aria-current="page">
12
+ * <svg class="c-nav__icon">...</svg>
13
+ * <span class="c-nav__label">Dashboard</span>
14
+ * </a>
15
+ * <details class="c-nav__section">
16
+ * <summary class="c-nav__section-trigger">
17
+ * <svg class="c-nav__icon">...</svg>
18
+ * <span>Projects</span>
19
+ * <svg class="c-nav__chevron">...</svg>
20
+ * </summary>
21
+ * <a class="c-nav__item c-nav__item--child">Active</a>
22
+ * <a class="c-nav__item c-nav__item--child">Archived</a>
23
+ * </details>
24
+ * </nav>
25
+ *
26
+ * The icon rail variant: when the sidebar is collapsed (icon-only mode),
27
+ * [data-variant="icons"] hides labels and centers icons.
28
+ */
29
+ @layer porchlight.components {
30
+ @scope (.c-nav) {
31
+ :scope {
32
+ --c-nav-gap: var(--pl-space-1);
33
+ --c-nav-pad: var(--pl-space-2);
34
+
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: var(--c-nav-gap);
38
+ padding: var(--c-nav-pad);
39
+ }
40
+
41
+ .c-nav__item {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: var(--pl-space-3);
45
+ padding: var(--pl-space-2) var(--pl-space-3);
46
+ border-radius: var(--pl-radius-md);
47
+ color: var(--pl-color-text-muted);
48
+ font-size: var(--pl-text-sm);
49
+ font-weight: var(--pl-font-weight-medium);
50
+ text-decoration: none;
51
+ cursor: pointer;
52
+ transition:
53
+ background-color var(--pl-duration-1) var(--pl-ease-standard),
54
+ color var(--pl-duration-1) var(--pl-ease-standard),
55
+ box-shadow var(--pl-duration-1) var(--pl-ease-standard);
56
+ }
57
+
58
+ .c-nav__item:hover {
59
+ background: var(--pl-color-surface-2);
60
+ color: var(--pl-color-text);
61
+ }
62
+
63
+ .c-nav__item:focus-visible {
64
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
65
+ outline-offset: var(--pl-focus-offset);
66
+ }
67
+
68
+ /* Active item - left-bar accent pattern: a 3px inset shadow on the
69
+ leading edge reads as a rail marker, strongly signalling "you are here".
70
+ Asymmetric border-radius (0 on start side) anchors the bar to the edge. */
71
+ .c-nav__item[aria-current="page"],
72
+ .c-nav__item[aria-current="true"] {
73
+ background: color-mix(in oklab, var(--pl-color-accent), transparent 90%);
74
+ color: var(--pl-color-accent);
75
+ font-weight: var(--pl-font-weight-semibold);
76
+ border-radius: 0 var(--pl-radius-md) var(--pl-radius-md) 0;
77
+ box-shadow: inset var(--pl-accent-bar-width) 0 0 var(--pl-color-accent);
78
+ }
79
+
80
+ /* Nested item (inside a section). */
81
+ .c-nav__item--child {
82
+ padding-inline-start: calc(
83
+ var(--pl-space-3) + 1.25rem + var(--pl-space-3)
84
+ );
85
+ font-size: var(--pl-text-sm);
86
+ }
87
+
88
+ .c-nav__icon {
89
+ inline-size: 1.25rem;
90
+ block-size: 1.25rem;
91
+ flex-shrink: 0;
92
+ color: inherit;
93
+ }
94
+
95
+ .c-nav__label {
96
+ flex: 1;
97
+ min-inline-size: 0;
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
+ white-space: nowrap;
101
+ }
102
+
103
+ /* Badge in a nav item (e.g. unread count). */
104
+ .c-nav__badge {
105
+ margin-inline-start: auto;
106
+ }
107
+
108
+ /* Collapsible section. */
109
+ .c-nav__section {
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: var(--pl-space-1);
113
+ }
114
+
115
+ .c-nav__section-trigger {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: var(--pl-space-3);
119
+ padding: var(--pl-space-2) var(--pl-space-3);
120
+ border-radius: var(--pl-radius-md);
121
+ color: var(--pl-color-text-muted);
122
+ font-size: var(--pl-text-sm);
123
+ font-weight: var(--pl-font-weight-medium);
124
+ cursor: pointer;
125
+ user-select: none;
126
+ list-style: none;
127
+ transition: all var(--pl-duration-1) var(--pl-ease-standard);
128
+
129
+ &::-webkit-details-marker {
130
+ display: none;
131
+ }
132
+
133
+ &::marker {
134
+ content: "";
135
+ }
136
+ }
137
+
138
+ .c-nav__section-trigger:hover {
139
+ background: var(--pl-color-surface-2);
140
+ color: var(--pl-color-text);
141
+ }
142
+
143
+ .c-nav__section-trigger:focus-visible {
144
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
145
+ outline-offset: var(--pl-focus-offset);
146
+ }
147
+
148
+ .c-nav__chevron {
149
+ margin-inline-start: auto;
150
+ inline-size: 1rem;
151
+ block-size: 1rem;
152
+ flex-shrink: 0;
153
+ color: var(--pl-color-text-muted);
154
+ transition: rotate var(--pl-duration-2) var(--pl-ease-standard);
155
+ }
156
+
157
+ .c-nav__section[open] .c-nav__chevron {
158
+ rotate: 90deg;
159
+ }
160
+
161
+ /* Icons-only variant (collapsed sidebar). */
162
+ :scope[data-variant="icons"] .c-nav__label,
163
+ :scope[data-variant="icons"] .c-nav__chevron,
164
+ :scope[data-variant="icons"] .c-nav__badge {
165
+ display: none;
166
+ }
167
+
168
+ :scope[data-variant="icons"] .c-nav__item,
169
+ :scope[data-variant="icons"] .c-nav__section-trigger {
170
+ justify-content: center;
171
+ padding: var(--pl-space-2);
172
+ }
173
+ }
174
+
175
+ @media (forced-colors: active) {
176
+ :where(.c-nav__item[aria-current="page"]) {
177
+ background: Highlight;
178
+ color: HighlightText;
179
+ }
180
+
181
+ :where(.c-nav__item:focus-visible) {
182
+ outline-color: Highlight;
183
+ }
184
+ }
185
+ }