@justai/cuts 0.138.0 → 0.140.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.
@@ -46,79 +46,75 @@ const icon = `${app.url}/icon.svg`;
46
46
  </a>
47
47
 
48
48
  <style>
49
- /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
50
- out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
51
- @layer justai.components {
52
- .card {
53
- display: flex;
54
- align-items: center;
55
- gap: 16px;
56
- /* 16, down from 18, to pay for the larger icon: the icon is now what sets the card's height, and the
57
- two px back on each side keep the row pitch where the shorter card put it. */
58
- padding: 16px;
59
- /* The card's own material, not the page column's — see --card-* in tokens.css. In light mode this is
60
- solid white with a real shadow; in dark it is the glass, which there actually refracts. */
61
- background: var(--card-bg);
62
- border: 1px solid var(--border);
63
- border-radius: var(--radius-lg);
64
- backdrop-filter: var(--card-blur);
65
- -webkit-backdrop-filter: var(--card-blur);
66
- box-shadow: var(--card-shadow);
67
- text-decoration: none;
68
- color: var(--text);
69
- transition: transform 0.3s var(--ease-spring), border-color 0.3s, background 0.3s,
70
- box-shadow 0.3s var(--ease-out);
71
- will-change: transform;
72
- }
73
- /* the lift and the shadow move together — a card that rises without its shadow growing reads as a
74
- sticker sliding, not an object leaving the page */
75
- .card:hover {
76
- transform: translateY(-2px);
77
- border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
78
- box-shadow: var(--card-shadow-hover);
79
- }
80
- .card:active { transform: translateY(-1px) scale(0.99); }
81
-
82
- /* the persona's canonical icon — a self-contained squircle, one face everywhere */
83
- .icon {
84
- flex: none;
85
- /* 60. It was 72 for a day, on the reasoning that the icons are the only colour on this page — everything
86
- else is ink on paper — so more of them should read as livelier. It read as too big instead, and that
87
- turns out to be measurable rather than a matter of taste: the text beside the icon is a fixed 66px (a
88
- 16px title on a 1.5 line box, 3px, then two clamped bio lines at 13.44/1.45), so at 72 the icon
89
- overshot the text by six pixels and the icon, not the content, was setting the card's height.
90
- 64 is the first size that fits inside the text block. 60 gives the same card height as 64 — past the
91
- crossover the text sets it either way — so the last four pixels cost no vertical space and buy a
92
- quieter mark. */
93
- width: 60px;
94
- height: 60px;
95
- border-radius: var(--radius-md);
96
- object-fit: cover;
97
- box-shadow: 0 8px 20px -12px rgba(0, 0, 0, 0.35);
98
- }
49
+ .card {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 16px;
53
+ /* 16, down from 18, to pay for the larger icon: the icon is now what sets the card's height, and the
54
+ two px back on each side keep the row pitch where the shorter card put it. */
55
+ padding: 16px;
56
+ /* The card's own material, not the page column's — see --card-* in tokens.css. In light mode this is
57
+ solid white with a real shadow; in dark it is the glass, which there actually refracts. */
58
+ background: var(--card-bg);
59
+ border: 1px solid var(--border);
60
+ border-radius: var(--radius-lg);
61
+ backdrop-filter: var(--card-blur);
62
+ -webkit-backdrop-filter: var(--card-blur);
63
+ box-shadow: var(--card-shadow);
64
+ text-decoration: none;
65
+ color: var(--text);
66
+ transition: transform 0.3s var(--ease-spring), border-color 0.3s, background 0.3s,
67
+ box-shadow 0.3s var(--ease-out);
68
+ will-change: transform;
69
+ }
70
+ /* the lift and the shadow move together — a card that rises without its shadow growing reads as a
71
+ sticker sliding, not an object leaving the page */
72
+ .card:hover {
73
+ transform: translateY(-2px);
74
+ border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
75
+ box-shadow: var(--card-shadow-hover);
76
+ }
77
+ .card:active { transform: translateY(-1px) scale(0.99); }
99
78
 
100
- .meta { flex: 1; min-width: 0; }
101
- .top { display: flex; align-items: center; gap: 8px; }
102
- h2 { font-size: var(--fs-base); font-weight: 600; letter-spacing: -0.01em; }
103
- /* Clamped to two lines. The bios run from one line to four, which made every card a different height
104
- and the column read ragged; the full text is on the app's own page anyway. */
105
- p {
106
- color: var(--text-dim); font-size: var(--fs-xs); line-height: 1.45; margin: 3px 0 0;
107
- display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
108
- }
109
- .badge {
110
- font-size: 0.66rem;
111
- font-weight: 700;
112
- text-transform: uppercase;
113
- letter-spacing: 0.04em;
114
- padding: 2px 7px;
115
- border-radius: var(--radius-pill);
116
- }
117
- /* Only NEW is shown now (for a persona's first week). LIVE/beta/soon badges were dropped — every card
118
- in the catalog is live by definition, so a "live" badge said nothing. */
119
- .badge.new { color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }
79
+ /* the persona's canonical icon — a self-contained squircle, one face everywhere */
80
+ .icon {
81
+ flex: none;
82
+ /* 60. It was 72 for a day, on the reasoning that the icons are the only colour on this page — everything
83
+ else is ink on paper — so more of them should read as livelier. It read as too big instead, and that
84
+ turns out to be measurable rather than a matter of taste: the text beside the icon is a fixed 66px (a
85
+ 16px title on a 1.5 line box, 3px, then two clamped bio lines at 13.44/1.45), so at 72 the icon
86
+ overshot the text by six pixels and the icon, not the content, was setting the card's height.
87
+ 64 is the first size that fits inside the text block. 60 gives the same card height as 64 — past the
88
+ crossover the text sets it either way — so the last four pixels cost no vertical space and buy a
89
+ quieter mark. */
90
+ width: 60px;
91
+ height: 60px;
92
+ border-radius: var(--radius-md);
93
+ object-fit: cover;
94
+ box-shadow: 0 8px 20px -12px rgba(0, 0, 0, 0.35);
95
+ }
120
96
 
121
- .chev { flex: none; width: 20px; height: 20px; color: var(--text-dim); transition: transform 0.3s var(--ease-out); }
122
- .card:hover .chev { transform: translateX(3px); color: var(--accent); }
97
+ .meta { flex: 1; min-width: 0; }
98
+ .top { display: flex; align-items: center; gap: 8px; }
99
+ h2 { font-size: var(--fs-base); font-weight: 600; letter-spacing: -0.01em; }
100
+ /* Clamped to two lines. The bios run from one line to four, which made every card a different height
101
+ and the column read ragged; the full text is on the app's own page anyway. */
102
+ p {
103
+ color: var(--text-dim); font-size: var(--fs-xs); line-height: 1.45; margin: 3px 0 0;
104
+ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
105
+ }
106
+ .badge {
107
+ font-size: 0.66rem;
108
+ font-weight: 700;
109
+ text-transform: uppercase;
110
+ letter-spacing: 0.04em;
111
+ padding: 2px 7px;
112
+ border-radius: var(--radius-pill);
123
113
  }
114
+ /* Only NEW is shown now (for a persona's first week). LIVE/beta/soon badges were dropped — every card
115
+ in the catalog is live by definition, so a "live" badge said nothing. */
116
+ .badge.new { color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }
117
+
118
+ .chev { flex: none; width: 20px; height: 20px; color: var(--text-dim); transition: transform 0.3s var(--ease-out); }
119
+ .card:hover .chev { transform: translateX(3px); color: var(--accent); }
124
120
  </style>
@@ -217,68 +217,64 @@ const chrome = face === "page";
217
217
  </div>
218
218
 
219
219
  <style>
220
- /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
221
- out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
222
- @layer justai.components {
223
- /* THE SHELL, scoped under `.pp` — the 26 rules files, me and feed each carried verbatim.
224
- *
225
- * They are written here at the SAME depth the children wrote them, and that is deliberate: a
226
- * child's own `.pp .x` rule ties with this one and wins on order, so a child that needs to differ
227
- * still can, without !important and without knowing anything about this file. */
220
+ /* THE SHELL, scoped under `.pp` — the 26 rules files, me and feed each carried verbatim.
221
+ *
222
+ * They are written here at the SAME depth the children wrote them, and that is deliberate: a
223
+ * child's own `.pp .x` rule ties with this one and wins on order, so a child that needs to differ
224
+ * still can, without !important and without knowing anything about this file. */
228
225
 
229
- .pp { min-height: 100vh; min-height: 100dvh; } /* the pair survives — see @justai/astro-config */
230
- .pp a { color: inherit; text-decoration: none; }
226
+ .pp { min-height: 100vh; min-height: 100dvh; } /* the pair survives — see @justai/astro-config */
227
+ .pp a { color: inherit; text-decoration: none; }
231
228
 
232
- /* the column — the fleet's one presentation at every width */
229
+ /* the column — the fleet's one presentation at every width */
230
+ .pp .app {
231
+ /* Set here AND read with a fallback, which is not belt-and-braces: a consumer that overrides
232
+ `.pp .app` — same selector, later stylesheet — drops the declaration and keeps the reads, and
233
+ an unresolved var() invalidates the whole `padding-inline`. The cut brings its own ground
234
+ (law 1) means the ground has to survive being partly overwritten. */
235
+ --gutter: 16px;
236
+ display: flex; flex-direction: column;
237
+ padding-inline: max(var(--gutter, 16px), env(safe-area-inset-left)) max(var(--gutter, 16px), env(safe-area-inset-right));
238
+ }
239
+ /* 640 is the fleet's one threshold and the fleet's one column width — @justai/ui declares
240
+ `--column`, and a media query cannot read a custom property, so the NUMBER has to be written
241
+ again here. Written again is not decided again: if this ever disagrees with base.css, this is
242
+ the copy that is wrong. */
243
+ @media (min-width: 640px) {
233
244
  .pp .app {
234
- /* Set here AND read with a fallback, which is not belt-and-braces: a consumer that overrides
235
- `.pp .app` — same selector, later stylesheet — drops the declaration and keeps the reads, and
236
- an unresolved var() invalidates the whole `padding-inline`. The cut brings its own ground
237
- (law 1) means the ground has to survive being partly overwritten. */
238
- --gutter: 16px;
239
- display: flex; flex-direction: column;
240
- padding-inline: max(var(--gutter, 16px), env(safe-area-inset-left)) max(var(--gutter, 16px), env(safe-area-inset-right));
241
- }
242
- /* 640 is the fleet's one threshold and the fleet's one column width — @justai/ui declares
243
- `--column`, and a media query cannot read a custom property, so the NUMBER has to be written
244
- again here. Written again is not decided again: if this ever disagrees with base.css, this is
245
- the copy that is wrong. */
246
- @media (min-width: 640px) {
247
- .pp .app {
248
- --gutter: 28px;
249
- max-width: 640px;
250
- max-width: var(--column, 640px);
251
- margin-left: auto; margin-right: auto; margin-inline: auto;
252
- padding-left: 28px; padding-right: 28px; padding-inline: var(--gutter, 28px);
253
- border-left: 1px solid var(--border); border-right: 1px solid var(--border);
254
- border-inline: 1px solid var(--border); background: var(--surface);
255
- backdrop-filter: blur(40px) saturate(1.4); -webkit-backdrop-filter: blur(40px) saturate(1.4);
256
- }
245
+ --gutter: 28px;
246
+ max-width: 640px;
247
+ max-width: var(--column, 640px);
248
+ margin-left: auto; margin-right: auto; margin-inline: auto;
249
+ padding-left: 28px; padding-right: 28px; padding-inline: var(--gutter, 28px);
250
+ border-left: 1px solid var(--border); border-right: 1px solid var(--border);
251
+ border-inline: 1px solid var(--border); background: var(--surface);
252
+ backdrop-filter: blur(40px) saturate(1.4); -webkit-backdrop-filter: blur(40px) saturate(1.4);
257
253
  }
254
+ }
258
255
 
259
- .pp .below { position: relative; }
260
- /* the sticky row means a section reached by its id arrives UNDER it unless it says otherwise */
261
- /* The landing offset is `--shell-top` and only that — measured by the shell, published on :root,
262
- enforced by personaScroll. This line carried a literal 82px through THREE fleet-wide sweeps of
263
- exactly that constant (base.css, Posts.astro, eight persona repos) and surfaced only when the
264
- children's console started warning about it: a scoped rule inside a cut is where a stale number
265
- hides longest, because nobody greps a component for a layout constant. */
266
- .pp #profile, .pp #kernel { scroll-margin-top: var(--shell-top, 78px); }
256
+ .pp .below { position: relative; }
257
+ /* the sticky row means a section reached by its id arrives UNDER it unless it says otherwise */
258
+ /* The landing offset is `--shell-top` and only that — measured by the shell, published on :root,
259
+ enforced by personaScroll. This line carried a literal 82px through THREE fleet-wide sweeps of
260
+ exactly that constant (base.css, Posts.astro, eight persona repos) and surfaced only when the
261
+ children's console started warning about it: a scoped rule inside a cut is where a stale number
262
+ hides longest, because nobody greps a component for a layout constant. */
263
+ .pp #profile, .pp #kernel { scroll-margin-top: var(--shell-top, 78px); }
267
264
 
268
- /* THE `sheet` FACE: no column, no ground, no chrome. The sheet around it IS the frame, and a
269
- second frame inside one is a box in a box. Keyed on the face name rather than on a boolean, so a
270
- third face styles itself by adding a selector instead of by renaming this one. */
271
- .pp[data-face="sheet"] { min-height: 0; }
272
- .pp[data-face="sheet"] .app { max-width: none; margin-inline: 0; border: 0; background: none;
273
- backdrop-filter: none; -webkit-backdrop-filter: none; }
265
+ /* THE `sheet` FACE: no column, no ground, no chrome. The sheet around it IS the frame, and a
266
+ second frame inside one is a box in a box. Keyed on the face name rather than on a boolean, so a
267
+ third face styles itself by adding a selector instead of by renaming this one. */
268
+ .pp[data-face="sheet"] { min-height: 0; }
269
+ .pp[data-face="sheet"] .app { max-width: none; margin-inline: 0; border: 0; background: none;
270
+ backdrop-filter: none; -webkit-backdrop-filter: none; }
274
271
 
275
- /* a mark fills its box; the picture inside it covers */
276
- .pp .mk { display: block; width: 100%; height: 100%; object-fit: cover; }
272
+ /* a mark fills its box; the picture inside it covers */
273
+ .pp .mk { display: block; width: 100%; height: 100%; object-fit: cover; }
277
274
 
278
- /* Motion is never forced on somebody who asked it not to be — and the biggest effects, the ones a
279
- surface is proudest of, are exactly what that setting exists to suppress. */
280
- @media (prefers-reduced-motion: reduce) {
281
- .pp *, .pp *::before, .pp *::after { animation: none !important; transition: none !important; }
282
- }
275
+ /* Motion is never forced on somebody who asked it not to be — and the biggest effects, the ones a
276
+ surface is proudest of, are exactly what that setting exists to suppress. */
277
+ @media (prefers-reduced-motion: reduce) {
278
+ .pp *, .pp *::before, .pp *::after { animation: none !important; transition: none !important; }
283
279
  }
284
280
  </style>
@@ -87,76 +87,72 @@ const { id, title, body, actionLabel, cancelLabel, destructive = false, ask = fa
87
87
  </dialog>
88
88
 
89
89
  <style>
90
- /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
91
- out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
92
- @layer justai.components {
93
- /* The dialog sits in the browser's top layer — above every z-index in every persona, by law. */
94
- .confirm {
95
- /* dock to the bottom like an iOS sheet (native <dialog> centers by default) */
96
- margin: auto auto max(12px, env(safe-area-inset-bottom));
97
- width: min(400px, calc(100vw - 24px));
98
- padding: 0;
99
- border: 0;
100
- background: none;
101
- overflow: visible;
102
- }
103
- .confirm::backdrop {
104
- background: rgba(0, 0, 0, 0.4);
105
- backdrop-filter: blur(4px);
106
- -webkit-backdrop-filter: blur(4px);
107
- }
108
- .confirm[open] .confirm-form { animation: confirm-up 0.4s var(--ease-spring) both; }
109
- .confirm[open]::backdrop { animation: confirm-fade 0.2s var(--ease-out) both; }
90
+ /* The dialog sits in the browser's top layer — above every z-index in every persona, by law. */
91
+ .confirm {
92
+ /* dock to the bottom like an iOS sheet (native <dialog> centers by default) */
93
+ margin: auto auto max(12px, env(safe-area-inset-bottom));
94
+ width: min(400px, calc(100vw - 24px));
95
+ padding: 0;
96
+ border: 0;
97
+ background: none;
98
+ overflow: visible;
99
+ }
100
+ .confirm::backdrop {
101
+ background: rgba(0, 0, 0, 0.4);
102
+ backdrop-filter: blur(4px);
103
+ -webkit-backdrop-filter: blur(4px);
104
+ }
105
+ .confirm[open] .confirm-form { animation: confirm-up 0.4s var(--ease-spring) both; }
106
+ .confirm[open]::backdrop { animation: confirm-fade 0.2s var(--ease-out) both; }
110
107
 
111
- .confirm-form { display: flex; flex-direction: column; gap: 8px; }
112
- .confirm-group {
113
- border-radius: 18px; overflow: hidden; border: 1px solid var(--border);
114
- /* Apple's THICK material, deliberately: at 80% the kernel behind ghosted through the card
115
- (founder-caught on the QR grid). The frosted feel comes from the blur, not from letting
116
- content bleed into the words — a sheet asking for a decision must be legible first. */
117
- background: color-mix(in srgb, var(--bg) 93%, transparent);
118
- backdrop-filter: blur(30px) saturate(1.5); -webkit-backdrop-filter: blur(30px) saturate(1.5);
119
- }
120
- .confirm-msg { padding: 18px 18px 16px; text-align: center; display: flex; flex-direction: column; gap: 5px; }
121
- .confirm-msg strong { font-size: var(--fs-md); font-weight: 700; letter-spacing: -0.01em; color: var(--text); }
122
- .confirm-msg span { font-size: var(--fs-xs); color: var(--text-dim); line-height: 1.45; }
123
- /* a title-less sheet's body is not supporting text under a headline — it IS the voice AND the
124
- title, so it speaks in the primary colour at the native body size (founder: the question
125
- deserves reading size, not caption size) */
126
- .confirm-msg span:only-child { color: var(--text); font-size: var(--fs-base); line-height: 1.5; }
127
- .confirm-act {
128
- width: 100%; padding: 15px; border: 0; border-block-start: 1px solid var(--border);
129
- /* --confirm-accent: the declared hook for personas whose accent is too BRIGHT to read as text
130
- on the frosted card in light mode (cleanmylink's mint, howlong's amber, whatismyip's mint —
131
- the same three that once hooked the old filled consent button). They set a theme-aware
132
- darker tone of their own hue; everyone else says nothing. */
133
- background: none; font: 600 var(--fs-md) var(--font, inherit); color: var(--confirm-accent, var(--accent)); cursor: pointer;
134
- transition: background 0.15s;
135
- }
136
- .confirm-act:active { background: color-mix(in srgb, var(--text) 9%, transparent); }
137
- /* focus is a wash, not a ring: the fleet's 4px focus ring gets CLIPPED by the card's
138
- overflow:hidden into two stray bars — inside a sheet, the focused button fills with a
139
- whisper of its own colour instead (currentColor: accent, red, or text alike) */
140
- .confirm-act:focus-visible { box-shadow: none; outline: none; background: color-mix(in srgb, currentColor 12%, transparent); }
141
- /* iOS system red — the one colour that means "this ends something" on every persona alike */
142
- .confirm-act.destructive { color: var(--danger); }
143
- /* the ASK genre: the invitation is the preferred path (Apple's bold preferredAction)… */
144
- .confirm-act.preferred { font-weight: 700; }
145
- /* …and the safe choice answers in a calm, full-contrast regular voice — equal size, equal tap,
146
- never dimmed (a decline you have to hunt for is not a free choice), just not the emphasis */
147
- .confirm-act.quiet { color: var(--text); font-weight: 400; }
148
- /* the safe choice wears the boldest face, in its own detached card */
149
- .confirm-cancel {
150
- border: 1px solid var(--border); border-radius: 18px;
151
- background: color-mix(in srgb, var(--bg) 93%, transparent);
152
- backdrop-filter: blur(30px) saturate(1.5); -webkit-backdrop-filter: blur(30px) saturate(1.5);
153
- font-weight: 700; color: var(--text);
154
- }
155
- @keyframes confirm-up { from { transform: translateY(20px); opacity: 0; } }
156
- @keyframes confirm-fade { from { opacity: 0; } }
157
- @media (prefers-reduced-motion: reduce) {
158
- .confirm[open] .confirm-form, .confirm[open]::backdrop { animation: none; }
159
- }
108
+ .confirm-form { display: flex; flex-direction: column; gap: 8px; }
109
+ .confirm-group {
110
+ border-radius: 18px; overflow: hidden; border: 1px solid var(--border);
111
+ /* Apple's THICK material, deliberately: at 80% the kernel behind ghosted through the card
112
+ (founder-caught on the QR grid). The frosted feel comes from the blur, not from letting
113
+ content bleed into the words — a sheet asking for a decision must be legible first. */
114
+ background: color-mix(in srgb, var(--bg) 93%, transparent);
115
+ backdrop-filter: blur(30px) saturate(1.5); -webkit-backdrop-filter: blur(30px) saturate(1.5);
116
+ }
117
+ .confirm-msg { padding: 18px 18px 16px; text-align: center; display: flex; flex-direction: column; gap: 5px; }
118
+ .confirm-msg strong { font-size: var(--fs-md); font-weight: 700; letter-spacing: -0.01em; color: var(--text); }
119
+ .confirm-msg span { font-size: var(--fs-xs); color: var(--text-dim); line-height: 1.45; }
120
+ /* a title-less sheet's body is not supporting text under a headline — it IS the voice AND the
121
+ title, so it speaks in the primary colour at the native body size (founder: the question
122
+ deserves reading size, not caption size) */
123
+ .confirm-msg span:only-child { color: var(--text); font-size: var(--fs-base); line-height: 1.5; }
124
+ .confirm-act {
125
+ width: 100%; padding: 15px; border: 0; border-block-start: 1px solid var(--border);
126
+ /* --confirm-accent: the declared hook for personas whose accent is too BRIGHT to read as text
127
+ on the frosted card in light mode (cleanmylink's mint, howlong's amber, whatismyip's mint —
128
+ the same three that once hooked the old filled consent button). They set a theme-aware
129
+ darker tone of their own hue; everyone else says nothing. */
130
+ background: none; font: 600 var(--fs-md) var(--font, inherit); color: var(--confirm-accent, var(--accent)); cursor: pointer;
131
+ transition: background 0.15s;
132
+ }
133
+ .confirm-act:active { background: color-mix(in srgb, var(--text) 9%, transparent); }
134
+ /* focus is a wash, not a ring: the fleet's 4px focus ring gets CLIPPED by the card's
135
+ overflow:hidden into two stray bars — inside a sheet, the focused button fills with a
136
+ whisper of its own colour instead (currentColor: accent, red, or text alike) */
137
+ .confirm-act:focus-visible { box-shadow: none; outline: none; background: color-mix(in srgb, currentColor 12%, transparent); }
138
+ /* iOS system red — the one colour that means "this ends something" on every persona alike */
139
+ .confirm-act.destructive { color: var(--danger); }
140
+ /* the ASK genre: the invitation is the preferred path (Apple's bold preferredAction)… */
141
+ .confirm-act.preferred { font-weight: 700; }
142
+ /* …and the safe choice answers in a calm, full-contrast regular voice — equal size, equal tap,
143
+ never dimmed (a decline you have to hunt for is not a free choice), just not the emphasis */
144
+ .confirm-act.quiet { color: var(--text); font-weight: 400; }
145
+ /* the safe choice wears the boldest face, in its own detached card */
146
+ .confirm-cancel {
147
+ border: 1px solid var(--border); border-radius: 18px;
148
+ background: color-mix(in srgb, var(--bg) 93%, transparent);
149
+ backdrop-filter: blur(30px) saturate(1.5); -webkit-backdrop-filter: blur(30px) saturate(1.5);
150
+ font-weight: 700; color: var(--text);
151
+ }
152
+ @keyframes confirm-up { from { transform: translateY(20px); opacity: 0; } }
153
+ @keyframes confirm-fade { from { opacity: 0; } }
154
+ @media (prefers-reduced-motion: reduce) {
155
+ .confirm[open] .confirm-form, .confirm[open]::backdrop { animation: none; }
160
156
  }
161
157
  </style>
162
158
 
@@ -108,23 +108,19 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
108
108
  </Sheet>
109
109
 
110
110
  <style>
111
- /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
112
- out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
113
- @layer justai.components {
114
- /* The frame is given the sheet's height and takes it — the kernel inside drops its own full-screen
115
- minimum when it is picking, precisely so it fits somebody else's sheet instead of scrolling
116
- inside it. */
117
- .jfp {
118
- display: flex;
119
- min-height: 62dvh;
120
- }
121
- .jfp-frame {
122
- flex: 1;
123
- width: 100%;
124
- border: 0;
125
- background: none;
126
- color-scheme: light dark;
127
- }
111
+ /* The frame is given the sheet's height and takes it — the kernel inside drops its own full-screen
112
+ minimum when it is picking, precisely so it fits somebody else's sheet instead of scrolling
113
+ inside it. */
114
+ .jfp {
115
+ display: flex;
116
+ min-height: 62dvh;
117
+ }
118
+ .jfp-frame {
119
+ flex: 1;
120
+ width: 100%;
121
+ border: 0;
122
+ background: none;
123
+ color-scheme: light dark;
128
124
  }
129
125
  </style>
130
126
 
@@ -77,55 +77,51 @@ const ordered = orderLocales(locales);
77
77
  </div>
78
78
 
79
79
  <style>
80
- /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
81
- out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
82
- @layer justai.components {
83
- /* No `position: relative` any more, and that absence is the point: nothing here is positioned
84
- against the trigger. <Sheet> presents in the top layer, so the picker can no longer be clipped
85
- by an overflow on the toolbar the way an absolutely-positioned dropdown could be. */
86
- .picker { display: contents; }
80
+ /* No `position: relative` any more, and that absence is the point: nothing here is positioned
81
+ against the trigger. <Sheet> presents in the top layer, so the picker can no longer be clipped
82
+ by an overflow on the toolbar the way an absolutely-positioned dropdown could be. */
83
+ .picker { display: contents; }
87
84
 
88
- /* most minimalist: just the 2-letter code, flush to the right edge */
89
- .trigger {
90
- display: inline-grid; place-items: center; min-width: 36px; height: 36px;
91
- font: inherit; font-size: var(--fs-xs); font-weight: 600; letter-spacing: 0.03em;
92
- color: var(--text-dim); background: none; border: none; padding: 0 8px; cursor: pointer;
93
- margin-inline-end: -7px;
94
- transition: color 0.2s, transform 0.2s var(--ease-spring);
95
- }
96
- .trigger:hover { color: var(--text); }
97
- .trigger:active { transform: scale(0.9); }
85
+ /* most minimalist: just the 2-letter code, flush to the right edge */
86
+ .trigger {
87
+ display: inline-grid; place-items: center; min-width: 36px; height: 36px;
88
+ font: inherit; font-size: var(--fs-xs); font-weight: 600; letter-spacing: 0.03em;
89
+ color: var(--text-dim); background: none; border: none; padding: 0 8px; cursor: pointer;
90
+ margin-inline-end: -7px;
91
+ transition: color 0.2s, transform 0.2s var(--ease-spring);
92
+ }
93
+ .trigger:hover { color: var(--text); }
94
+ .trigger:active { transform: scale(0.9); }
98
95
 
99
- /* iOS's search field: a filled pill rather than an outlined box, sitting above the list. */
100
- .search {
101
- width: 100%;
102
- font: inherit; font-size: var(--fs-base); color: var(--text);
103
- background: color-mix(in srgb, var(--text) 7%, transparent);
104
- border: 1px solid transparent; border-radius: 10px;
105
- padding: 10px 12px; outline: none;
106
- margin: 0 0 6px;
107
- }
108
- .search::placeholder { color: var(--text-dim); }
109
- .search:focus { border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
96
+ /* iOS's search field: a filled pill rather than an outlined box, sitting above the list. */
97
+ .search {
98
+ width: 100%;
99
+ font: inherit; font-size: var(--fs-base); color: var(--text);
100
+ background: color-mix(in srgb, var(--text) 7%, transparent);
101
+ border: 1px solid transparent; border-radius: 10px;
102
+ padding: 10px 12px; outline: none;
103
+ margin: 0 0 6px;
104
+ }
105
+ .search::placeholder { color: var(--text-dim); }
106
+ .search:focus { border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
110
107
 
111
- .list { padding-block-end: 2px; }
112
- .item {
113
- display: flex; align-items: center; justify-content: space-between; gap: 12px;
114
- /* 44px — the platform's touch target. The old 9px padding made a mouse's row, and row height is
115
- the clearest single tell of a web dropdown wearing an app's clothes. */
116
- min-height: 44px;
117
- padding: 0 12px; border-radius: 10px;
118
- text-decoration: none; color: var(--text); font-size: var(--fs-sm);
119
- transition: background 0.12s var(--ease-out);
120
- }
121
- /* :active before :hover — a finger never hovers, and the feedback must not be desktop-only. */
122
- .item:active { background: color-mix(in srgb, var(--text) 12%, transparent); }
123
- @media (hover: hover) { .item:hover { background: color-mix(in srgb, var(--text) 8%, transparent); } }
124
- .item:focus-visible { outline: none; background: color-mix(in srgb, var(--accent) 16%, transparent); }
125
- /* The chosen locale reads as TEXT — see ActionRow for why this takes the ink hook. */
126
- .item.active { color: var(--accent-text, var(--accent)); font-weight: 600; }
127
- .item svg { width: 17px; height: 17px; flex: none; }
108
+ .list { padding-block-end: 2px; }
109
+ .item {
110
+ display: flex; align-items: center; justify-content: space-between; gap: 12px;
111
+ /* 44px — the platform's touch target. The old 9px padding made a mouse's row, and row height is
112
+ the clearest single tell of a web dropdown wearing an app's clothes. */
113
+ min-height: 44px;
114
+ padding: 0 12px; border-radius: 10px;
115
+ text-decoration: none; color: var(--text); font-size: var(--fs-sm);
116
+ transition: background 0.12s var(--ease-out);
128
117
  }
118
+ /* :active before :hover — a finger never hovers, and the feedback must not be desktop-only. */
119
+ .item:active { background: color-mix(in srgb, var(--text) 12%, transparent); }
120
+ @media (hover: hover) { .item:hover { background: color-mix(in srgb, var(--text) 8%, transparent); } }
121
+ .item:focus-visible { outline: none; background: color-mix(in srgb, var(--accent) 16%, transparent); }
122
+ /* The chosen locale reads as TEXT — see ActionRow for why this takes the ink hook. */
123
+ .item.active { color: var(--accent-text, var(--accent)); font-weight: 600; }
124
+ .item svg { width: 17px; height: 17px; flex: none; }
129
125
  </style>
130
126
 
131
127
  <script>