@leftium/nimble.css 0.11.0 → 0.13.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.
package/src/_forms.scss CHANGED
@@ -13,14 +13,15 @@
13
13
  select, textarea) {
14
14
  --_input-bg: color-mix(in oklch, var(#{$prefix}surface-1), var(#{$prefix}surface-2) 20%);
15
15
 
16
- padding: 0.5em 0.75em;
16
+ padding: 0.5em 0.75em; // 0.5em: OP ~size-2; 0.75em: no OP match
17
+ // OP normalize: padding-block: --size-1, padding-inline: --size-2
17
18
  min-height: calc(1em * 1.5 + 1em + 2px); // line-height + vertical padding + border
18
19
  background-color: var(--_input-bg);
19
20
  border: 1px solid var(#{$prefix}border);
20
21
  border-radius: var(#{$prefix}radius);
21
22
  color: var(#{$prefix}text);
22
23
  font: inherit;
23
- font-size: 1rem; // >=16px prevents iOS Safari auto-zoom on focus
24
+ font-size: 1rem; // OP --size-3; >=16px prevents iOS Safari auto-zoom on focus
24
25
  transition: border-color 0.2s, box-shadow 0.2s;
25
26
  }
26
27
 
@@ -31,6 +32,12 @@
31
32
  margin-bottom: var(#{$prefix}spacing);
32
33
  }
33
34
 
35
+ // When an input is the last visible child of a block that already has
36
+ // margin-bottom (e.g. <p>), kill the input's margin to avoid doubling.
37
+ :where(p, div) > :where(input, select, textarea):nth-last-child(1 of :not(datalist, script, style)) {
38
+ margin-bottom: 0;
39
+ }
40
+
34
41
  // ----- Focus -----
35
42
 
36
43
  :where(input, select, textarea):focus-visible {
@@ -45,7 +52,7 @@
45
52
  display: block;
46
53
  margin-top: calc(var(#{$prefix}spacing) * -0.75); // pull up under the input
47
54
  margin-bottom: var(#{$prefix}spacing);
48
- font-size: 0.875em;
55
+ font-size: 0.875em; // no OP match (7/8)
49
56
  color: color-mix(in oklch, var(#{$prefix}text), transparent 40%);
50
57
  }
51
58
 
@@ -72,17 +79,17 @@
72
79
 
73
80
  :where(label) {
74
81
  display: block;
75
- margin-bottom: 0.25em;
82
+ margin-bottom: 0.25em; // OP ~size-1 (em-relative)
76
83
  }
77
84
 
78
85
  :where(label:has(+ input, + select, + textarea)) {
79
- font-weight: 600;
86
+ font-weight: 600; // OP --font-weight-6
80
87
  }
81
88
 
82
89
  // file and range are short/inline controls — increase the label gap to match
83
90
  // the visual weight of the spacing above and beside them.
84
91
  :where(label:has(+ [type="file"], + [type="range"])) {
85
- margin-bottom: 0.5em;
92
+ margin-bottom: 0.5em; // OP ~size-2 (em-relative)
86
93
  }
87
94
 
88
95
  // ----- Fieldset -----
@@ -95,9 +102,16 @@
95
102
  max-width: 100%;
96
103
  }
97
104
 
105
+ // Kill last-child bottom margin so it doesn't stack with fieldset padding.
106
+ // Uses :nth-last-child(of ...) to skip invisible elements (datalist, script).
107
+ :where(fieldset) > :nth-last-child(1 of :not(datalist, script, style)),
108
+ :where(fieldset) > :nth-last-child(1 of :not(datalist, script, style)) > :nth-last-child(1 of :not(datalist, script, style)) {
109
+ margin-bottom: 0;
110
+ }
111
+
98
112
  :where(legend) {
99
- font-weight: 600;
100
- padding-inline: 0.25em;
113
+ font-weight: 600; // OP --font-weight-6
114
+ padding-inline: 0.25em; // OP ~size-1 (em-relative)
101
115
  }
102
116
 
103
117
  // ----- Checkbox & Radio -----
@@ -105,16 +119,16 @@
105
119
 
106
120
  :where([type="checkbox"], [type="radio"]) {
107
121
  accent-color: var(#{$prefix}primary);
108
- width: 1.125em;
109
- height: 1.125em;
122
+ width: 1.125em; // no OP match (OP normalize: --size-3 = 1rem)
123
+ height: 1.125em; // no OP match (OP normalize: --size-3 = 1rem)
110
124
  margin: 0; // Remove browser default margin so flex labels align flush left
111
125
  }
112
126
 
113
127
  :where(label:has([type="checkbox"], [type="radio"])) {
114
128
  display: flex;
115
129
  align-items: center;
116
- gap: 0.35em;
117
- margin-bottom: 0.5em;
130
+ gap: 0.35em; // no OP match (near ~size-1)
131
+ margin-bottom: 0.5em; // OP ~size-2 (em-relative)
118
132
  }
119
133
 
120
134
  // Last checkbox/radio label in a group drops its bottom margin
@@ -126,8 +140,8 @@
126
140
  // ----- Search (pill shape) -----
127
141
 
128
142
  :where([type="search"]) {
129
- border-radius: 5rem;
130
- padding-inline: 1.25em;
143
+ border-radius: 5rem; // OP --size-10 (pill)
144
+ padding-inline: 1.25em; // OP ~size-4 (em-relative)
131
145
  }
132
146
 
133
147
  // ----- Range -----
@@ -157,9 +171,9 @@
157
171
  }
158
172
 
159
173
  :where([type="file"])::file-selector-button {
160
- padding: 0.5em 1em;
161
- margin-right: 0.75em;
162
- margin-inline-end: 0.75em;
174
+ padding: 0.5em 1em; // OP ~size-2 ~size-3 (em-relative)
175
+ margin-right: 0.75em; // no OP match (between ~size-2/~size-3)
176
+ margin-inline-end: 0.75em; // no OP match (between ~size-2/~size-3)
163
177
  background-color: var(#{$prefix}primary);
164
178
  color: var(#{$prefix}primary-contrast);
165
179
  border: 1px solid var(#{$prefix}primary);
@@ -178,7 +192,7 @@
178
192
  // Natural width so picker icon stays near content (excluded from full-width rule above).
179
193
  // appearance: none is applied outside @layer (see below) to fix iOS Safari sizing.
180
194
  :where([type="date"], [type="month"], [type="week"], [type="time"], [type="datetime-local"]) {
181
- min-width: 10em; // Prevent collapse when empty (e.g. time with no value)
195
+ min-width: 10em; // no OP match; prevent collapse when empty (e.g. time with no value)
182
196
  margin-bottom: var(#{$prefix}spacing);
183
197
  }
184
198
 
@@ -188,7 +202,7 @@
188
202
  --_color-size: calc(1em * 1.5 + 1em + 2px); // Match text input height
189
203
  margin-bottom: var(#{$prefix}spacing);
190
204
 
191
- --_color-pad: 0.25em;
205
+ --_color-pad: 0.25em; // OP ~size-1 (em-relative)
192
206
 
193
207
  height: var(--_color-size);
194
208
  width: calc((var(--_color-size) - 2 * var(--_color-pad)) * 1.618 + 2 * var(--_color-pad)); // Golden ratio inner swatch
@@ -216,14 +230,14 @@
216
230
  :where(label:has([type="checkbox"][role="switch"])) {
217
231
  display: flex;
218
232
  align-items: center;
219
- gap: 0.5em;
233
+ gap: 0.5em; // OP ~size-2 (em-relative)
220
234
  }
221
235
 
222
236
  :where([type="checkbox"][role="switch"]) {
223
237
  appearance: none;
224
- width: 2.5em;
225
- height: 1.25em;
226
- border-radius: 1em;
238
+ width: 2.5em; // no OP match (custom switch width)
239
+ height: 1.25em; // OP ~size-4 (em-relative)
240
+ border-radius: 1em; // OP ~size-3 (pill, em-relative)
227
241
  background-color: var(#{$prefix}border);
228
242
  position: relative;
229
243
  cursor: pointer;
@@ -1,7 +1,7 @@
1
1
  // ==========================================================================
2
2
  // nimble.css — Grid Column Assignment
3
- // Assigns body's direct children to the centered content column (column 3).
4
- // 5-column grid: 1fr | shadow-gap | content | shadow-gap | 1fr
3
+ // Assigns body's direct children to the centered content column (column 2).
4
+ // 3-column grid: 1fr | content | 1fr
5
5
  // Global (not scoped) so the body grid layout always applies.
6
6
  // ==========================================================================
7
7
 
@@ -11,40 +11,41 @@
11
11
  @layer nimble.base {
12
12
 
13
13
  body > * {
14
- grid-column: 3;
14
+ grid-column: 2;
15
15
  min-width: 0; // Allow grid children to shrink below intrinsic content width
16
16
  }
17
17
 
18
+
18
19
  // Fix for frameworks (SvelteKit, etc.) that insert a
19
20
  // <div style="display: contents"> wrapper between <body> and content.
20
21
  // display: contents removes the element from the box tree for layout,
21
22
  // but NOT for CSS selector matching — so body > * misses the actual
22
23
  // content elements. This rule mirrors the body > * rule above.
23
24
  body > [style*='display: contents'] > * {
24
- grid-column: 3;
25
+ grid-column: 2;
25
26
  min-width: 0;
26
27
  }
27
28
 
28
29
  // Content shadow: absolutely-positioned pseudo spanning full body height.
29
- // Width matches grid columns 2–4 (gap + content + gap) via the same
30
- // min() expression used in the grid definition, plus the two gap columns.
31
- // Hidden on small viewports where content is edge-to-edge.
32
- @media (min-width: #{$breakpoint-phone}) {
33
- body::before {
34
- content: '';
35
- position: absolute;
36
- top: 0;
37
- bottom: 0;
38
- left: 50%;
39
- transform: translateX(-50%);
40
- width: min(
41
- #{string.unquote('calc(var(#{$prefix}content-width) + 2 * var(#{$prefix}content-shadow-gap))')},
42
- #{string.unquote('calc(100% - 2 * var(#{$prefix}spacing))')}
43
- );
44
- box-shadow: var(#{$prefix}content-shadow);
45
- pointer-events: none;
46
- z-index: -1;
47
- }
30
+ // Width = content column + shadow gap on each side, creating a visual
31
+ // separation between the shadow edge and the content.
32
+ // The clamp() formula handles visibility: shadow shows at full width when
33
+ // there's at least 2*gap of space on each side between shadow edge and
34
+ // viewport edge, otherwise snaps to 0. No breakpoint needed.
35
+ body::before {
36
+ content: '';
37
+ position: absolute;
38
+ top: 0;
39
+ bottom: 0;
40
+ left: 50%;
41
+ transform: translateX(-50%);
42
+ width: clamp(0px,
43
+ #{string.unquote('calc((100% - 2 * var(#{$prefix}spacing) - var(#{$prefix}content-width) - 4 * var(#{$prefix}content-shadow-gap)) * 9999)')},
44
+ #{string.unquote('calc(var(#{$prefix}content-width) + 2 * var(#{$prefix}content-shadow-gap))')}
45
+ );
46
+ box-shadow: var(#{$prefix}content-shadow);
47
+ pointer-events: none;
48
+ z-index: -1;
48
49
  }
49
50
 
50
51
  }
@@ -2,8 +2,15 @@
2
2
  // nimble.css — Layout Utilities
3
3
  // Global layout classes that interact with the body grid.
4
4
  // Always available, including on .no-nimble elements.
5
+ //
6
+ // Breakout hierarchy (narrow → wide):
7
+ // (default) — content column (--nc-content-width)
8
+ // .bleed-edge — content + shadow gap on each side (shadow boundary)
9
+ // .bleed-wide — up to $wide-width (1200px), centered
10
+ // .bleed-full — full viewport width
5
11
  // ==========================================================================
6
12
 
13
+ @use 'sass:string';
7
14
  @use 'config' as *;
8
15
 
9
16
  @if $enable-utilities {
@@ -24,17 +31,27 @@
24
31
  padding-inline: var(#{$prefix}spacing);
25
32
  }
26
33
 
27
- // Break out of centered container to full width.
34
+ // Break out to shadow/paper edge (content + gap on each side).
35
+ // Uses the same shadow-visibility condition as the body::before pseudo:
36
+ // when the shadow is visible, caps at content-width + 2*gap (shadow boundary);
37
+ // when the shadow is hidden (narrow viewport), goes full width.
28
38
  // position: relative creates a stacking context so background
29
39
  // covers adjacent siblings' content shadows.
30
- .full-bleed {
40
+ .bleed-edge {
31
41
  grid-column: 1 / -1;
42
+ width: 100%;
43
+ max-width: clamp(
44
+ #{string.unquote('calc(var(#{$prefix}content-width) + 2 * var(#{$prefix}content-shadow-gap))')},
45
+ #{string.unquote('calc((100% - 2 * var(#{$prefix}spacing) - var(#{$prefix}content-width) - 4 * var(#{$prefix}content-shadow-gap)) * -9999)')},
46
+ 100%
47
+ );
48
+ margin-inline: auto;
32
49
  box-shadow: none;
33
50
  position: relative;
34
51
  }
35
52
 
36
53
  // Break out to wide max-width
37
- .wide {
54
+ .bleed-wide {
38
55
  grid-column: 1 / -1;
39
56
  box-shadow: none;
40
57
  width: 100%;
@@ -43,6 +60,29 @@
43
60
  padding-inline: var(#{$prefix}spacing);
44
61
  }
45
62
 
63
+ // Break out of centered container to full viewport width.
64
+ // position: relative creates a stacking context so background
65
+ // covers adjacent siblings' content shadows.
66
+ .bleed-full {
67
+ grid-column: 1 / -1;
68
+ box-shadow: none;
69
+ position: relative;
70
+ }
71
+
72
+ // Responsive equal-column grid
73
+ // Mobile: single column. Above phone breakpoint: auto-fit columns.
74
+ // margin-bottom matches vertical rhythm of p, ul, etc.
75
+ .grid {
76
+ display: grid;
77
+ grid-template-columns: 1fr;
78
+ gap: var(#{$prefix}spacing);
79
+ margin-bottom: var(#{$prefix}spacing);
80
+
81
+ @media (min-width: #{$breakpoint-phone}) {
82
+ grid-template-columns: repeat(auto-fit, minmax(0%, 1fr));
83
+ }
84
+ }
85
+
46
86
  }
47
87
 
48
88
  }
package/src/_links.scss CHANGED
@@ -11,7 +11,7 @@
11
11
  :where(a:not([role="button"])) {
12
12
  color: var(#{$prefix}primary);
13
13
  text-decoration: underline;
14
- text-underline-offset: 0.15em;
14
+ text-underline-offset: 0.15em; // no OP match; OP normalize: 1px (2px Firefox)
15
15
  text-decoration-color: #{string.unquote('color-mix(in oklch, var(#{$prefix}primary), transparent 50%)')};
16
16
  transition: color 0.2s, text-decoration-color 0.2s;
17
17
  }
package/src/_media.scss CHANGED
@@ -18,9 +18,9 @@
18
18
  }
19
19
 
20
20
  :where(figcaption) {
21
- font-size: 0.9em;
21
+ font-size: 0.9em; // no OP match; OP normalize: --font-size-1 (1rem)
22
22
  color: color-mix(in oklch, var(#{$prefix}text), transparent 40%);
23
- margin-top: 0.5em;
23
+ margin-top: 0.5em; // OP ~size-2 (em-relative)
24
24
  }
25
25
 
26
26
  }
package/src/_meter.scss CHANGED
@@ -16,7 +16,7 @@
16
16
  -moz-appearance: none;
17
17
  appearance: none;
18
18
  width: 100%;
19
- height: 0.5rem;
19
+ height: 0.5rem; // OP --size-2
20
20
  overflow: hidden;
21
21
  border: 0;
22
22
  border-radius: var(#{$prefix}radius);
@@ -32,7 +32,7 @@
32
32
  border-radius: var(#{$prefix}radius);
33
33
  background-color: var(#{$prefix}surface-3);
34
34
  border: 0;
35
- height: 0.5rem;
35
+ height: 0.5rem; // OP --size-2
36
36
  }
37
37
 
38
38
  // WebKit: value bars for each semantic state
package/src/_print.scss CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  a[href]::after {
15
15
  content: " (" attr(href) ")";
16
- font-size: 0.85em;
16
+ font-size: 0.85em; // no OP match (near ~size-3)
17
17
  color: #555;
18
18
  }
19
19
 
@@ -8,81 +8,12 @@
8
8
 
9
9
  @layer nimble.base {
10
10
 
11
+ // Native system progress bars on all platforms.
12
+ // Custom styling via appearance: none is broken on iOS Safari (partial
13
+ // stripping of native chrome) and cannot be scoped away from WebKit/Blink
14
+ // via CSS alone. Native bars look fine everywhere.
11
15
  :where(progress) {
12
- -webkit-appearance: none;
13
- appearance: none;
14
- position: relative;
15
16
  width: 100%;
16
- height: 0.5rem;
17
- overflow: hidden;
18
- border: 0;
19
- border-radius: var(#{$prefix}radius);
20
- background-color: var(#{$prefix}surface-3);
21
- color: var(#{$prefix}primary); // Firefox uses color for the bar
22
- }
23
-
24
- :where(progress)::-webkit-progress-bar {
25
- border-radius: var(#{$prefix}radius);
26
- background-color: var(#{$prefix}surface-3);
27
- }
28
-
29
- :where(progress)::-webkit-progress-value {
30
- background-color: var(#{$prefix}primary);
31
- border-radius: var(#{$prefix}radius);
32
- transition: inline-size 0.3s ease;
33
- }
34
-
35
- :where(progress)::-moz-progress-bar {
36
- background-color: var(#{$prefix}primary);
37
- border-radius: var(#{$prefix}radius);
38
- }
39
-
40
- // Indeterminate: evenly-spaced blue bands travel continuously rightward.
41
- // Gradient is 200% wide with two identical 10%-wide bands at 15% and 65%.
42
- // They're 50% apart — exactly one visible width (100/200 = 50% of gradient).
43
- // Animating position by -200% = one full gradient width → seamless loop.
44
- :where(progress):not([value]) {
45
- --nc-progress-track:
46
- linear-gradient(to right,
47
- var(#{$prefix}surface-3) 0%,
48
- var(#{$prefix}primary) 25%,
49
- var(#{$prefix}primary) 25%,
50
- var(#{$prefix}surface-3) 50%,
51
- var(#{$prefix}surface-3) 50%,
52
- var(#{$prefix}primary) 75%,
53
- var(#{$prefix}primary) 75%,
54
- var(#{$prefix}surface-3) 100%);
55
- --nc-progress-track-size: 200% 100%;
56
- }
57
-
58
- @media (prefers-reduced-motion: no-preference) {
59
- @supports selector(progress::after) {
60
- :where(progress):not([value])::after {
61
- content: "";
62
- position: absolute;
63
- inset: 0;
64
- background: var(--nc-progress-track);
65
- background-size: var(--nc-progress-track-size);
66
- animation: nc-progress-indeterminate 12s linear infinite;
67
- }
68
- }
69
-
70
- :where(progress):not([value])::-webkit-progress-bar {
71
- background: var(--nc-progress-track);
72
- background-size: var(--nc-progress-track-size);
73
- animation: nc-progress-indeterminate 12s linear infinite;
74
- }
75
-
76
- :where(progress):not([value])::-moz-progress-bar {
77
- background: var(--nc-progress-track);
78
- background-size: var(--nc-progress-track-size);
79
- animation: nc-progress-indeterminate 12s linear infinite;
80
- }
81
- }
82
-
83
- @keyframes nc-progress-indeterminate {
84
- from { background-position: 0% 0%; }
85
- to { background-position: -200% 0%; }
86
17
  }
87
18
 
88
19
  }
package/src/_reset.scss CHANGED
@@ -81,11 +81,11 @@
81
81
  }
82
82
 
83
83
  :where(sub) {
84
- bottom: -0.25em;
84
+ bottom: -0.25em; // OP ~size-1 (em-relative, negative)
85
85
  }
86
86
 
87
87
  :where(sup) {
88
- top: -0.5em;
88
+ top: -0.5em; // OP ~size-2 (em-relative, negative)
89
89
  }
90
90
 
91
91
  // --- Embedded content ---
package/src/_select.scss CHANGED
@@ -32,7 +32,7 @@
32
32
  border: 1px solid var(#{$prefix}border);
33
33
  border-radius: var(#{$prefix}radius);
34
34
  background-color: var(#{$prefix}surface-1);
35
- padding: 0.25em;
35
+ padding: 0.25em; // OP ~size-1 (em-relative)
36
36
  // Fade in/out animation
37
37
  opacity: 0;
38
38
  transition: opacity 0.2s, overlay 0.2s allow-discrete, display 0.2s allow-discrete;
@@ -50,7 +50,8 @@
50
50
 
51
51
  // Options inside the picker
52
52
  :where(select) option {
53
- padding: 0.5em 0.75em;
53
+ padding: 0.5em 0.75em; // 0.5em: OP ~size-2; 0.75em: no OP match
54
+ // OP normalize: select uses 0.75ch block, --size-relative-4 inline
54
55
  border-radius: var(#{$prefix}radius);
55
56
  transition: background-color 0.15s;
56
57
  }
package/src/_tables.scss CHANGED
@@ -14,13 +14,14 @@
14
14
  }
15
15
 
16
16
  :where(th, td) {
17
- padding: 0.5em 0.75em;
17
+ padding: 0.5em 0.75em; // 0.5em: OP ~size-2; 0.75em: no OP match
18
+ // OP normalize: --size-2 (0.5rem) both axes
18
19
  border-bottom: 1px solid #{string.unquote('color-mix(in oklch, var(#{$prefix}border), transparent 40%)')};
19
20
  text-align: start;
20
21
  }
21
22
 
22
23
  :where(thead th, thead td) {
23
- font-weight: 600;
24
+ font-weight: 600; // OP --font-weight-6
24
25
  border-bottom-width: 2px;
25
26
  background-color: var(#{$prefix}surface-2);
26
27
  text-wrap: balance;
@@ -12,22 +12,19 @@
12
12
  // h2–h6 always carry top margin so they breathe above any preceding content,
13
13
  // regardless of DOM nesting (first-child of section, etc.).
14
14
  // Spec §8.3
15
+ // OP normalize uses --font-lineheight-1 (1.25) for ALL headings + --font-weight-9 (900).
16
+ // nimble uses a graduated lh scale + weight 700 (--font-weight-7).
17
+ // OP normalize heading sizes: h1=3.5rem h2=2.5rem h3=2rem h4=1.5rem h5=1.25rem h6=1.1rem
15
18
  $_heading-scale: (
16
- h1: (size: 2rem, lh: 1.1, mt: 0),
17
- h2: (size: 1.75rem, lh: 1.15, mt: 2rem),
18
- h3: (size: 1.5rem, lh: 1.2, mt: 1.5rem),
19
- h4: (size: 1.25rem, lh: 1.3, mt: 1.5rem),
20
- h5: (size: 1.125rem, lh: 1.4, mt: 1.5rem),
21
- h6: (size: 1rem, lh: 1.5, mt: 1.5rem),
19
+ h1: (size: 2rem, lh: 1.1, mt: 0), // OP --font-size-5/--size-7; lh: --font-lineheight-0
20
+ h2: (size: 1.75rem, lh: 1.15, mt: 2rem), // OP --size-6; lh: no OP match (near lh-0); mt: --size-7
21
+ h3: (size: 1.5rem, lh: 1.2, mt: 1.5rem), // OP --font-size-4/--size-5; lh: no OP match (near lh-1); mt: --size-5
22
+ h4: (size: 1.25rem, lh: 1.3, mt: 1.5rem), // OP --font-size-3/--size-4; lh: no OP match (near lh-1); mt: --size-5
23
+ h5: (size: 1.125rem, lh: 1.4, mt: 1.5rem), // no OP match (OP --font-size-2: 1.1rem); lh: no OP match; mt: --size-5
24
+ h6: (size: 1rem, lh: 1.5, mt: 1.5rem), // OP --font-size-1/--size-3; lh: --font-lineheight-3; mt: --size-5
22
25
  );
23
26
 
24
- // Phone breakpoint heading overrides
25
- // Spec §8.3
26
- $_heading-phone: (
27
- h1: 1.75rem,
28
- h2: 1.5rem,
29
- h3: 1.3rem,
30
- );
27
+
31
28
 
32
29
  @layer nimble.base {
33
30
 
@@ -38,21 +35,12 @@ $_heading-phone: (
38
35
  font-size: map.get($vals, size);
39
36
  line-height: map.get($vals, lh);
40
37
  margin-top: map.get($vals, mt);
41
- margin-bottom: var(#{$prefix}spacing);
42
- font-weight: 700;
38
+ margin-bottom: var(#{$prefix}spacing); // OP --size-3
39
+ font-weight: 700; // OP --font-weight-7 (OP normalize uses weight-9: 900)
43
40
  text-wrap: balance;
44
41
  }
45
42
  }
46
43
 
47
- // Phone breakpoint: scale down h1-h3
48
- @media (max-width: #{$breakpoint-phone}) {
49
- @each $tag, $size in $_heading-phone {
50
- #{$tag} {
51
- font-size: #{$size};
52
- }
53
- }
54
- }
55
-
56
44
  // ----- Vertical rhythm -----
57
45
  // Block elements: no top margin, consistent bottom margin
58
46
  // Spec §8.4
@@ -62,16 +50,29 @@ $_heading-phone: (
62
50
  margin-bottom: var(#{$prefix}spacing);
63
51
  }
64
52
 
53
+ // Prevent double spacing at page edges: body has padding-block,
54
+ // so strip margins that would stack with it.
55
+ // Must live inside @scope so it beats the scoped margin rules above.
56
+ // Uses :nth-child/:nth-last-child(of ...) to skip <script>/<style>.
57
+ body > :first-child,
58
+ body > :first-child > :first-child {
59
+ margin-top: 0;
60
+ }
61
+ body > :nth-last-child(1 of :not(script, style, dialog)),
62
+ body > :nth-last-child(1 of :not(script, style, dialog)) > :last-child {
63
+ margin-bottom: 0;
64
+ }
65
+
65
66
 
66
67
 
67
68
  // ----- Lists -----
68
69
 
69
70
  ul, ol {
70
- padding-inline-start: 1.5em;
71
+ padding-inline-start: 1.5em; // OP ~size-5 (em-relative); OP normalize: --size-8 (3rem)
71
72
  }
72
73
 
73
74
  :where(li) {
74
- margin-bottom: 0.25em;
75
+ margin-bottom: 0.25em; // OP ~size-1 (em-relative)
75
76
  }
76
77
 
77
78
  // Remove bottom margin on nested lists
@@ -80,12 +81,12 @@ $_heading-phone: (
80
81
  }
81
82
 
82
83
  dt {
83
- font-weight: 600;
84
+ font-weight: 600; // OP --font-weight-6 (OP normalize uses weight-7: 700)
84
85
  }
85
86
 
86
87
  dd {
87
- margin-inline-start: 1.5em;
88
- margin-bottom: 0.5em;
88
+ margin-inline-start: 1.5em; // OP ~size-5 (em-relative)
89
+ margin-bottom: 0.5em; // OP ~size-2 (em-relative)
89
90
  }
90
91
 
91
92
  // ----- Blockquote -----
@@ -94,15 +95,15 @@ $_heading-phone: (
94
95
  blockquote {
95
96
  margin-block: var(#{$prefix}spacing);
96
97
  margin-inline: 0;
97
- padding: 0.25em var(#{$prefix}spacing);
98
- border-inline-start: 0.25rem solid var(#{$prefix}border);
98
+ padding: 0.25em var(#{$prefix}spacing); // OP ~size-1 (em-relative), --size-3
99
+ border-inline-start: 0.25rem solid var(#{$prefix}border); // OP --size-1
99
100
  font-style: italic;
100
101
  }
101
102
 
102
103
  :where(blockquote) footer,
103
104
  :where(blockquote) cite {
104
105
  font-style: normal;
105
- font-size: 0.9em;
106
+ font-size: 0.9em; // no OP match (near ~size-3)
106
107
  color: color-mix(in oklch, var(#{$prefix}text), transparent 40%);
107
108
  }
108
109
 
@@ -120,7 +121,7 @@ $_heading-phone: (
120
121
  // Spec §9.10
121
122
 
122
123
  mark {
123
- padding: 0.1em 0.25em;
124
+ padding: 0.1em 0.25em; // 0.1em: no OP match (micro); 0.25em: OP ~size-1
124
125
  background-color: #{string.unquote('light-dark(#fde68a, oklch(0.55 0.12 85))')};
125
126
  color: #{string.unquote('light-dark(inherit, oklch(0.95 0.01 85))')};
126
127
  border-radius: 2px;
@@ -18,6 +18,14 @@
18
18
  @use 'layout-utilities';
19
19
  @use 'print';
20
20
 
21
+ // Sentinel markers — no-nimble.js uses these to find where scopeable rules begin
22
+ @layer nimble.base {
23
+ :root { --nimble-scope-start: 1 }
24
+ }
25
+ @layer nimble.utilities {
26
+ :root { --nimble-scope-start: 1 }
27
+ }
28
+
21
29
  // Component-level styles — optionally scoped
22
30
  @if $exclude-selector {
23
31
  @scope (:root) to (#{$exclude-selector}) {
package/src/nimble.scss CHANGED
@@ -19,6 +19,14 @@
19
19
  @use 'layout-utilities';
20
20
  @use 'print';
21
21
 
22
+ // Sentinel markers — no-nimble.js uses these to find where scopeable rules begin
23
+ @layer nimble.base {
24
+ :root { --nimble-scope-start: 1 }
25
+ }
26
+ @layer nimble.utilities {
27
+ :root { --nimble-scope-start: 1 }
28
+ }
29
+
22
30
  // Component-level styles — optionally scoped
23
31
  @if $exclude-selector {
24
32
  @scope (:root) to (#{$exclude-selector}) {