@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.
package/src/Posts.astro CHANGED
@@ -160,193 +160,189 @@ const vars = [
160
160
  {RELABEL && <script is:inline set:html={RELABEL}></script>}
161
161
 
162
162
  <style>
163
- /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
164
- out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
165
- @layer justai.components {
166
- /* ---- POSTS cut — a conversation: ONE line descends from the post's avatar and
167
- connects every comment & reply (they're all replies to the post). ---- */
168
- /* the cut owns its own landing offset (was `#kernel, #posts { scroll-margin-top }` in each persona —
169
- which orphaned once #posts moved here and carried this component's scope cid instead of the persona's). */
170
- /* The landing offset is `--shell-top` and only that — see base.css. A hardcoded twin here is a
171
- second opinion the browser can pick when it scrolls to `#posts` by itself. */
172
- .posts { border-block-start: 1px solid var(--border); scroll-margin-top: var(--shell-top); }
173
- /* A post is hoverable because it is a link to its own page, and the hover used to say so with a grey wash
174
- — `--text` at 3.5% — which reads as dimming rather than as lifting. It now uses the card material, so
175
- hovering a post looks like the same gesture as hovering a catalogue card.
163
+ /* ---- POSTS cut — a conversation: ONE line descends from the post's avatar and
164
+ connects every comment & reply (they're all replies to the post). ---- */
165
+ /* the cut owns its own landing offset (was `#kernel, #posts { scroll-margin-top }` in each persona —
166
+ which orphaned once #posts moved here and carried this component's scope cid instead of the persona's). */
167
+ /* The landing offset is `--shell-top` and only that — see base.css. A hardcoded twin here is a
168
+ second opinion the browser can pick when it scrolls to `#posts` by itself. */
169
+ .posts { border-block-start: 1px solid var(--border); scroll-margin-top: var(--shell-top); }
170
+ /* A post is hoverable because it is a link to its own page, and the hover used to say so with a grey wash
171
+ — `--text` at 3.5% — which reads as dimming rather than as lifting. It now uses the card material, so
172
+ hovering a post looks like the same gesture as hovering a catalogue card.
176
173
 
177
- `var(--card-bg, ...)` with the old wash as the fallback ON PURPOSE. Those tokens exist in app.zone only;
178
- the other eleven personas have no catalogue and never defined them, and their column already uses
179
- `--surface` as its own background, so falling back to that would have made the hover invisible. This way
180
- app.zone gets the material and nobody else changes at all, which is the difference between an
181
- improvement and a regression in eleven repos.
174
+ `var(--card-bg, ...)` with the old wash as the fallback ON PURPOSE. Those tokens exist in app.zone only;
175
+ the other eleven personas have no catalogue and never defined them, and their column already uses
176
+ `--surface` as its own background, so falling back to that would have made the hover invisible. This way
177
+ app.zone gets the material and nobody else changes at all, which is the difference between an
178
+ improvement and a regression in eleven repos.
182
179
 
183
- 16px of horizontal padding with a matching negative margin, so the box grows outward and the text does
184
- not move. 16 rather than the 18 above it because the column's own padding is 16px on a phone and 28px
185
- from 768px up: at 18 the box would overflow the narrow case by 2px. Two pixels of asymmetry nobody can
186
- see beats a horizontal scrollbar on a phone.
180
+ 16px of horizontal padding with a matching negative margin, so the box grows outward and the text does
181
+ not move. 16 rather than the 18 above it because the column's own padding is 16px on a phone and 28px
182
+ from 768px up: at 18 the box would overflow the narrow case by 2px. Two pixels of asymmetry nobody can
183
+ see beats a horizontal scrollbar on a phone.
187
184
 
188
- Square corners, stated rather than inherited — a radius here would make it a card, and it is not one:
189
- it spans the column and sits directly against its neighbours. */
190
- .post {
191
- padding-block: 18px;
192
- padding-inline: 16px;
193
- margin-inline: -16px;
194
- border-radius: 0;
195
- cursor: pointer;
196
- transition: background 0.15s var(--ease-out), transform 0.15s var(--ease-out),
197
- border-radius 0.15s var(--ease-out);
198
- }
199
- /* THE FILL LIVES IN ITS OWN PROPERTY, and that is not tidiness — it is the difference between a hover and
200
- no hover. A color-mix over a bare --card-bg (no fallback) written straight into `background` is an
201
- INVALID declaration for a persona that never defined --card-bg, and an invalid declaration is dropped, so
202
- those pages would answer the cursor with nothing at all. Here the fallback sits inside the mix, so the
203
- value is always valid; the defaults mirror app.zone's own tokens per scheme.
204
- (app.zone is the only persona on this line today, but that is a fact about the migration, not a guarantee.)
185
+ Square corners, stated rather than inherited — a radius here would make it a card, and it is not one:
186
+ it spans the column and sits directly against its neighbours. */
187
+ .post {
188
+ padding-block: 18px;
189
+ padding-inline: 16px;
190
+ margin-inline: -16px;
191
+ border-radius: 0;
192
+ cursor: pointer;
193
+ transition: background 0.15s var(--ease-out), transform 0.15s var(--ease-out),
194
+ border-radius 0.15s var(--ease-out);
195
+ }
196
+ /* THE FILL LIVES IN ITS OWN PROPERTY, and that is not tidiness — it is the difference between a hover and
197
+ no hover. A color-mix over a bare --card-bg (no fallback) written straight into `background` is an
198
+ INVALID declaration for a persona that never defined --card-bg, and an invalid declaration is dropped, so
199
+ those pages would answer the cursor with nothing at all. Here the fallback sits inside the mix, so the
200
+ value is always valid; the defaults mirror app.zone's own tokens per scheme.
201
+ (app.zone is the only persona on this line today, but that is a fact about the migration, not a guarantee.)
205
202
 
206
- 55% rather than the full card colour, and no hairline. The full-strength version was measured at 7
207
- luminance steps of lift in light, and it read as a slab competing with the text — the inset hairline was
208
- doing most of that. At 55% the lift is 4 in light and still 9 in dark, so it calms down where it was loud
209
- without disappearing where it was already subtle. 35% was tried and rejected: 2 steps in dark, which on a
210
- bright screen is nothing.
203
+ 55% rather than the full card colour, and no hairline. The full-strength version was measured at 7
204
+ luminance steps of lift in light, and it read as a slab competing with the text — the inset hairline was
205
+ doing most of that. At 55% the lift is 4 in light and still 9 in dark, so it calms down where it was loud
206
+ without disappearing where it was already subtle. 35% was tried and rejected: 2 steps in dark, which on a
207
+ bright screen is nothing.
211
208
 
212
- The radius and the scale are why it now invites rather than announces. They only exist on hover — at rest
213
- the post has no box to round. */
214
- .post:hover {
215
- background: color-mix(in srgb, var(--card-bg, rgba(255, 255, 255, 0.06)) 55%, transparent);
216
- backdrop-filter: var(--card-blur, none);
217
- -webkit-backdrop-filter: var(--card-blur, none);
218
- border-radius: 14px;
219
- transform: scale(1.006);
220
- }
221
- /* The light default is white because that is what --card-bg is there; the dark default is a faint white
222
- overlay because on dark --card-bg equals --surface, which is the column, so copying it would vanish. */
223
- @media (prefers-color-scheme: light) {
224
- :global(html:not([data-theme="dark"])) .post:hover { background: color-mix(in srgb, var(--card-bg, #fff) 55%, transparent); }
225
- }
226
- .post + .post { border-block-start: 1px solid var(--border); }
209
+ The radius and the scale are why it now invites rather than announces. They only exist on hover — at rest
210
+ the post has no box to round. */
211
+ .post:hover {
212
+ background: color-mix(in srgb, var(--card-bg, rgba(255, 255, 255, 0.06)) 55%, transparent);
213
+ backdrop-filter: var(--card-blur, none);
214
+ -webkit-backdrop-filter: var(--card-blur, none);
215
+ border-radius: 14px;
216
+ transform: scale(1.006);
217
+ }
218
+ /* The light default is white because that is what --card-bg is there; the dark default is a faint white
219
+ overlay because on dark --card-bg equals --surface, which is the column, so copying it would vanish. */
220
+ @media (prefers-color-scheme: light) {
221
+ :global(html:not([data-theme="dark"])) .post:hover { background: color-mix(in srgb, var(--card-bg, #fff) 55%, transparent); }
222
+ }
223
+ .post + .post { border-block-start: 1px solid var(--border); }
227
224
 
228
- .trow { display: flex; gap: 12px; }
229
- .avc { flex: none; width: 42px; display: flex; flex-direction: column; align-items: center; }
230
- .ln { width: 2px; flex: 1 1 auto; min-height: 12px; margin-block-start: 6px; background: var(--border); border-radius: var(--radius-pill); }
231
- .cc { flex: 1; min-width: 0; padding-block-end: 14px; }
232
- .trow.last .cc { padding-block-end: 0; }
225
+ .trow { display: flex; gap: 12px; }
226
+ .avc { flex: none; width: 42px; display: flex; flex-direction: column; align-items: center; }
227
+ .ln { width: 2px; flex: 1 1 auto; min-height: 12px; margin-block-start: 6px; background: var(--border); border-radius: var(--radius-pill); }
228
+ .cc { flex: 1; min-width: 0; padding-block-end: 14px; }
229
+ .trow.last .cc { padding-block-end: 0; }
233
230
 
234
- .pa { flex: none; width: 42px; height: 42px; border-radius: var(--radius-sm); display: grid; place-items: center; font-size: 22px; background: var(--feed-av-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
235
- /* A human's avatar: their initial on a disc in their own colour, which arrives from the API as --c.
236
- It was the last flat-filled object in the family, so it takes the house FILL recipe
237
- (app-zone/.justai/expert/how-an-app-zone-icon-is-lit.md): an off-centre radial at 37% 31% with three
238
- stops at L+22 / L+4 / L−11, so the light falls from the upper left and the lower right carries the
239
- shade.
231
+ .pa { flex: none; width: 42px; height: 42px; border-radius: var(--radius-sm); display: grid; place-items: center; font-size: 22px; background: var(--feed-av-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
232
+ /* A human's avatar: their initial on a disc in their own colour, which arrives from the API as --c.
233
+ It was the last flat-filled object in the family, so it takes the house FILL recipe
234
+ (app-zone/.justai/expert/how-an-app-zone-icon-is-lit.md): an off-centre radial at 37% 31% with three
235
+ stops at L+22 / L+4 / L−11, so the light falls from the upper left and the lower right carries the
236
+ shade.
240
237
 
241
- THE SPECULAR IS DELIBERATELY OMITTED, which is the one place this departs from the recipe. The doc
242
- defines it as "what makes it a sphere rather than a lighter disc" — exactly right for howlong's beads,
243
- which are beads, and wrong here. With it the disc read as a ball; without it the same three stops read
244
- as a flat disc with light across it, which is what an avatar should be. Measured on Dana's #0a84ff: the
245
- highlight sits 63 luminance steps above the flat colour instead of 94, and the rim is untouched at L99
246
- either way — only the middle came down.
238
+ THE SPECULAR IS DELIBERATELY OMITTED, which is the one place this departs from the recipe. The doc
239
+ defines it as "what makes it a sphere rather than a lighter disc" — exactly right for howlong's beads,
240
+ which are beads, and wrong here. With it the disc read as a ball; without it the same three stops read
241
+ as a flat disc with light across it, which is what an avatar should be. Measured on Dana's #0a84ff: the
242
+ highlight sits 63 luminance steps above the flat colour instead of 94, and the rim is untouched at L99
243
+ either way — only the middle came down.
247
244
 
248
- The stops are derived in CSS rather than precomputed, because the colour only exists at runtime.
249
- `hsl(from var(--c) h s calc(l ± n))` is the rule stated literally: hue and saturation are passed
250
- through untouched and lightness does all the shading. Measured — lit end 210.2° S100, shaded end
251
- 210.1° S100, so nothing drifts.
245
+ The stops are derived in CSS rather than precomputed, because the colour only exists at runtime.
246
+ `hsl(from var(--c) h s calc(l ± n))` is the rule stated literally: hue and saturation are passed
247
+ through untouched and lightness does all the shading. Measured — lit end 210.2° S100, shaded end
248
+ 210.1° S100, so nothing drifts.
252
249
 
253
- NO % ON THOSE NUMBERS. Inside relative colour syntax `l` resolves to a <number>, so `calc(l + 22%)`
254
- mixes types, the declaration is dropped, and the disc silently renders flat — it looked finished and
255
- measured as zero light-to-shadow difference until the units came off. `background-color` is kept as a
256
- separate declaration for the same reason: where the syntax is unsupported the disc loses its light
257
- rather than disappearing.
250
+ NO % ON THOSE NUMBERS. Inside relative colour syntax `l` resolves to a <number>, so `calc(l + 22%)`
251
+ mixes types, the declaration is dropped, and the disc silently renders flat — it looked finished and
252
+ measured as zero light-to-shadow difference until the units came off. `background-color` is kept as a
253
+ separate declaration for the same reason: where the syntax is unsupported the disc loses its light
254
+ rather than disappearing.
258
255
 
259
- `max(0, 39 - l)` is the doc's L28 floor as a LIFT applied to all three stops, not a clamp on the dark
260
- end: clamping compresses the spread and weakens the emboss unevenly. 39 is the shortfall threshold
261
- (28 + 11), so a colour light enough to clear it adds nothing. */
262
- .uav {
263
- flex: none; width: 30px; height: 30px; border-radius: 50%;
264
- display: grid; place-items: center; font-size: 13px; font-weight: 600; color: #fff;
265
- background-color: var(--c, #888);
266
- background-image:
267
- radial-gradient(ellipse 80% 80% at 37% 31%,
268
- hsl(from var(--c, #888) h s calc(l + 22 + max(0, 39 - l))) 0%,
269
- hsl(from var(--c, #888) h s calc(l + 4 + max(0, 39 - l))) 40%,
270
- hsl(from var(--c, #888) h s calc(l - 11 + max(0, 39 - l))) 100%);
271
- }
272
- .rav { flex: none; width: 30px; height: 30px; border-radius: var(--radius-sm); display: grid; place-items: center; font-size: 15px; background: var(--feed-av-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
273
- .pa :global(svg), .rav :global(svg) { display: block; }
274
- .pa .mk, .rav .mk { width: 100%; height: 100%; object-fit: cover; display: block; }
275
- /* a dark tile must invert in light mode, not stay dark — personas whose avatar ground is a gradient
276
- pass avatarBgLight; the rest fall back to their single background. */
277
- @media (prefers-color-scheme: light) { :global(html:not([data-theme="dark"])) .pa, :global(html:not([data-theme="dark"])) .rav { background: var(--feed-av-bg-light, var(--feed-av-bg, #fff)); } }
256
+ `max(0, 39 - l)` is the doc's L28 floor as a LIFT applied to all three stops, not a clamp on the dark
257
+ end: clamping compresses the spread and weakens the emboss unevenly. 39 is the shortfall threshold
258
+ (28 + 11), so a colour light enough to clear it adds nothing. */
259
+ .uav {
260
+ flex: none; width: 30px; height: 30px; border-radius: 50%;
261
+ display: grid; place-items: center; font-size: 13px; font-weight: 600; color: #fff;
262
+ background-color: var(--c, #888);
263
+ background-image:
264
+ radial-gradient(ellipse 80% 80% at 37% 31%,
265
+ hsl(from var(--c, #888) h s calc(l + 22 + max(0, 39 - l))) 0%,
266
+ hsl(from var(--c, #888) h s calc(l + 4 + max(0, 39 - l))) 40%,
267
+ hsl(from var(--c, #888) h s calc(l - 11 + max(0, 39 - l))) 100%);
268
+ }
269
+ .rav { flex: none; width: 30px; height: 30px; border-radius: var(--radius-sm); display: grid; place-items: center; font-size: 15px; background: var(--feed-av-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
270
+ .pa :global(svg), .rav :global(svg) { display: block; }
271
+ .pa .mk, .rav .mk { width: 100%; height: 100%; object-fit: cover; display: block; }
272
+ /* a dark tile must invert in light mode, not stay dark — personas whose avatar ground is a gradient
273
+ pass avatarBgLight; the rest fall back to their single background. */
274
+ @media (prefers-color-scheme: light) { :global(html:not([data-theme="dark"])) .pa, :global(html:not([data-theme="dark"])) .rav { background: var(--feed-av-bg-light, var(--feed-av-bg, #fff)); } }
278
275
 
279
- /* A bare mark stands on the feed's own ground, so the tile and its hairline have to go — and they have to
280
- go from the CONTAINER, which is why this reads upward from the image with :has(). Scoping it to the image
281
- is what keeps a sibling persona's tiled icon tiled: .rav wraps their reply as well as a self-reply.
282
- `contain` rather than `cover`, because a mark with transparent margins must not be cropped to fill. */
283
- .pa:has(.mk.bare), .rav:has(.mk.bare) {
284
- background: none; border-color: transparent;
285
- /* The slot rounds its corners and clips, which is right for a tile whose own corners are round and wrong
286
- for a mark with no tile: the arms reach the corners, so the radius sliced their round outer tips flat.
287
- Measured before the fix — 27.5px wide against 28.0 tall inside a square 30px slot, the asymmetry being
288
- the clipping showing up as numbers. Nothing is left to clip once the mark is inset below, but both go
289
- anyway, or a later change to the inset brings the slicing back silently. */
290
- border-radius: 0; overflow: visible;
291
- }
292
- /* 72%, measured rather than judged. Filling the slot, the arms painted 92% of it; a sibling's tiled icon
293
- paints its glyph across 61%, because the glyph sits inset within its own tile. So app.zone carried half
294
- again the painted width of every persona it was replying to, in a thread where it is one speaker among
295
- peers rather than the platform above them. At 72% the arms land near 67% — a shade above the siblings,
296
- which suits a bracket form being lighter than a filled ring. The feed permalink page uses the same number
297
- for the same reason; the two surfaces render one conversation and must not disagree about it. */
298
- .pa .mk.bare, .rav .mk.bare { width: 72%; height: 72%; object-fit: contain; }
299
- /* The scheme pair: the page decides, because the SVG's own media query does not survive <img>.
300
- Written as `.pa .mk.av-*` rather than `.av-*` deliberately — `.pa .mk { display: block }` above is two
301
- classes deep, so a single-class `.av-light { display: none }` loses on specificity and the toggle silently
302
- does nothing. Measured: it showed the pale-ground file in both schemes until these selectors matched. */
303
- .pa .mk.av-dark, .rav .mk.av-dark { display: none; }
304
- @media (prefers-color-scheme: dark) {
305
- .pa .mk.av-light, .rav .mk.av-light { display: none; }
306
- .pa .mk.av-dark, .rav .mk.av-dark { display: block; }
307
- }
308
- /* The dark fork's data-theme twin: a light-system page forced dark must swap the marks too. */
309
- :global(html[data-theme="dark"]) .pa .mk.av-light, :global(html[data-theme="dark"]) .rav .mk.av-light { display: none; }
310
- :global(html[data-theme="dark"]) .pa .mk.av-dark, :global(html[data-theme="dark"]) .rav .mk.av-dark { display: block; }
276
+ /* A bare mark stands on the feed's own ground, so the tile and its hairline have to go — and they have to
277
+ go from the CONTAINER, which is why this reads upward from the image with :has(). Scoping it to the image
278
+ is what keeps a sibling persona's tiled icon tiled: .rav wraps their reply as well as a self-reply.
279
+ `contain` rather than `cover`, because a mark with transparent margins must not be cropped to fill. */
280
+ .pa:has(.mk.bare), .rav:has(.mk.bare) {
281
+ background: none; border-color: transparent;
282
+ /* The slot rounds its corners and clips, which is right for a tile whose own corners are round and wrong
283
+ for a mark with no tile: the arms reach the corners, so the radius sliced their round outer tips flat.
284
+ Measured before the fix — 27.5px wide against 28.0 tall inside a square 30px slot, the asymmetry being
285
+ the clipping showing up as numbers. Nothing is left to clip once the mark is inset below, but both go
286
+ anyway, or a later change to the inset brings the slicing back silently. */
287
+ border-radius: 0; overflow: visible;
288
+ }
289
+ /* 72%, measured rather than judged. Filling the slot, the arms painted 92% of it; a sibling's tiled icon
290
+ paints its glyph across 61%, because the glyph sits inset within its own tile. So app.zone carried half
291
+ again the painted width of every persona it was replying to, in a thread where it is one speaker among
292
+ peers rather than the platform above them. At 72% the arms land near 67% — a shade above the siblings,
293
+ which suits a bracket form being lighter than a filled ring. The feed permalink page uses the same number
294
+ for the same reason; the two surfaces render one conversation and must not disagree about it. */
295
+ .pa .mk.bare, .rav .mk.bare { width: 72%; height: 72%; object-fit: contain; }
296
+ /* The scheme pair: the page decides, because the SVG's own media query does not survive <img>.
297
+ Written as `.pa .mk.av-*` rather than `.av-*` deliberately — `.pa .mk { display: block }` above is two
298
+ classes deep, so a single-class `.av-light { display: none }` loses on specificity and the toggle silently
299
+ does nothing. Measured: it showed the pale-ground file in both schemes until these selectors matched. */
300
+ .pa .mk.av-dark, .rav .mk.av-dark { display: none; }
301
+ @media (prefers-color-scheme: dark) {
302
+ .pa .mk.av-light, .rav .mk.av-light { display: none; }
303
+ .pa .mk.av-dark, .rav .mk.av-dark { display: block; }
304
+ }
305
+ /* The dark fork's data-theme twin: a light-system page forced dark must swap the marks too. */
306
+ :global(html[data-theme="dark"]) .pa .mk.av-light, :global(html[data-theme="dark"]) .rav .mk.av-light { display: none; }
307
+ :global(html[data-theme="dark"]) .pa .mk.av-dark, :global(html[data-theme="dark"]) .rav .mk.av-dark { display: block; }
311
308
 
312
- .ph { display: flex; align-items: center; gap: 5px; font-size: var(--fs-xs); flex-wrap: wrap; }
313
- .ph b { font-weight: 600; }
314
- .phandle, .dot, .pt { color: var(--text-dim); font-weight: 400; }
315
- /* a persona's name in the feed links to its profile (X-style); humans have no link */
316
- .pn { color: inherit; text-decoration: none; }
317
- /* `a.pn`, not `.pn` — a persona whose home we were not told renders as a span, and a name that
318
- lights up under the cursor while going nowhere is a promise the markup cannot keep. */
319
- a.pn:hover b { color: var(--accent-text, var(--accent)); }
320
- a.pt { text-decoration: none; }
321
- a.pt:hover { text-decoration: underline; }
322
- .pbd { font-size: var(--fs-sm); line-height: 1.5; margin-block-start: 4px; }
323
- /* The translation notice — dim, small, directly under the words it is about, and carrying the ONE
324
- link back to what the persona actually wrote, so the link is the only lit thing in the row.
325
- Matches the feed permalink page exactly: the two surfaces render one conversation. */
326
- .tnote { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; margin-block-start: 6px; font-size: var(--fs-2xs); color: var(--text-dim); }
327
- .tnote a { color: var(--confirm-accent, var(--accent)); text-decoration: none; }
328
- .tnote a:hover { text-decoration: underline; }
329
- .pact { display: flex; gap: 22px; margin-block-start: 12px; color: var(--text-dim); }
330
- .pact svg { width: 17px; height: 17px; }
331
- .pact span { cursor: pointer; transition: color 0.2s; }
332
- .pact span:hover { color: var(--accent-text, var(--accent)); }
333
- .ch { display: flex; align-items: center; gap: 4px; font-size: var(--fs-xs); }
334
- .ch b { font-weight: 600; }
335
- .ch .muted { color: var(--text-dim); font-weight: 400; }
336
- .ck { width: 14px; height: 14px; flex: none; }
337
- .cb { font-size: var(--fs-xs); line-height: 1.45; margin-block-start: 2px; }
309
+ .ph { display: flex; align-items: center; gap: 5px; font-size: var(--fs-xs); flex-wrap: wrap; }
310
+ .ph b { font-weight: 600; }
311
+ .phandle, .dot, .pt { color: var(--text-dim); font-weight: 400; }
312
+ /* a persona's name in the feed links to its profile (X-style); humans have no link */
313
+ .pn { color: inherit; text-decoration: none; }
314
+ /* `a.pn`, not `.pn` — a persona whose home we were not told renders as a span, and a name that
315
+ lights up under the cursor while going nowhere is a promise the markup cannot keep. */
316
+ a.pn:hover b { color: var(--accent-text, var(--accent)); }
317
+ a.pt { text-decoration: none; }
318
+ a.pt:hover { text-decoration: underline; }
319
+ .pbd { font-size: var(--fs-sm); line-height: 1.5; margin-block-start: 4px; }
320
+ /* The translation notice — dim, small, directly under the words it is about, and carrying the ONE
321
+ link back to what the persona actually wrote, so the link is the only lit thing in the row.
322
+ Matches the feed permalink page exactly: the two surfaces render one conversation. */
323
+ .tnote { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; margin-block-start: 6px; font-size: var(--fs-2xs); color: var(--text-dim); }
324
+ .tnote a { color: var(--confirm-accent, var(--accent)); text-decoration: none; }
325
+ .tnote a:hover { text-decoration: underline; }
326
+ .pact { display: flex; gap: 22px; margin-block-start: 12px; color: var(--text-dim); }
327
+ .pact svg { width: 17px; height: 17px; }
328
+ .pact span { cursor: pointer; transition: color 0.2s; }
329
+ .pact span:hover { color: var(--accent-text, var(--accent)); }
330
+ .ch { display: flex; align-items: center; gap: 4px; font-size: var(--fs-xs); }
331
+ .ch b { font-weight: 600; }
332
+ .ch .muted { color: var(--text-dim); font-weight: 400; }
333
+ .ck { width: 14px; height: 14px; flex: none; }
334
+ .cb { font-size: var(--fs-xs); line-height: 1.45; margin-block-start: 2px; }
338
335
 
339
- /* the maker's signature — every persona signs off with justai.pro */
340
- .z-foot { text-align: center; color: var(--text-dim); font-size: var(--fs-2xs); padding-block: 24px max(16px, env(safe-area-inset-bottom)); }
341
- /* Underlined, deliberately: a link inside body text must not rely on colour alone to be seen
342
- (WCAG link-in-text-block — the twins' quality gate flagged it where accent-vs-dim contrast ran
343
- thin). The offset keeps it quiet; the ability to see it does not depend on your colour vision. */
344
- /* --confirm-accent, then --accent: the raw accent is a BRAND colour, not a text colour — at this
345
- size on a light ground Apple blue reads 3.2:1. The fleet already solved this once, for the
346
- sheet: every persona carries a hue-preserving darkened accent that clears 4.5 as text. A link
347
- is text; it uses the text-grade accent. */
348
- .z-foot a { color: var(--confirm-accent, var(--accent)); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
349
- }
336
+ /* the maker's signature — every persona signs off with justai.pro */
337
+ .z-foot { text-align: center; color: var(--text-dim); font-size: var(--fs-2xs); padding-block: 24px max(16px, env(safe-area-inset-bottom)); }
338
+ /* Underlined, deliberately: a link inside body text must not rely on colour alone to be seen
339
+ (WCAG link-in-text-block — the twins' quality gate flagged it where accent-vs-dim contrast ran
340
+ thin). The offset keeps it quiet; the ability to see it does not depend on your colour vision. */
341
+ /* --confirm-accent, then --accent: the raw accent is a BRAND colour, not a text colour — at this
342
+ size on a light ground Apple blue reads 3.2:1. The fleet already solved this once, for the
343
+ sheet: every persona carries a hue-preserving darkened accent that clears 4.5 as text. A link
344
+ is text; it uses the text-grade accent. */
345
+ .z-foot a { color: var(--confirm-accent, var(--accent)); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
350
346
  </style>
351
347
 
352
348
  <script>