@justai/cuts 0.138.0 → 0.139.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.138.0",
3
+ "version": "0.139.0",
4
4
  "description": "A persona's named parts \u2014 the page shell that holds them, and the cuts themselves.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@justai/core": "^0.36.0",
45
- "@justai/ui": "^0.25.0",
45
+ "@justai/ui": "^0.26.0",
46
46
  "@justai/atrium": "^0.1.9"
47
47
  },
48
48
  "scripts": {
@@ -45,36 +45,32 @@ const { title, note } = Astro.props;
45
45
  </div>
46
46
 
47
47
  <style>
48
- /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
49
- out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
50
- @layer justai.components {
51
- /* queries: two action lists touching · no gap between groups · the second group sits on the first
52
- THE BREAK IS THE GRAMMAR, so the component owns it.
53
- A group carried its INTERNAL 7px and nothing between itself and the next one, which left the
54
- break — the thing that separates "things you might do" from "the door with no way back" — to
55
- whichever consumer remembered a wrapper. files did in one sheet and not in another, and the
56
- second one read as one long list with a red row at the end.
57
- `:last-child` so a lone group adds no trailing space it did not ask for. */
58
- .jal-group { display: flex; flex-direction: column; gap: 7px; }
59
- .jal-group + .jal-group { margin-block-start: 22px; }
60
- /* Inset and grouped: the card is the group, and `overflow: hidden` is what lets a row's own
61
- background reach the rounded corner instead of squaring it off on press. */
62
- .jal {
63
- display: flex;
64
- flex-direction: column;
65
- background: var(--surface-2);
66
- border: 1px solid var(--border);
67
- border-radius: var(--radius-md);
68
- overflow: hidden;
69
- }
70
- .jal-title {
71
- font-size: var(--fs-xs);
72
- font-weight: 600;
73
- color: var(--text-dim);
74
- text-transform: uppercase;
75
- letter-spacing: 0.04em;
76
- padding-inline: 4px;
77
- }
78
- .jal-note { font-size: var(--fs-xs); color: var(--text-dim); padding-inline: 4px; line-height: 1.45; }
48
+ /* queries: two action lists touching · no gap between groups · the second group sits on the first
49
+ THE BREAK IS THE GRAMMAR, so the component owns it.
50
+ A group carried its INTERNAL 7px and nothing between itself and the next one, which left the
51
+ break — the thing that separates "things you might do" from "the door with no way back" — to
52
+ whichever consumer remembered a wrapper. files did in one sheet and not in another, and the
53
+ second one read as one long list with a red row at the end.
54
+ `:last-child` so a lone group adds no trailing space it did not ask for. */
55
+ .jal-group { display: flex; flex-direction: column; gap: 7px; }
56
+ .jal-group + .jal-group { margin-block-start: 22px; }
57
+ /* Inset and grouped: the card is the group, and `overflow: hidden` is what lets a row's own
58
+ background reach the rounded corner instead of squaring it off on press. */
59
+ .jal {
60
+ display: flex;
61
+ flex-direction: column;
62
+ background: var(--surface-2);
63
+ border: 1px solid var(--border);
64
+ border-radius: var(--radius-md);
65
+ overflow: hidden;
79
66
  }
67
+ .jal-title {
68
+ font-size: var(--fs-xs);
69
+ font-weight: 600;
70
+ color: var(--text-dim);
71
+ text-transform: uppercase;
72
+ letter-spacing: 0.04em;
73
+ padding-inline: 4px;
74
+ }
75
+ .jal-note { font-size: var(--fs-xs); color: var(--text-dim); padding-inline: 4px; line-height: 1.45; }
80
76
  </style>
@@ -69,76 +69,72 @@ const { id, label, tone = "plain", detail, hidden, i18n, ...rest } = Astro.props
69
69
  </button>
70
70
 
71
71
  <style>
72
- /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
73
- out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
74
- @layer justai.components {
75
- /* A ROW, not a button in a line: it takes the full width it is given, so the label is never the
76
- thing that has to shrink. The 44px floor is the one number iOS never moves — it is a fingertip,
77
- and `min-height` rather than a fixed height so a reader who raised their text size gets a
78
- taller row instead of a clipped one. */
79
- .jal-row {
80
- display: flex;
81
- align-items: center;
82
- gap: 12px;
83
- width: 100%;
84
- min-height: 44px;
85
- padding: 11px 14px;
86
- border: 0;
87
- background: none;
88
- color: var(--text);
89
- font: inherit;
90
- font-size: var(--fs-sm);
91
- font-weight: 500;
92
- text-align: start;
93
- cursor: pointer;
94
- /* The separator lives on the row and is removed from the last one by the list — so a row can be
95
- hidden or revealed without leaving a hairline hanging under nothing. */
96
- border-block-end: 1px solid var(--border);
97
- transition: background 0.15s;
98
- }
99
- .jal-row:last-child,
100
- .jal-row[hidden] + .jal-row:last-child { border-block-end: 0; }
101
- .jal-row:active { background: color-mix(in srgb, var(--text) 8%, transparent); }
102
- @media (hover: hover) {
103
- .jal-row:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
104
- }
105
- .jal-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
106
-
107
- .jal-ico {
108
- display: inline-flex;
109
- flex: none;
110
- color: var(--text-dim);
111
- }
112
- /* The glyph is cut to a fixed box, so it is sized in px on purpose — it is a mark, not reading
113
- text, and the box it sits in is a hand size rather than a type size. */
114
- .jal-ico :global(svg) { width: 20px; height: 20px; display: block; }
72
+ /* A ROW, not a button in a line: it takes the full width it is given, so the label is never the
73
+ thing that has to shrink. The 44px floor is the one number iOS never moves — it is a fingertip,
74
+ and `min-height` rather than a fixed height so a reader who raised their text size gets a
75
+ taller row instead of a clipped one. */
76
+ .jal-row {
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 12px;
80
+ width: 100%;
81
+ min-height: 44px;
82
+ padding: 11px 14px;
83
+ border: 0;
84
+ background: none;
85
+ color: var(--text);
86
+ font: inherit;
87
+ font-size: var(--fs-sm);
88
+ font-weight: 500;
89
+ text-align: start;
90
+ cursor: pointer;
91
+ /* The separator lives on the row and is removed from the last one by the list — so a row can be
92
+ hidden or revealed without leaving a hairline hanging under nothing. */
93
+ border-block-end: 1px solid var(--border);
94
+ transition: background 0.15s;
95
+ }
96
+ .jal-row:last-child,
97
+ .jal-row[hidden] + .jal-row:last-child { border-block-end: 0; }
98
+ .jal-row:active { background: color-mix(in srgb, var(--text) 8%, transparent); }
99
+ @media (hover: hover) {
100
+ .jal-row:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
101
+ }
102
+ .jal-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
115
103
 
116
- /* The label takes the room. This is the whole point of the component: nothing here clips. */
117
- .jal-label { flex: 1; min-width: 0; }
118
- .jal-detail { color: var(--text-dim); font-size: var(--fs-xs); flex: none; }
104
+ .jal-ico {
105
+ display: inline-flex;
106
+ flex: none;
107
+ color: var(--text-dim);
108
+ }
109
+ /* The glyph is cut to a fixed box, so it is sized in px on purpose — it is a mark, not reading
110
+ text, and the box it sits in is a hand size rather than a type size. */
111
+ .jal-ico :global(svg) { width: 20px; height: 20px; display: block; }
119
112
 
120
- /* queries: the accent row is unreadable in light mode · is-accent contrast · accent as text ·
121
- my persona's accent is too bright to read in a sheet
122
- THE ACCENT AS INK IS NOT THE ACCENT AS PAINT. A fill only has to be SEEN; a word has to be READ,
123
- and a persona whose accent is light fails contrast here while looking perfect as a button — tts's
124
- amber measures 1.67:1 on the light surface against the 4.5 a word needs.
125
- This is the same problem ConfirmSheet already solved with `--confirm-accent`, one component over,
126
- for the same stated reason ("the action reads as TEXT"). The hook is additive: a consumer that
127
- sets nothing gets exactly what it got before, and one that sets `--accent-text` gets a readable
128
- row. Prefer fixing it here over a persona globalising this class — that is what tts had to do
129
- while this shipped, and a persona reaching into a cut's internals is a fix that rots. */
130
- .jal-row.is-accent { color: var(--accent-text, var(--accent)); font-weight: 600; }
131
- .jal-row.is-accent .jal-ico { color: var(--accent-text, var(--accent)); }
132
- .jal-row.is-danger { color: var(--danger); }
133
- .jal-row.is-danger .jal-ico { color: var(--danger); }
134
- .jal-row.is-danger:active { background: color-mix(in srgb, var(--danger) 12%, transparent); }
135
- @media (hover: hover) {
136
- .jal-row.is-danger:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); }
137
- }
113
+ /* The label takes the room. This is the whole point of the component: nothing here clips. */
114
+ .jal-label { flex: 1; min-width: 0; }
115
+ .jal-detail { color: var(--text-dim); font-size: var(--fs-xs); flex: none; }
138
116
 
139
- .jal-row[disabled] { opacity: 0.4; cursor: default; }
140
- /* A class-set display beats the user agent's `[hidden] { display: none }` — that has bitten this
141
- ecosystem twice already, in this very sheet. Said again where the display is set. */
142
- .jal-row[hidden] { display: none !important; }
117
+ /* queries: the accent row is unreadable in light mode · is-accent contrast · accent as text ·
118
+ my persona's accent is too bright to read in a sheet
119
+ THE ACCENT AS INK IS NOT THE ACCENT AS PAINT. A fill only has to be SEEN; a word has to be READ,
120
+ and a persona whose accent is light fails contrast here while looking perfect as a button — tts's
121
+ amber measures 1.67:1 on the light surface against the 4.5 a word needs.
122
+ This is the same problem ConfirmSheet already solved with `--confirm-accent`, one component over,
123
+ for the same stated reason ("the action reads as TEXT"). The hook is additive: a consumer that
124
+ sets nothing gets exactly what it got before, and one that sets `--accent-text` gets a readable
125
+ row. Prefer fixing it here over a persona globalising this class — that is what tts had to do
126
+ while this shipped, and a persona reaching into a cut's internals is a fix that rots. */
127
+ .jal-row.is-accent { color: var(--accent-text, var(--accent)); font-weight: 600; }
128
+ .jal-row.is-accent .jal-ico { color: var(--accent-text, var(--accent)); }
129
+ .jal-row.is-danger { color: var(--danger); }
130
+ .jal-row.is-danger .jal-ico { color: var(--danger); }
131
+ .jal-row.is-danger:active { background: color-mix(in srgb, var(--danger) 12%, transparent); }
132
+ @media (hover: hover) {
133
+ .jal-row.is-danger:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); }
143
134
  }
135
+
136
+ .jal-row[disabled] { opacity: 0.4; cursor: default; }
137
+ /* A class-set display beats the user agent's `[hidden] { display: none }` — that has bitten this
138
+ ecosystem twice already, in this very sheet. Said again where the display is set. */
139
+ .jal-row[hidden] { display: none !important; }
144
140
  </style>
@@ -92,48 +92,44 @@ const href = `https://${apex}`;
92
92
  </a>
93
93
 
94
94
  <style>
95
- /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
96
- out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
97
- @layer justai.components {
98
- /* icon-only, flat — flush to the left edge of the (full-bleed) toolbar */
99
- .home {
100
- display: inline-grid; place-items: center; width: 36px; height: 36px;
101
- color: var(--text-dim); text-decoration: none; margin-inline-start: -7px;
102
- transition: color 0.2s, transform 0.2s var(--ease-spring);
103
- }
104
- .home:hover { color: var(--text); }
105
- .home:active { transform: scale(0.9); }
106
- .home svg { width: 20px; height: 20px; display: block; }
95
+ /* icon-only, flat — flush to the left edge of the (full-bleed) toolbar */
96
+ .home {
97
+ display: inline-grid; place-items: center; width: 36px; height: 36px;
98
+ color: var(--text-dim); text-decoration: none; margin-inline-start: -7px;
99
+ transition: color 0.2s, transform 0.2s var(--ease-spring);
100
+ }
101
+ .home:hover { color: var(--text); }
102
+ .home:active { transform: scale(0.9); }
103
+ .home svg { width: 20px; height: 20px; display: block; }
107
104
 
108
- /* At rest every part of the mark is the row's ink, so the silhouette reads as one shape rather than
109
- a logo parked in a toolbar. The centre is FILLED rather than knocked out: in the coloured drawing
110
- it is a counter-shape separating four saturated arms, and in one colour there is nothing to
111
- separate — a hole there would be a gap the mark does not have. */
112
- .azmark .arm,
113
- .azmark .ctr { fill: currentColor; }
105
+ /* At rest every part of the mark is the row's ink, so the silhouette reads as one shape rather than
106
+ a logo parked in a toolbar. The centre is FILLED rather than knocked out: in the coloured drawing
107
+ it is a counter-shape separating four saturated arms, and in one colour there is nothing to
108
+ separate — a hole there would be a gap the mark does not have. */
109
+ .azmark .arm,
110
+ .azmark .ctr { fill: currentColor; }
114
111
 
115
- /* queries: the home icon is smaller than the other glyphs · 20px of ink · pgmark size
116
- FILLED, AND THE viewBox IS THE INK'S OWN BOUNDS. This mark was three round-capped strokes, which
117
- is the same shape — but getBBox reports a stroke's centreline only, and the fleet's "every icon
118
- paints 20px of ink" spec measured it at 9.9px beside 20px glyphs. The spec was right: at that
119
- size it really was half its neighbours. Rotated rounded rects draw exactly what the strokes did
120
- and measure what they look like. */
121
- .pgmark .arm { fill: currentColor; }
122
- .home:hover .pgmark .a1, .home:focus-visible .pgmark .a1 { fill: #3D6FD9; }
123
- .home:hover .pgmark .a2, .home:focus-visible .pgmark .a2 { fill: #E8503A; }
124
- .home:hover .pgmark .a3, .home:focus-visible .pgmark .a3 { fill: #F5A623; }
112
+ /* queries: the home icon is smaller than the other glyphs · 20px of ink · pgmark size
113
+ FILLED, AND THE viewBox IS THE INK'S OWN BOUNDS. This mark was three round-capped strokes, which
114
+ is the same shape — but getBBox reports a stroke's centreline only, and the fleet's "every icon
115
+ paints 20px of ink" spec measured it at 9.9px beside 20px glyphs. The spec was right: at that
116
+ size it really was half its neighbours. Rotated rounded rects draw exactly what the strokes did
117
+ and measure what they look like. */
118
+ .pgmark .arm { fill: currentColor; }
119
+ .home:hover .pgmark .a1, .home:focus-visible .pgmark .a1 { fill: #3D6FD9; }
120
+ .home:hover .pgmark .a2, .home:focus-visible .pgmark .a2 { fill: #E8503A; }
121
+ .home:hover .pgmark .a3, .home:focus-visible .pgmark .a3 { fill: #F5A623; }
125
122
 
126
- .home:hover .azmark .a1, .home:focus-visible .azmark .a1 { fill: url(#azHome1); }
127
- .home:hover .azmark .a2, .home:focus-visible .azmark .a2 { fill: url(#azHome2); }
128
- .home:hover .azmark .a3, .home:focus-visible .azmark .a3 { fill: url(#azHome3); }
129
- .home:hover .azmark .a4, .home:focus-visible .azmark .a4 { fill: url(#azHome4); }
130
- /* The centre follows the ground, and both values are the project's own: #F5F1E8 is the centre of
131
- Logo Black, drawn for dark grounds, and #2B2724 the centre of Logo White, drawn for pale ones.
132
- Dark is the un-queried default because the fleet's tokens are dark-first. */
133
- .home:hover .azmark .ctr, .home:focus-visible .azmark .ctr { fill: #F5F1E8; }
134
- @media (prefers-color-scheme: light) {
135
- :global(html:not([data-theme="dark"])) .home:hover .azmark .ctr, :global(html:not([data-theme="dark"])) .home:focus-visible .azmark .ctr { fill: #2B2724; }
136
- }
123
+ .home:hover .azmark .a1, .home:focus-visible .azmark .a1 { fill: url(#azHome1); }
124
+ .home:hover .azmark .a2, .home:focus-visible .azmark .a2 { fill: url(#azHome2); }
125
+ .home:hover .azmark .a3, .home:focus-visible .azmark .a3 { fill: url(#azHome3); }
126
+ .home:hover .azmark .a4, .home:focus-visible .azmark .a4 { fill: url(#azHome4); }
127
+ /* The centre follows the ground, and both values are the project's own: #F5F1E8 is the centre of
128
+ Logo Black, drawn for dark grounds, and #2B2724 the centre of Logo White, drawn for pale ones.
129
+ Dark is the un-queried default because the fleet's tokens are dark-first. */
130
+ .home:hover .azmark .ctr, .home:focus-visible .azmark .ctr { fill: #F5F1E8; }
131
+ @media (prefers-color-scheme: light) {
132
+ :global(html:not([data-theme="dark"])) .home:hover .azmark .ctr, :global(html:not([data-theme="dark"])) .home:focus-visible .azmark .ctr { fill: #2B2724; }
137
133
  }
138
134
  </style>
139
135
 
@@ -389,96 +389,92 @@ const isNew = (added: string): boolean => { const t0 = Date.parse(added || "");
389
389
  </script>
390
390
 
391
391
  <style>
392
- /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
393
- out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
394
- @layer justai.components {
395
- /* The three gaps down this column are one rhythm: 26 to the search, 18 from it to the grid, 14
396
- between cards. Each step is about 1.4x the next, so they read as one thing narrowing — section,
397
- then control-to-list, then item-to-item.
398
- THE TOP ONE USED TO BE `clamp(20px, 5vh, 44px)`, which is not a chosen number: it is whatever the
399
- viewport gives. Measured, the same page had 37px of it at 700px tall and 46px at 900px, against a
400
- fixed 18px below — so the search sat 1.8x from the bar on a laptop and 2.2x on a desktop, and at
401
- that distance it stops reading as the head of the grid and starts looking as though it drifted
402
- down toward it. A gap that changes with the window is a rhythm nobody designed. */
403
- .kbody { flex: 1 0 auto; display: flex; flex-direction: column; padding-block: 26px 16px; }
392
+ /* The three gaps down this column are one rhythm: 26 to the search, 18 from it to the grid, 14
393
+ between cards. Each step is about 1.4x the next, so they read as one thing narrowing — section,
394
+ then control-to-list, then item-to-item.
395
+ THE TOP ONE USED TO BE `clamp(20px, 5vh, 44px)`, which is not a chosen number: it is whatever the
396
+ viewport gives. Measured, the same page had 37px of it at 700px tall and 46px at 900px, against a
397
+ fixed 18px below — so the search sat 1.8x from the bar on a laptop and 2.2x on a desktop, and at
398
+ that distance it stops reading as the head of the grid and starts looking as though it drifted
399
+ down toward it. A gap that changes with the window is a rhythm nobody designed. */
400
+ .kbody { flex: 1 0 auto; display: flex; flex-direction: column; padding-block: 26px 16px; }
404
401
 
405
- .controls { margin-block-end: 18px; }
402
+ .controls { margin-block-end: 18px; }
406
403
 
407
- /* The field carries the CARD's material, and the reason is a measurement rather than a preference.
408
- On this page "raised" means whiter plus a shadow: the cards are `--card-bg` (#fff in light) on an
409
- off-white ground. The field was `--surface-2` — 4% BLACK over that same ground — with no shadow, so
410
- it was not lifting, it was recessed, and a recessed input in light reads as disabled. Giving it the
411
- card recipe means the page has ONE raised surface instead of two treatments arguing.
412
- Dark loses nothing: there `--card-bg` and `--surface` are the same value, so the fill stays the 6%
413
- white overlay that was already lifting correctly, and only the shadow is added. That asymmetry is
414
- why the complaint could only come from the light side. */
415
- .search {
416
- display: flex; align-items: center; gap: 10px;
417
- background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-md);
418
- box-shadow: var(--card-shadow);
419
- /* the end padding is the button's, not the text's — 16px would park the icon away from the edge it
420
- belongs to, and the button brings its own 30px of hit area */
421
- padding: 12px 8px 12px 16px;
422
- transition: border-color 0.2s var(--ease-out), box-shadow 0.25s var(--ease-out);
423
- }
424
- /* focus lives on the whole box as a soft OUTER breeze — not a thick inner ring. The card shadow is
425
- re-stated after the ring rather than replaced by it: `box-shadow` is one property, so naming only
426
- the ring here would drop the lift the moment somebody clicked into the field. */
427
- .search:focus-within {
428
- border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
429
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent), var(--card-shadow);
430
- }
431
- .search svg { width: 18px; height: 18px; color: var(--text-dim); flex: none; }
432
- .search input { flex: 1; min-width: 0; border: none; background: none; color: var(--text); font: inherit; font-size: var(--fs-base); outline: none; }
433
- .search input:focus-visible { box-shadow: none; } /* the box carries the breeze; the input has no ring of its own */
404
+ /* The field carries the CARD's material, and the reason is a measurement rather than a preference.
405
+ On this page "raised" means whiter plus a shadow: the cards are `--card-bg` (#fff in light) on an
406
+ off-white ground. The field was `--surface-2` — 4% BLACK over that same ground — with no shadow, so
407
+ it was not lifting, it was recessed, and a recessed input in light reads as disabled. Giving it the
408
+ card recipe means the page has ONE raised surface instead of two treatments arguing.
409
+ Dark loses nothing: there `--card-bg` and `--surface` are the same value, so the fill stays the 6%
410
+ white overlay that was already lifting correctly, and only the shadow is added. That asymmetry is
411
+ why the complaint could only come from the light side. */
412
+ .search {
413
+ display: flex; align-items: center; gap: 10px;
414
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-md);
415
+ box-shadow: var(--card-shadow);
416
+ /* the end padding is the button's, not the text's — 16px would park the icon away from the edge it
417
+ belongs to, and the button brings its own 30px of hit area */
418
+ padding: 12px 8px 12px 16px;
419
+ transition: border-color 0.2s var(--ease-out), box-shadow 0.25s var(--ease-out);
420
+ }
421
+ /* focus lives on the whole box as a soft OUTER breeze — not a thick inner ring. The card shadow is
422
+ re-stated after the ring rather than replaced by it: `box-shadow` is one property, so naming only
423
+ the ring here would drop the lift the moment somebody clicked into the field. */
424
+ .search:focus-within {
425
+ border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
426
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent), var(--card-shadow);
427
+ }
428
+ .search svg { width: 18px; height: 18px; color: var(--text-dim); flex: none; }
429
+ .search input { flex: 1; min-width: 0; border: none; background: none; color: var(--text); font: inherit; font-size: var(--fs-base); outline: none; }
430
+ .search input:focus-visible { box-shadow: none; } /* the box carries the breeze; the input has no ring of its own */
434
431
 
435
- /* ---- the filter: a button at the field's own end, and the shelves in a sheet under it ---- */
436
- .fwrap { position: relative; } /* the panel hangs off this, not off .search, so focus styles stay put */
432
+ /* ---- the filter: a button at the field's own end, and the shelves in a sheet under it ---- */
433
+ .fwrap { position: relative; } /* the panel hangs off this, not off .search, so focus styles stay put */
437
434
 
438
- /* A 30px hit area inside a box whose height is set by the input's line box: the negative block margin
439
- pulls its outer height back under that line, so adding this button does NOT make the field taller.
440
- Measured before and after — the field is the same height it was with the chip row. */
441
- .fbtn {
442
- flex: none; display: grid; place-items: center; width: 30px; height: 30px; margin-block: -6px;
443
- border: none; background: none; padding: 0; border-radius: var(--radius-sm); cursor: pointer;
444
- color: var(--text-dim);
445
- transition: color 0.25s var(--ease-out), background 0.25s var(--ease-out);
446
- }
447
- /* `.search svg` sets colour on the ELEMENT, and a direct declaration beats an inherited one — so
448
- without this the button's own colour (and the accent below) would never reach its icon. Equal
449
- specificity, later in the sheet, so this wins. */
450
- .fbtn svg { width: 18px; height: 18px; color: inherit; }
451
- .fbtn:hover, .fbtn:focus-visible { color: var(--text); background: color-mix(in srgb, var(--text) 8%, transparent); }
452
- /* the row that used to announce the active shelf is gone, so the button carries it */
453
- .fbtn.on { color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); }
454
- .fpanel {
455
- position: absolute; inset-inline: 0; top: calc(100% + 8px); z-index: 5;
456
- display: flex; flex-direction: column; gap: 2px; padding: 6px;
457
- /* the column's own material — a translucent ground would let the cards read through the labels */
458
- background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
459
- box-shadow: 0 18px 40px -20px rgba(0, 0, 0, 0.45);
460
- backdrop-filter: blur(30px) saturate(1.4);
461
- -webkit-backdrop-filter: blur(30px) saturate(1.4);
462
- }
463
- .opt {
464
- display: flex; align-items: center; gap: 9px; padding: 9px 10px;
465
- border: none; background: none; border-radius: var(--radius-sm); cursor: pointer;
466
- color: var(--text); font: inherit; font-size: var(--fs-sm); text-align: start;
467
- transition: background 0.2s var(--ease-out);
468
- }
469
- .opt:hover, .opt:focus-visible { background: color-mix(in srgb, var(--text) 8%, transparent); }
470
- .opt .lb { flex: 1; min-width: 0; }
471
- /* the count is the shelf's size, so a person knows what is behind a word before spending a click */
472
- .opt .n { flex: none; font-variant-numeric: tabular-nums; font-size: var(--fs-2xs); color: var(--text-dim); }
473
- /* hidden, not absent: the tick keeps its column either way, so the labels do not jump sideways when
474
- the selection moves from one row to another */
475
- .opt .tick { flex: none; width: 15px; height: 15px; color: var(--accent); visibility: hidden; }
476
- .opt.active .tick { visibility: visible; }
477
- .opt.active .lb { font-weight: 600; }
478
-
479
- .grid { display: grid; grid-template-columns: 1fr; gap: 14px; }
480
- /* no entrance animation — when you walk into the room, everyone is already in their place.
481
- the cards are simply there on load; the only motion is a whisper on hover. */
482
- .empty { text-align: center; color: var(--text-dim); padding: 40px 0; }
435
+ /* A 30px hit area inside a box whose height is set by the input's line box: the negative block margin
436
+ pulls its outer height back under that line, so adding this button does NOT make the field taller.
437
+ Measured before and after — the field is the same height it was with the chip row. */
438
+ .fbtn {
439
+ flex: none; display: grid; place-items: center; width: 30px; height: 30px; margin-block: -6px;
440
+ border: none; background: none; padding: 0; border-radius: var(--radius-sm); cursor: pointer;
441
+ color: var(--text-dim);
442
+ transition: color 0.25s var(--ease-out), background 0.25s var(--ease-out);
443
+ }
444
+ /* `.search svg` sets colour on the ELEMENT, and a direct declaration beats an inherited one — so
445
+ without this the button's own colour (and the accent below) would never reach its icon. Equal
446
+ specificity, later in the sheet, so this wins. */
447
+ .fbtn svg { width: 18px; height: 18px; color: inherit; }
448
+ .fbtn:hover, .fbtn:focus-visible { color: var(--text); background: color-mix(in srgb, var(--text) 8%, transparent); }
449
+ /* the row that used to announce the active shelf is gone, so the button carries it */
450
+ .fbtn.on { color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); }
451
+ .fpanel {
452
+ position: absolute; inset-inline: 0; top: calc(100% + 8px); z-index: 5;
453
+ display: flex; flex-direction: column; gap: 2px; padding: 6px;
454
+ /* the column's own material — a translucent ground would let the cards read through the labels */
455
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
456
+ box-shadow: 0 18px 40px -20px rgba(0, 0, 0, 0.45);
457
+ backdrop-filter: blur(30px) saturate(1.4);
458
+ -webkit-backdrop-filter: blur(30px) saturate(1.4);
483
459
  }
460
+ .opt {
461
+ display: flex; align-items: center; gap: 9px; padding: 9px 10px;
462
+ border: none; background: none; border-radius: var(--radius-sm); cursor: pointer;
463
+ color: var(--text); font: inherit; font-size: var(--fs-sm); text-align: start;
464
+ transition: background 0.2s var(--ease-out);
465
+ }
466
+ .opt:hover, .opt:focus-visible { background: color-mix(in srgb, var(--text) 8%, transparent); }
467
+ .opt .lb { flex: 1; min-width: 0; }
468
+ /* the count is the shelf's size, so a person knows what is behind a word before spending a click */
469
+ .opt .n { flex: none; font-variant-numeric: tabular-nums; font-size: var(--fs-2xs); color: var(--text-dim); }
470
+ /* hidden, not absent: the tick keeps its column either way, so the labels do not jump sideways when
471
+ the selection moves from one row to another */
472
+ .opt .tick { flex: none; width: 15px; height: 15px; color: var(--accent); visibility: hidden; }
473
+ .opt.active .tick { visibility: visible; }
474
+ .opt.active .lb { font-weight: 600; }
475
+
476
+ .grid { display: grid; grid-template-columns: 1fr; gap: 14px; }
477
+ /* no entrance animation — when you walk into the room, everyone is already in their place.
478
+ the cards are simply there on load; the only motion is a whisper on hover. */
479
+ .empty { text-align: center; color: var(--text-dim); padding: 40px 0; }
484
480
  </style>