@aparte/core 0.16.7 → 0.16.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/components/bubble/aparte-chat-bubble.d.ts +33 -18
  2. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  3. package/dist/components/chat/aparte-chat.d.ts +32 -25
  4. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  5. package/dist/components/composer/aparte-composer-action.d.ts +1 -1
  6. package/dist/components/composer/aparte-composer-add-attachment.d.ts +1 -1
  7. package/dist/components/composer/aparte-composer-attachments.d.ts +1 -1
  8. package/dist/components/composer/aparte-composer-cancel.d.ts +2 -2
  9. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  10. package/dist/components/composer/aparte-composer-input.d.ts +6 -6
  11. package/dist/components/composer/aparte-composer-send.d.ts +3 -3
  12. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  13. package/dist/components/composer/aparte-composer.d.ts +9 -2
  14. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  15. package/dist/components/context/aparte-context.d.ts +0 -1
  16. package/dist/components/context/aparte-context.d.ts.map +1 -1
  17. package/dist/components/conversation-list/aparte-conversation-list.d.ts +6 -6
  18. package/dist/components/elicitation/aparte-elicitation.d.ts +20 -18
  19. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  20. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts +9 -2
  21. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
  22. package/dist/components/sidebar/aparte-sidebar.d.ts +16 -2
  23. package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -1
  24. package/dist/components/split/aparte-split.d.ts +9 -4
  25. package/dist/components/split/aparte-split.d.ts.map +1 -1
  26. package/dist/components/status/aparte-chat-status.d.ts +3 -2
  27. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  28. package/dist/components/viewport/aparte-chat-viewport.d.ts +27 -4
  29. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  30. package/dist/config/icon-provider.d.ts +6 -0
  31. package/dist/config/icon-provider.d.ts.map +1 -1
  32. package/dist/config/locale.d.ts +3 -1
  33. package/dist/config/locale.d.ts.map +1 -1
  34. package/dist/custom-elements.json +11304 -11171
  35. package/dist/{dialog-triggers-Dw_DIlQ7.js → dialog-triggers-D8wNICrT.js} +24 -19
  36. package/dist/dialog-triggers-D8wNICrT.js.map +1 -0
  37. package/dist/generated/element-attributes.d.ts +13 -3
  38. package/dist/generated/element-attributes.d.ts.map +1 -1
  39. package/dist/{glyphs-DQTeOj4w.js → glyphs-C8z2oJUg.js} +36 -27
  40. package/dist/{glyphs-DQTeOj4w.js.map → glyphs-C8z2oJUg.js.map} +1 -1
  41. package/dist/icons/extended.d.ts +0 -6
  42. package/dist/icons/extended.d.ts.map +1 -1
  43. package/dist/icons/glyphs.d.ts +13 -4
  44. package/dist/icons/glyphs.d.ts.map +1 -1
  45. package/dist/icons.js +28 -31
  46. package/dist/icons.js.map +1 -1
  47. package/dist/index.css +831 -359
  48. package/dist/index.js +176 -54
  49. package/dist/index.js.map +1 -1
  50. package/dist/index.node.js +1 -1
  51. package/dist/primitives/icon/aparte-icon.d.ts +1 -1
  52. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts +2 -3
  53. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts.map +1 -1
  54. package/dist/primitives/select/aparte-optgroup.d.ts +1 -1
  55. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  56. package/dist/primitives/select/aparte-option.d.ts +4 -4
  57. package/dist/primitives/select/aparte-option.d.ts.map +1 -1
  58. package/dist/primitives/select/aparte-select.d.ts +21 -12
  59. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  60. package/dist/renderers/segments/code.d.ts.map +1 -1
  61. package/dist/renderers/segments/error.d.ts.map +1 -1
  62. package/dist/renderers/segments/thinking.d.ts.map +1 -1
  63. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  64. package/package.json +2 -2
  65. package/dist/dialog-triggers-Dw_DIlQ7.js.map +0 -1
package/dist/index.css CHANGED
@@ -183,7 +183,6 @@
183
183
  below (it was a frozen plum hex that outlived every rebrand); the assistant is
184
184
  plain prose (AI-chat convention) — flip it to get a two-bubble chat-app look. */
185
185
  --aparte-message-content-bg-assistant: transparent;
186
- --aparte-message-content-radius: 14px;
187
186
 
188
187
  /* Aliases used by some segment renderers (artifact card, tool pill).
189
188
  Mapped to existing tokens so consumers that override --aparte-surface-1/2
@@ -206,8 +205,6 @@
206
205
  --aparte-thumb-remove-bg-hover: rgba(0, 0, 0, 0.85);
207
206
  --aparte-thumb-remove-color: #ffffff;
208
207
 
209
- /* Action Bar */
210
- --aparte-action-bar-btn-size: 28px;
211
208
  /* The mark (display/mark.css): how much of its intent a marked row's ground takes,
212
209
  and how wide the bar on its start edge is. One pair for the select's chosen option,
213
210
  the checked field choice, the active conversation and an answered question. */
@@ -215,12 +212,16 @@
215
212
  --aparte-mark-bar: 2px;
216
213
 
217
214
  /* Branch Picker */
218
- --aparte-branch-picker-btn-size: 20px;
219
- --aparte-branch-picker-btn-icon-size: 16px;
220
215
  --aparte-branch-picker-label-min-width: 32px;
221
216
 
222
217
  /* Avatars */
223
218
  --aparte-avatar-size: 32px;
219
+ /* Fractions of the size, computed on the element (display/avatar.css): the initials'
220
+ type, the corner and the group's overlap follow the ramp instead of sitting at one
221
+ absolute value for 20px and 56px alike. */
222
+ --aparte-avatar-initials-ratio: 0.34;
223
+ --aparte-avatar-radius-ratio: 0.28;
224
+ --aparte-avatar-overlap-ratio: 0.28;
224
225
  --aparte-avatar-image-user: none;
225
226
  --aparte-avatar-image-assistant: none;
226
227
  --aparte-avatar-image-size: 90%;
@@ -230,21 +231,18 @@
230
231
  /* Input */
231
232
  --aparte-input-editor-max-height: 200px;
232
233
 
233
- /* Icons — one knob, inherited: a container declares it, every glyph below follows. */
234
- --aparte-icon-size: 14px;
235
- --aparte-icon-size-sm: 12px;
236
- --aparte-icon-size-lg: 16px;
237
- --aparte-icon-size-xl: 20px;
234
+ /* Icons: one knob, inherited — a container declares `--aparte-icon-size` and every
235
+ glyph below follows. The steps are on the type scale, in the derived layer. */
238
236
 
239
- /* Send button + inline action buttons */
240
- --aparte-send-btn-size: 36px;
241
- --aparte-input-action-btn-size: 36px;
237
+ /* Send button + inline action buttons (their size is the lg step, in the derived layer) */
242
238
  --aparte-input-action-btn-icon-size: 20px;
243
239
  --aparte-model-select-min-width: 120px;
244
240
 
245
241
  /* Status Indicator */
246
242
  --aparte-status-bg: transparent;
247
243
  --aparte-status-dot-size: 6px;
244
+ /* The floor of every pulsing dot's opacity: above the perception threshold. */
245
+ --aparte-pulse-floor: 0.55;
248
246
 
249
247
  /* Waiting Indicator (the bubble's own dots, before the first token) */
250
248
  --aparte-waiting-height: 1.5em;
@@ -252,7 +250,6 @@
252
250
  /* Thinking Segment */
253
251
  --aparte-thinking-content-max-height: 300px;
254
252
  --aparte-thinking-content-line-height: 1.55;
255
- --aparte-thinking-toggle-size: 9px;
256
253
  --aparte-thinking-rail-width: 2px;
257
254
 
258
255
  /* Elicitation — the panel a tool's question is asked in.
@@ -282,8 +279,7 @@
282
279
  --aparte-code-copy-icon-size: 14px;
283
280
  --aparte-code-inline-font-size: 0.875em;
284
281
 
285
- /* Scroll-to-bottom button */
286
- --aparte-scroll-btn-size: 36px;
282
+ /* Scroll-to-bottom button (its size is the lg step, in the derived layer) */
287
283
  --aparte-scroll-btn-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
288
284
 
289
285
  /* Error segment */
@@ -291,8 +287,6 @@
291
287
  --aparte-error-border: #fecaca;
292
288
  --aparte-error-solid: #dc2626;
293
289
  --aparte-error-text: #991b1b;
294
- --aparte-error-title: #7f1d1d;
295
- --aparte-error-icon-size: 20px;
296
290
  --aparte-error-rail-width: 4px;
297
291
 
298
292
  /* Prose (aparte-segment-content) */
@@ -327,12 +321,9 @@
327
321
 
328
322
  /* Input (composer) */
329
323
 
330
- /* Conversation List */
331
- --aparte-conv-action-btn-size: 20px;
332
324
 
333
325
  /* Scroll rail — the ticks beside the transcript. Off-scale on purpose: a 2px line
334
326
  and a 14px mark are a minimap's measures, not spacing steps. */
335
- --aparte-scroll-rail-width: 20px;
336
327
  --aparte-scroll-rail-tick-size: 14px;
337
328
  --aparte-scroll-rail-tick-thickness: 2px;
338
329
 
@@ -341,7 +332,6 @@
341
332
  /* Context gauge, ring variant — a diameter, and a stroke in the ring's own 36-unit
342
333
  box. Literals like the rail's: a 22px ring is a toolbar's measure, not a spacing step. */
343
334
  --aparte-context-ring-size: 22px;
344
- --aparte-context-ring-stroke: 4;
345
335
 
346
336
  /* Shell — the sidebar's column, the header's row and the split's seam. */
347
337
  --aparte-sidebar-width: 260px;
@@ -353,9 +343,15 @@
353
343
  is 12px because that is a target, and it grows to the touch target on a coarse
354
344
  pointer (responsive.css). */
355
345
  --aparte-split-position: 38%;
356
- --aparte-split-min: 20rem;
346
+ /* Never more than the container has: at 320px this floor was wider than a phone, so
347
+ the primary pane took everything and the end pane was annihilated to 0. */
348
+ --aparte-split-min: min(20rem, 100%);
357
349
  --aparte-split-max: 60%;
358
- --aparte-split-handle-size: 4px;
350
+ /* The seam's TRACK — the grab zone, and what the grid reserves. The painted line
351
+ inside it is `--aparte-split-seam-width` (derived layer): one token used to size
352
+ both, so the line could not be thinned without moving the layout, and at 4px it
353
+ was four times the kit's rule with no grip — an interaction drawn as a decoration. */
354
+ --aparte-split-handle-size: 12px;
359
355
  --aparte-split-hit-area: 12px;
360
356
  /* Sizes that are not steps of a scale. Each is off-scale on purpose — a 40px
361
357
  file tile is a tile, not fourteen spacing units — so each gets a name rather
@@ -406,6 +402,9 @@
406
402
  --aparte-menu-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
407
403
  --aparte-popover-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 10px 28px rgba(0, 0, 0, 0.16);
408
404
  --aparte-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
405
+ /* The composer laid over the transcript (overlay-composer): a thing that floats has to
406
+ be seen floating, and its ground is the transcript's own. */
407
+ --aparte-composer-overlay-shadow: 0 -4px 16px rgba(0, 0, 0, 0.06);
409
408
  --aparte-switch-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
410
409
  --aparte-range-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
411
410
 
@@ -444,11 +443,28 @@
444
443
  --aparte-skeleton-text-last-width: 60%;
445
444
  --aparte-progress-indeterminate-width: 40%;
446
445
 
446
+ /* The control height scale — four steps, and EVERY control reads one: a text
447
+ button used to have no height (23px of padding and line-height) beside 24, 29, 32,
448
+ 36 and 44px neighbours, and the 36px family (send, the input's action button, the
449
+ scroll button) lived off the scale as literals. sm is the small control (action bar,
450
+ branch arrows, a row's ⋯), md the default button, lg the field and the composer's
451
+ controls at rest, xl the large button. */
447
452
  --aparte-btn-size-sm: 24px;
448
453
  --aparte-btn-size-md: 32px;
449
- --aparte-btn-size-lg: 40px;
454
+ --aparte-btn-size-lg: 36px;
455
+ --aparte-btn-size-xl: 40px;
456
+ /* The field family's literal sizes, beside the button's: the derived knobs
457
+ (paddings, radius) sit in the anchored layer below, like the button's own. */
458
+ --aparte-field-textarea-min-height: 80px;
459
+ --aparte-checkbox-size: 18px;
460
+ --aparte-radio-size: 18px;
461
+ --aparte-radio-dot-size: 8px;
462
+ --aparte-switch-width: 40px;
463
+ --aparte-switch-height: 22px;
464
+ --aparte-switch-thumb-inset: 2px;
465
+ --aparte-range-thumb-size: 16px;
466
+ --aparte-range-track-height: 4px;
450
467
 
451
- --aparte-focus-outline-offset: -2px;
452
468
  --aparte-tool-spinner-size: 10px;
453
469
  --aparte-model-select-chevron-room: 28px;
454
470
  }
@@ -482,6 +498,9 @@
482
498
  --aparte-scroll-btn-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
483
499
  --aparte-select-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 10px 28px rgba(0, 0, 0, 0.55);
484
500
  --aparte-menu-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
501
+ --aparte-composer-overlay-shadow: 0 -4px 16px rgba(0, 0, 0, 0.4);
502
+ /* Declared once at :root it darkened a dark page by 8/255: the modal state vanished. */
503
+ --aparte-scrim: rgba(0, 0, 0, 0.6);
485
504
  --aparte-popover-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 10px 28px rgba(0, 0, 0, 0.55);
486
505
  --aparte-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
487
506
  --aparte-switch-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
@@ -491,7 +510,6 @@
491
510
  --aparte-error-border: #7f1d1d;
492
511
  --aparte-error-solid: #f87171;
493
512
  --aparte-error-text: #fca5a5;
494
- --aparte-error-title: #fecaca;
495
513
  }
496
514
  }
497
515
 
@@ -521,6 +539,10 @@
521
539
  --aparte-menu-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
522
540
  --aparte-popover-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 10px 28px rgba(0, 0, 0, 0.16);
523
541
  --aparte-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
542
+ /* The composer laid over the transcript (overlay-composer): a thing that floats has to
543
+ be seen floating, and its ground is the transcript's own. */
544
+ --aparte-composer-overlay-shadow: 0 -4px 16px rgba(0, 0, 0, 0.06);
545
+ --aparte-scrim: rgba(0, 0, 0, 0.35);
524
546
  --aparte-switch-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
525
547
  --aparte-range-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
526
548
  --aparte-ink-l: 0.40;
@@ -528,7 +550,6 @@
528
550
  --aparte-error-border: #fecaca;
529
551
  --aparte-error-solid: #dc2626;
530
552
  --aparte-error-text: #991b1b;
531
- --aparte-error-title: #7f1d1d;
532
553
  }
533
554
 
534
555
 
@@ -568,6 +589,8 @@
568
589
  --aparte-scroll-btn-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
569
590
  --aparte-select-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 10px 28px rgba(0, 0, 0, 0.55);
570
591
  --aparte-menu-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
592
+ --aparte-composer-overlay-shadow: 0 -4px 16px rgba(0, 0, 0, 0.4);
593
+ --aparte-scrim: rgba(0, 0, 0, 0.6);
571
594
  --aparte-popover-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 10px 28px rgba(0, 0, 0, 0.55);
572
595
  --aparte-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
573
596
  --aparte-switch-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
@@ -587,7 +610,6 @@
587
610
  --aparte-error-border: #7f1d1d;
588
611
  --aparte-error-solid: #f87171;
589
612
  --aparte-error-text: #fca5a5;
590
- --aparte-error-title: #fecaca;
591
613
  }
592
614
 
593
615
  /* =========================================
@@ -653,8 +675,14 @@ aparte-chat {
653
675
  --aparte-font-size-xs: calc(0.6875rem * var(--aparte-font-scale));
654
676
  --aparte-font-size-sm: calc(0.75rem * var(--aparte-font-scale));
655
677
  --aparte-font-size-md: calc(0.8125rem * var(--aparte-font-scale));
656
- --aparte-font-size-lg: calc(0.875rem * var(--aparte-font-scale));
657
678
  --aparte-font-size-base: calc(0.9375rem * var(--aparte-font-scale));
679
+ /* Above the body, in the order the names say. `lg` used to be 0.875rem — SMALLER than
680
+ `base` — with nothing above it, so a title and a placeholder shared a size and a
681
+ card's title was smaller than its body. A scale whose name lies is worse than a
682
+ short one. */
683
+ --aparte-font-size-lg: calc(1.0625rem * var(--aparte-font-scale));
684
+ --aparte-font-size-xl: calc(1.25rem * var(--aparte-font-scale));
685
+ --aparte-font-size-2xl: calc(1.5rem * var(--aparte-font-scale));
658
686
 
659
687
  /* Elicitation and the conversation item, on the scale.
660
688
  They were literals off every step — 0.76 / 0.78 / 0.8 / 0.82rem and a `7px 10px`
@@ -680,7 +708,6 @@ aparte-chat {
680
708
  vertical half (#56) — a shorthand cannot be read by parts. */
681
709
  --aparte-message-padding-block: var(--aparte-space-8);
682
710
  --aparte-message-padding-inline: var(--aparte-space-6);
683
- --aparte-message-padding: var(--aparte-message-padding-block) var(--aparte-message-padding-inline);
684
711
  --aparte-message-content-padding: var(--aparte-space-5) var(--aparte-space-7);
685
712
  --aparte-action-bar-gap: var(--aparte-space-2);
686
713
  --aparte-branch-picker-gap: var(--aparte-space-2);
@@ -697,14 +724,15 @@ aparte-chat {
697
724
  --aparte-input-padding-y: var(--aparte-space-5);
698
725
  --aparte-conv-list-gap: var(--aparte-space-1);
699
726
  --aparte-conv-item-gap: var(--aparte-space-3);
700
- --aparte-elic-option-radius: var(--aparte-radius-lg);
727
+ --aparte-elic-option-radius: var(--aparte-radius-md);
701
728
  --aparte-elic-input-radius: var(--aparte-radius-md);
702
729
  --aparte-tool-row-radius: var(--aparte-radius-md);
703
- --aparte-avatar-font-size: var(--aparte-font-size-lg);
704
- --aparte-name-font-size: var(--aparte-font-size-lg);
730
+ /* The split's painted seam: the kit's rule, inside a 12px track. */
731
+ --aparte-split-seam-width: var(--aparte-border-width);
732
+ --aparte-name-font-size: var(--aparte-font-size-md);
705
733
  --aparte-timestamp-font-size: var(--aparte-font-size-sm);
706
734
  --aparte-branch-picker-label-size: var(--aparte-font-size-sm);
707
- --aparte-input-font-size: var(--aparte-font-size-lg);
735
+ --aparte-input-font-size: var(--aparte-font-size-base);
708
736
  --aparte-content-font-size: var(--aparte-font-size-base);
709
737
  --aparte-input-editor-font-size: var(--aparte-font-size-base);
710
738
  --aparte-status-font-size: var(--aparte-font-size-md);
@@ -717,6 +745,9 @@ aparte-chat {
717
745
  moves --aparte-space-unit or --aparte-font-scale carries them along. */
718
746
  /* A tooltip is an INVERTED surface: dark on a light theme, light on a dark one.
719
747
  Reading the text pair gives exactly that, and for free — no third palette. */
748
+ /* The distance between a placed tooltip (`data-side`) and its anchor: the arrow's
749
+ half-diagonal, so the tip lands on the trigger rather than 2px short of it. */
750
+ --aparte-tooltip-gap: var(--aparte-space-3);
720
751
  --aparte-tooltip-bg: var(--aparte-text);
721
752
  --aparte-tooltip-color: var(--aparte-text-inverse);
722
753
 
@@ -752,6 +783,14 @@ aparte-chat {
752
783
  --aparte-alert-padding: var(--aparte-space-4) var(--aparte-space-5);
753
784
  --aparte-alert-radius: var(--aparte-radius-md);
754
785
  --aparte-alert-rail-width: var(--aparte-space-1);
786
+ /* The icon scale, in rem times the type factor — the one scale in the theme that
787
+ was pinned in px while every type size followed `--aparte-font-scale`, so a glyph
788
+ beside text shrank optically the moment a reader enlarged the text. The loose
789
+ default is 16 (it was 14, and its documentation already said 16). */
790
+ --aparte-icon-size: calc(1rem * var(--aparte-font-scale));
791
+ --aparte-icon-size-sm: calc(0.75rem * var(--aparte-font-scale));
792
+ --aparte-icon-size-lg: calc(1.125rem * var(--aparte-font-scale));
793
+ --aparte-icon-size-xl: calc(1.25rem * var(--aparte-font-scale));
755
794
  --aparte-alert-icon-size: var(--aparte-space-8);
756
795
  --aparte-alert-dismiss-size: var(--aparte-btn-size-sm);
757
796
  --aparte-alert-title-font-size: var(--aparte-font-size-md);
@@ -759,8 +798,7 @@ aparte-chat {
759
798
 
760
799
  --aparte-avatar-fallback-bg: var(--aparte-surface-3);
761
800
  --aparte-avatar-fallback-color: var(--aparte-text);
762
- --aparte-avatar-group-overlap: var(--aparte-space-3);
763
- --aparte-avatar-group-ring-width: var(--aparte-border-width);
801
+ --aparte-avatar-group-ring-width: calc(var(--aparte-border-width) * 2);
764
802
  --aparte-avatar-group-ring-color: var(--aparte-surface-1);
765
803
 
766
804
  --aparte-badge-font-size: var(--aparte-font-size-2xs);
@@ -781,7 +819,7 @@ aparte-chat {
781
819
  --aparte-card-header-padding: var(--aparte-space-4) var(--aparte-space-5);
782
820
  --aparte-card-body-padding: var(--aparte-space-5);
783
821
  --aparte-card-footer-padding: var(--aparte-space-4) var(--aparte-space-5);
784
- --aparte-card-header-font-size: var(--aparte-font-size-lg);
822
+ --aparte-card-header-font-size: var(--aparte-font-size-base);
785
823
 
786
824
  --aparte-divider-margin: var(--aparte-space-5);
787
825
  --aparte-divider-label-gap: var(--aparte-space-4);
@@ -797,22 +835,56 @@ aparte-chat {
797
835
  --aparte-progress-height: var(--aparte-space-2);
798
836
  --aparte-progress-fill: var(--aparte-primary);
799
837
 
800
- --aparte-skeleton-base: var(--aparte-surface-2);
838
+ /* The ground of a gauge or a placeholder — the thing a value is read against, so it
839
+ has to be seen. Derived RELATIVE to the page, not a surface level: a level sat at
840
+ two different distances from the ground in light and dark. */
841
+ --aparte-track: color-mix(in srgb, var(--aparte-text) 8%, var(--aparte-bg));
842
+ --aparte-skeleton-base: var(--aparte-track);
843
+ /* The rail is as wide as its widest tick (the current one is 1.6× the resting size);
844
+ it used to be a 20px literal clipping a 22.4px tick. */
845
+ --aparte-scroll-rail-width: calc(var(--aparte-scroll-rail-tick-size) * 1.6);
801
846
  --aparte-skeleton-shine: var(--aparte-surface-3);
802
- --aparte-skeleton-text-height: var(--aparte-space-6);
803
- --aparte-skeleton-text-gap: var(--aparte-space-3);
847
+ /* The line it stands for: as tall as the content type, and a gap that is the rest of
848
+ the content line, so the text that replaces it lands where it was (T21). It was 12px
849
+ on an 18px step for a 16px line on a 27px step — a third of a line of jump per line. */
850
+ --aparte-skeleton-text-height: var(--aparte-content-font-size);
851
+ --aparte-skeleton-text-gap: calc(var(--aparte-content-font-size) * (var(--aparte-content-line-height) - 1));
804
852
  --aparte-skeleton-circle-size: var(--aparte-avatar-size);
805
853
 
806
854
  --aparte-spinner-thickness: var(--aparte-space-1);
807
855
 
808
- --aparte-field-error-ring: 0 0 0 var(--aparte-focus-outline-width) var(--aparte-error-solid);
856
+
857
+ /* The field family's knobs, beside the button's — they used to be declared on
858
+ `.aparte-field` itself, which is why `.aparte-field-group` (their PARENT) and
859
+ `.aparte-color` (a sibling recipe) read a property that did not exist there and
860
+ rendered SQUARE: a custom property is substituted where it is declared and only
861
+ inherits downwards. Same reason they belong here rather than there: the theming
862
+ guide sends a consumer to `:root`, and every other family's knobs are here.
863
+ The *channels* stay local on purpose (`--aparte-checkbox-intent` and friends are
864
+ filled by a modifier, not set by a theme). */
865
+ --aparte-field-padding: var(--aparte-space-2) var(--aparte-space-3);
866
+ --aparte-field-padding-sm: var(--aparte-space-1) var(--aparte-space-2);
867
+ --aparte-field-padding-lg: var(--aparte-space-3) var(--aparte-space-4);
868
+ --aparte-field-radius: var(--aparte-radius-md);
869
+ /* The field's resting height is the lg step of the control scale, one step above the
870
+ default button and level with the composer's controls; `--sm` and `--lg` sit one
871
+ step either side. It used to be padding plus line-height, ~29px, the second
872
+ tallest thing in a row of five different heights. */
873
+ --aparte-field-size: var(--aparte-btn-size-lg);
874
+ --aparte-field-size-sm: var(--aparte-btn-size-md);
875
+ --aparte-field-size-lg: var(--aparte-btn-size-xl);
876
+ /* The 36px family, on the scale: named, not literal. */
877
+ --aparte-send-btn-size: var(--aparte-btn-size-lg);
878
+ --aparte-input-action-btn-size: var(--aparte-btn-size-lg);
879
+ --aparte-scroll-btn-size: var(--aparte-btn-size-lg);
809
880
 
810
881
  --aparte-btn-gap: var(--aparte-space-2);
811
882
  --aparte-btn-padding: var(--aparte-space-2) var(--aparte-space-3);
812
883
  --aparte-btn-radius: var(--aparte-radius-md);
813
- --aparte-btn-font-size: var(--aparte-font-size-sm);
884
+ /* Control text is the md step (14px). One step lower was the origin of the "poor"
885
+ look: 13px labels in 32px boxes, beside 15px prose. */
886
+ --aparte-btn-font-size: var(--aparte-font-size-md);
814
887
  --aparte-btn-bg-hover: var(--aparte-surface-2);
815
- --aparte-btn-focus-offset: var(--aparte-space-1);
816
888
  --aparte-btn-bg-active: var(--aparte-surface-3);
817
889
  --aparte-btn-bg-toggled: var(--aparte-surface-2);
818
890
  --aparte-btn-color-toggled: var(--aparte-text);
@@ -822,11 +894,17 @@ aparte-chat {
822
894
  --aparte-btn-padding-sm: var(--aparte-space-1) var(--aparte-space-2);
823
895
  --aparte-btn-padding-lg: var(--aparte-space-3) var(--aparte-space-5);
824
896
 
897
+ /* One knob for the composer's control set — the send button, the input's action
898
+ buttons, the editor's single-line height: the lg step at rest, the touch target
899
+ under a coarse pointer (responsive.css). It used to be undeclared, with `44px`
900
+ written as a fallback at every reader, so a desktop composer was a 62px bar around
901
+ 15px of text and the coarse-pointer block had nothing left to enlarge. */
902
+ --aparte-composer-control-size: var(--aparte-btn-size-lg);
825
903
  --aparte-input-container-min-height: var(--aparte-touch-target-size);
826
- --aparte-elic-message-size: var(--aparte-font-size-md);
827
- --aparte-elic-title-size: var(--aparte-font-size-md);
904
+ --aparte-elic-message-size: var(--aparte-font-size-base);
905
+ --aparte-elic-title-size: var(--aparte-font-size-base);
828
906
  --aparte-elic-desc-size: var(--aparte-font-size-sm);
829
- --aparte-elic-option-title-size: var(--aparte-font-size-lg);
907
+ --aparte-elic-option-title-size: var(--aparte-font-size-md);
830
908
  --aparte-elic-option-desc-size: var(--aparte-font-size-sm);
831
909
  --aparte-elic-skip-size: var(--aparte-font-size-md);
832
910
  --aparte-elic-step-size: var(--aparte-font-size-sm);
@@ -836,6 +914,17 @@ aparte-chat {
836
914
 
837
915
  /* Borders */
838
916
  --aparte-border-focus: var(--aparte-primary);
917
+ /* THE focus ring: a 2px outline in this colour, one spacing step OUTSIDE the box, on
918
+ every control. It used to be drawn two ways — this outline on seventeen recipes and a
919
+ soft box-shadow wash (30 % of the accent, 1.39:1 against the page) on five — and the
920
+ offset was −2px, inside the box, so on a bordered row the ring sat as a second
921
+ concentric line and the next row painted over its edge. */
922
+ --aparte-focus-outline-offset: var(--aparte-space-1);
923
+ /* The edge of a CONTROL — a field, a choice box, the select's trigger. `--aparte-border`
924
+ separates two regions and is right to be faint; a thing someone can touch has to be
925
+ seen, and six previews showed a control with no visible edge. Derived from the ink
926
+ and the ground so both schemes follow; the mix is a first setting. */
927
+ --aparte-border-control: color-mix(in srgb, var(--aparte-text) 30%, var(--aparte-surface-1));
839
928
 
840
929
  /* Component-specific radius (overridable per app) */
841
930
  --aparte-radius-input: var(--aparte-radius-lg);
@@ -843,19 +932,25 @@ aparte-chat {
843
932
  --aparte-radius-send-btn: var(--aparte-radius-md);
844
933
  --aparte-radius-action-btn: var(--aparte-radius-sm);
845
934
  --aparte-radius-select: var(--aparte-radius-md);
846
- --aparte-radius-avatar: var(--aparte-radius-md);
847
935
  --aparte-radius-bubble: var(--aparte-radius-lg);
936
+ /* The bubble's corner IS the bubble radius above: it was a 14px literal off the scale,
937
+ the most visible corner of the transcript, and the knob named for it drove nothing. */
938
+ --aparte-message-content-radius: var(--aparte-radius-bubble);
848
939
  --aparte-radius-code: var(--aparte-radius-lg);
849
940
  --aparte-radius-thinking: var(--aparte-radius-lg);
850
941
  --aparte-radius-error: var(--aparte-radius-md);
942
+ /* The floating surfaces. They read a step of the scale directly, so a theme that
943
+ wanted square menus and round bubbles had to move the step — and every other
944
+ reader of it. */
945
+ --aparte-radius-menu: var(--aparte-radius-lg);
946
+ --aparte-radius-popover: var(--aparte-radius-lg);
947
+ --aparte-radius-dialog: var(--aparte-radius-xl);
948
+ --aparte-radius-tooltip: var(--aparte-radius-sm);
851
949
 
852
950
  /* The error ring on an avatar. A full shadow value, like
853
951
  --aparte-scroll-btn-shadow: the whole ring is the knob, not its width. */
854
952
  --aparte-avatar-error-ring: 0 0 0 2px var(--aparte-error-solid);
855
953
 
856
- /* Focus ring — derived from the accent so it themes automatically. */
857
- --aparte-focus-ring: 0 0 0 3px color-mix(in srgb, var(--aparte-primary) 30%, transparent);
858
-
859
954
  /* Messages */
860
955
  --aparte-message-content-text-user: var(--aparte-text);
861
956
  --aparte-message-content-text-assistant: var(--aparte-text);
@@ -865,7 +960,9 @@ aparte-chat {
865
960
  in sRGB rather than derived with `oklch(from …)`, because a wash has to keep the
866
961
  SURFACE's lightness, not the accent's; 12 % is where the default palette reads as a
867
962
  tint, not a fill, on both grounds. */
868
- --aparte-message-content-bg-user: color-mix(in srgb, var(--aparte-primary) 12%, var(--aparte-surface-1));
963
+ /* The mark's own tint: at 12 % the user bubble — the one mark that says "this turn is
964
+ yours" — sat at 1.03:1 on the light page. */
965
+ --aparte-message-content-bg-user: color-mix(in srgb, var(--aparte-primary) var(--aparte-mark-tint), var(--aparte-surface-1));
869
966
  /* The third surface: the second, pulled 6 % toward the text. Close to what the old
870
967
  literals encoded (`#e7dfce` from `#f1ebdf` light, `#342a40` from `#2a2333` dark —
871
968
  5 to 9 % per channel, not one figure), now one rule that follows the masters. */
@@ -893,6 +990,12 @@ aparte-chat {
893
990
  --aparte-thumb-radius: var(--aparte-radius-lg);
894
991
 
895
992
  /* Action Bar */
993
+ /* The small controls of a turn and of a conversation row are the recipe's small step
994
+ (24px, the WCAG 2.5.8 floor). The action bar used to feed 28 with a 24 exception for
995
+ the last user turn, the branch arrows redrew 20 over their own `--sm`, the row's
996
+ `⋯` redrew 20: three control heights in one 14px row. */
997
+ --aparte-action-bar-btn-size: var(--aparte-btn-size-sm);
998
+ --aparte-conv-action-btn-size: var(--aparte-btn-size-sm);
896
999
  --aparte-action-bar-btn-color: var(--aparte-text-muted);
897
1000
  --aparte-action-bar-btn-hover-bg: var(--aparte-surface-2);
898
1001
  --aparte-action-bar-btn-hover-color: var(--aparte-text);
@@ -903,11 +1006,10 @@ aparte-chat {
903
1006
  --aparte-branch-picker-label-color: var(--aparte-text-muted);
904
1007
 
905
1008
  /* Avatars */
906
- --aparte-avatar-radius: var(--aparte-radius-avatar);
907
1009
  --aparte-avatar-bg-user: var(--aparte-primary);
908
1010
  --aparte-avatar-text-user: var(--aparte-text-inverse);
909
1011
  --aparte-avatar-bg-assistant: var(--aparte-surface-3);
910
- --aparte-avatar-text-assistant: var(--aparte-text-inverse);
1012
+ --aparte-avatar-text-assistant: var(--aparte-text);
911
1013
 
912
1014
  /* Text Sizes */
913
1015
  --aparte-name-color: var(--aparte-text);
@@ -936,6 +1038,10 @@ aparte-chat {
936
1038
  --aparte-code-header-bg: var(--aparte-surface-2);
937
1039
  --aparte-code-border: var(--aparte-border);
938
1040
  --aparte-code-header-text: var(--aparte-text-muted);
1041
+ /* The block's own type. It used to inherit the prose's size and leading (16px on 1.7)
1042
+ under the browser's generic monospace — the largest, airiest text in the message. */
1043
+ --aparte-code-block-font-size: var(--aparte-font-size-md);
1044
+ --aparte-code-block-line-height: var(--aparte-line-height-normal);
939
1045
 
940
1046
  /* Scroll-to-bottom button */
941
1047
 
@@ -955,6 +1061,9 @@ aparte-chat {
955
1061
 
956
1062
  /* Shell */
957
1063
  --aparte-sidebar-bg: var(--aparte-surface-2);
1064
+ /* The one inline inset of the column's four regions — header, search, body, footer.
1065
+ They used to carry three (16 / 12 / 12 / 16) and the content sat on two axes. */
1066
+ --aparte-sidebar-inset: var(--aparte-space-3);
958
1067
 
959
1068
  /* Conversation List */
960
1069
  --aparte-conv-item-radius: var(--aparte-radius-md);
@@ -1034,6 +1143,28 @@ aparte-chat {
1034
1143
  animation: aparte-pulse var(--aparte-duration-pulse) infinite ease-in-out;
1035
1144
  }
1036
1145
 
1146
+ /* Under reduced motion the loop stops outright. The duration tokens are reset to
1147
+ 0.01ms in responsive.css, but that leaves `infinite` in place: a 0.01ms cycle
1148
+ is not stillness, it is a flicker. The descendant sweep there does not reach a
1149
+ recipe used outside aparté's own elements, so each looping recipe stops itself,
1150
+ the way display/icon.css already does. */
1151
+ @media (prefers-reduced-motion: reduce) {
1152
+ .aparte-dot { animation: none; }
1153
+ }
1154
+
1155
+ /* `hidden` means hidden. 71 recipes in these sheets set `display` on their base class,
1156
+ and an author declaration beats the browser's own `[hidden] { display: none }` — so a
1157
+ hidden `.aparte-btn` was out of the accessibility tree and still painted: the copy
1158
+ button of a tool-only turn stood there with `hidden` on it. Scoped to our own classes
1159
+ (a prefix match, not a bare `[hidden]`, so a host's markup is not touched), and
1160
+ `!important` because the whole point is to outrank the recipe's `display`. The
1161
+ handful of per-element `[hidden]` rules that predate this stay; they are now
1162
+ redundant, not wrong. */
1163
+ [class^="aparte-"][hidden],
1164
+ [class*=" aparte-"][hidden] {
1165
+ display: none !important;
1166
+ }
1167
+
1037
1168
  /* =========================================
1038
1169
  aparte-conversation-list
1039
1170
  ========================================= */
@@ -1065,7 +1196,7 @@ aparte-conversation-list {
1065
1196
  .aparte-link:hover { color: var(--aparte-link-color-hover); }
1066
1197
  .aparte-link:focus-visible {
1067
1198
  outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
1068
- outline-offset: var(--aparte-btn-focus-offset);
1199
+ outline-offset: var(--aparte-focus-outline-offset);
1069
1200
  }
1070
1201
  /* Quiet — for a link inside a dense row, where a coloured one would shout. It keeps
1071
1202
  the underline, which is the part that carries the meaning. */
@@ -1088,8 +1219,11 @@ aparte-conversation-list {
1088
1219
  same thing at slightly different rhythms; a travelling wave, where a row wants one,
1089
1220
  comes from an animation-delay on its later dots, not from a skewed curve. */
1090
1221
  @keyframes aparte-pulse {
1091
- 0%, 100% { opacity: 0.3; transform: scale(0.85); }
1092
- 50% { opacity: 1; transform: scale(1.1); }
1222
+ /* Opacity alone: the scale that used to ride along made a row of waiting dots change
1223
+ width in a loop. And a floor above the perception threshold — at 0.3 the status
1224
+ dot spent half of every cycle at 1.55:1, and a capture froze it there. */
1225
+ 0%, 100% { opacity: var(--aparte-pulse-floor); }
1226
+ 50% { opacity: 1; }
1093
1227
  }
1094
1228
 
1095
1229
  /* One rotation, for every spinner and every spinning glyph. */
@@ -1110,9 +1244,9 @@ aparte-conversation-list {
1110
1244
  * <aparte-icon name="copy"></aparte-icon>
1111
1245
  * </button>
1112
1246
  * <div class="aparte-btn-group" role="group" aria-label="Versions">
1113
- * <button class="aparte-btn aparte-btn--surface">‹</button>
1247
+ * <button class="aparte-btn aparte-btn--surface" aria-label="Previous"><aparte-icon name="prevBranch"></aparte-icon></button>
1114
1248
  * <button class="aparte-btn aparte-btn--surface">1 / 2</button>
1115
- * <button class="aparte-btn aparte-btn--surface">›</button>
1249
+ * <button class="aparte-btn aparte-btn--surface" aria-label="Next"><aparte-icon name="nextBranch"></aparte-icon></button>
1116
1250
  * </div>
1117
1251
  * <div class="aparte-btn-group" role="group" aria-label="View">
1118
1252
  * <button class="aparte-btn aparte-btn--primary aparte-btn--outline" aria-pressed="true">Chat</button>
@@ -1139,6 +1273,13 @@ aparte-conversation-list {
1139
1273
  */
1140
1274
 
1141
1275
  .aparte-btn {
1276
+ /* The glyph reads this: an `<aparte-icon>` (or a raw `svg.aparte-icon`) inside a button
1277
+ takes the button's icon size, per size modifier. */
1278
+ --aparte-icon-size: var(--aparte-btn-icon-size);
1279
+ /* The box, for EVERY button: `--icon` and `--circle` used to be the only readers of
1280
+ the size, so a text button was padding plus line-height — 23px beside a 32px icon
1281
+ button in the same row. The size is declared here and read below as a minimum. */
1282
+ --aparte-btn-size: var(--aparte-btn-size-md);
1142
1283
  /* The intent, as a value. Every fill below reads THIS and nothing else, which is
1143
1284
  what keeps the two axes independent. */
1144
1285
  --aparte-btn-intent: var(--aparte-neutral);
@@ -1157,6 +1298,8 @@ aparte-conversation-list {
1157
1298
  --aparte-btn-intent-ink: var(--aparte-btn-intent);
1158
1299
 
1159
1300
  display: inline-flex;
1301
+
1302
+ vertical-align: middle;
1160
1303
  align-items: center;
1161
1304
  justify-content: center;
1162
1305
  gap: var(--aparte-btn-gap);
@@ -1167,6 +1310,7 @@ aparte-conversation-list {
1167
1310
  be set by the composer's own row rule, which is exactly the kind of thing a recipe
1168
1311
  should not need a caller for. */
1169
1312
  box-sizing: border-box;
1313
+ min-block-size: var(--aparte-btn-size);
1170
1314
  padding: var(--aparte-btn-padding);
1171
1315
  border: var(--aparte-border-width) solid transparent;
1172
1316
  border-radius: var(--aparte-btn-radius);
@@ -1321,15 +1465,21 @@ aparte-conversation-list {
1321
1465
  .aparte-btn--soft:active:not(:disabled) { background: color-mix(in srgb, var(--aparte-btn-intent) 30%, transparent); }
1322
1466
  .aparte-btn--solid:active:not(:disabled) { background: var(--aparte-btn-intent); filter: brightness(0.93); }
1323
1467
 
1468
+ /* Drawn, not faded. Opacity on the whole button faded the glyph with its ground and
1469
+ read at 2:1 on the send button; "inactive" and "disappearing" are not the same
1470
+ message. A neutral ground, the muted ink, no intent — whatever the fill was. */
1324
1471
  .aparte-btn:disabled,
1325
1472
  .aparte-btn[aria-disabled='true'] {
1326
1473
  cursor: not-allowed;
1327
- opacity: var(--aparte-disabled-opacity);
1474
+ background: var(--aparte-surface-2);
1475
+ color: var(--aparte-text-muted);
1476
+ border-color: transparent;
1477
+ filter: none;
1328
1478
  }
1329
1479
 
1330
1480
  .aparte-btn:focus-visible {
1331
- outline: var(--aparte-focus-outline-width) solid var(--aparte-btn-intent);
1332
- outline-offset: var(--aparte-btn-focus-offset);
1481
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
1482
+ outline-offset: var(--aparte-focus-outline-offset);
1333
1483
  }
1334
1484
 
1335
1485
  /* Toggled — a disclosure that is open, a toggle that is on, a tab that is current. */
@@ -1362,25 +1512,39 @@ aparte-conversation-list {
1362
1512
  .aparte-btn--block { display: flex; width: 100%; }
1363
1513
 
1364
1514
  .aparte-btn--sm { --aparte-btn-size: var(--aparte-btn-size-sm); --aparte-btn-icon-size: var(--aparte-icon-size-sm); font-size: var(--aparte-font-size-sm); padding: var(--aparte-btn-padding-sm); }
1365
- .aparte-btn--lg { --aparte-btn-size: var(--aparte-btn-size-lg); --aparte-btn-icon-size: var(--aparte-icon-size-xl); font-size: var(--aparte-font-size-lg); padding: var(--aparte-btn-padding-lg); }
1515
+ .aparte-btn--lg { --aparte-btn-size: var(--aparte-btn-size-lg); --aparte-btn-icon-size: var(--aparte-icon-size-xl); font-size: var(--aparte-font-size-base); padding: var(--aparte-btn-padding-lg); }
1516
+ .aparte-btn--xl { --aparte-btn-size: var(--aparte-btn-size-xl); --aparte-btn-icon-size: var(--aparte-icon-size-xl); font-size: var(--aparte-font-size-base); padding: var(--aparte-btn-padding-lg); }
1366
1517
  .aparte-btn--sm.aparte-btn--icon,
1367
1518
  .aparte-btn--sm.aparte-btn--circle,
1368
1519
  .aparte-btn--lg.aparte-btn--icon,
1369
- .aparte-btn--lg.aparte-btn--circle { padding: 0; }
1520
+ .aparte-btn--lg.aparte-btn--circle,
1521
+ .aparte-btn--xl.aparte-btn--icon,
1522
+ .aparte-btn--xl.aparte-btn--circle { padding: 0; }
1370
1523
 
1371
1524
  /* An icon inside a button never takes the pointer: the click belongs to the button,
1372
- and an SVG target makes `event.target` lie to a delegated listener. It is sized with
1373
- the button so the two axes stay in step. */
1525
+ and an SVG target makes `event.target` lie to a delegated listener. */
1374
1526
  .aparte-btn > svg,
1527
+ .aparte-btn > aparte-icon,
1375
1528
  .aparte-btn > .aparte-btn__icon {
1376
1529
  pointer-events: none;
1377
1530
  flex-shrink: 0;
1531
+ }
1532
+
1533
+ /* A raw SVG or a consumer's own icon box is sized with the button so the two axes stay
1534
+ in step. An `<aparte-icon>` is NOT sized here: it renders its `<svg>` inside itself, a
1535
+ grandchild this child combinator never reached, so the documented markup kept the
1536
+ glyph at its loose 14px default in a button that asked for 16. The recipe feeds the
1537
+ icon's own knob instead — `--aparte-icon-size`, declared on `.aparte-btn` above — and
1538
+ `display/icon.css` draws it, at `--sm`'s 12 and `--lg`'s 20 like everything else. */
1539
+ .aparte-btn > svg:not(.aparte-icon),
1540
+ .aparte-btn > .aparte-btn__icon {
1378
1541
  width: var(--aparte-btn-icon-size);
1379
1542
  height: var(--aparte-btn-icon-size);
1380
1543
  }
1381
1544
 
1382
1545
  /* ── Group — buttons that read as one control. ───────────────────────────────── */
1383
- .aparte-btn-group { display: inline-flex; }
1546
+ .aparte-btn-group { display: inline-flex;
1547
+ vertical-align: middle; }
1384
1548
  .aparte-btn-group > .aparte-btn { border-radius: 0; }
1385
1549
  /* Logical properties, so the joined edge follows the writing direction instead of
1386
1550
  assuming left-to-right. */
@@ -1464,24 +1628,24 @@ aparte-conversation-list {
1464
1628
  /* ── The shared text-entry recipe ────────────────────────────────────────────── */
1465
1629
 
1466
1630
  .aparte-field {
1467
- /* Padding recipe, declared here: nothing in the shared token file defines a field
1468
- padding yet, so this file owns its own default and size variants. */
1469
- --aparte-field-padding: var(--aparte-space-2) var(--aparte-space-3);
1470
- /* Same reason, and a gap the recipe had: every other recipe names its own radius
1471
- (`--aparte-btn-radius`, `--aparte-tag-radius`, `--aparte-field-choice-radius`) and
1472
- this one hardcoded the scale step, so a field could not be re-cornered from its
1473
- own element the way every sibling can. */
1474
- --aparte-field-radius: var(--aparte-radius-md);
1475
-
1631
+ /* Every knob this family reads lives in `theme.css` on `:root` — including the
1632
+ radius and the paddings, which used to be declared right here. That was the one
1633
+ construction bug of the kit: `.aparte-field-group` is this element's PARENT and
1634
+ `.aparte-color` a sibling recipe, both read `--aparte-field-radius`, and a custom
1635
+ property declared here is invisible to both, so they computed `border-radius: 0`
1636
+ and every field group in the library rendered square. */
1476
1637
  box-sizing: border-box;
1477
1638
  display: block;
1478
1639
  width: 100%;
1640
+ /* A resting height from the control scale, so a field and the button beside it stop
1641
+ disagreeing by 6px; the size modifiers move it a step either way. */
1642
+ min-block-size: var(--aparte-field-size);
1479
1643
  font: inherit;
1480
- font-size: var(--aparte-font-size-sm);
1644
+ font-size: var(--aparte-font-size-md);
1481
1645
  line-height: var(--aparte-line-height-normal);
1482
1646
  color: var(--aparte-text);
1483
1647
  background: var(--aparte-surface-1);
1484
- border: var(--aparte-border-width) solid var(--aparte-border);
1648
+ border: var(--aparte-border-width) solid var(--aparte-border-control);
1485
1649
  border-radius: var(--aparte-field-radius);
1486
1650
  padding: var(--aparte-field-padding);
1487
1651
  transition:
@@ -1516,10 +1680,13 @@ aparte-conversation-list {
1516
1680
  border-color: var(--aparte-border-focus);
1517
1681
  }
1518
1682
 
1683
+ /* Drawn, not faded: the muted ink on the quieter ground, and the edge back to the region
1684
+ colour. Opacity faded the text with its box and read under 3:1. */
1519
1685
  .aparte-field:disabled {
1520
1686
  cursor: not-allowed;
1521
- opacity: var(--aparte-disabled-opacity);
1522
1687
  background: var(--aparte-surface-2);
1688
+ color: var(--aparte-text-muted);
1689
+ border-color: var(--aparte-border);
1523
1690
  }
1524
1691
 
1525
1692
  .aparte-field[readonly] {
@@ -1533,37 +1700,35 @@ aparte-conversation-list {
1533
1700
  border-color: var(--aparte-error);
1534
1701
  }
1535
1702
 
1703
+ /* The one ring (see --aparte-focus-outline-offset in theme.css): an outline outside the
1704
+ box, and the border left alone — a highlighted border under an offset ring read as two
1705
+ concentric lines. An invalid field's ring takes the error colour. */
1536
1706
  .aparte-field:focus-visible {
1537
- outline: none;
1538
- border-color: var(--aparte-border-focus);
1539
- box-shadow: var(--aparte-focus-ring);
1707
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
1708
+ outline-offset: var(--aparte-focus-outline-offset);
1540
1709
  }
1541
1710
 
1542
1711
  .aparte-field[aria-invalid='true']:focus-visible {
1543
- border-color: var(--aparte-error);
1544
- box-shadow: var(--aparte-field-error-ring);
1712
+ outline-color: var(--aparte-error-solid);
1545
1713
  }
1546
1714
 
1547
1715
  /* ── Sizes ───────────────────────────────────────────────────────────────────── */
1548
1716
 
1549
1717
  .aparte-field--sm {
1550
- --aparte-field-padding-sm: var(--aparte-space-1) var(--aparte-space-2);
1551
-
1718
+ --aparte-field-size: var(--aparte-field-size-sm);
1552
1719
  padding: var(--aparte-field-padding-sm);
1553
- font-size: var(--aparte-font-size-xs);
1720
+ font-size: var(--aparte-font-size-sm);
1554
1721
  }
1555
1722
 
1556
1723
  .aparte-field--lg {
1557
- --aparte-field-padding-lg: var(--aparte-space-3) var(--aparte-space-4);
1558
-
1724
+ --aparte-field-size: var(--aparte-field-size-lg);
1559
1725
  padding: var(--aparte-field-padding-lg);
1560
- font-size: var(--aparte-font-size-md);
1726
+ font-size: var(--aparte-font-size-base);
1561
1727
  }
1562
1728
 
1563
1729
  /* ── Textarea ────────────────────────────────────────────────────────────────── */
1564
1730
 
1565
1731
  .aparte-field--textarea {
1566
- --aparte-field-textarea-min-height: 80px;
1567
1732
 
1568
1733
  display: block;
1569
1734
  min-block-size: var(--aparte-field-textarea-min-height);
@@ -1578,7 +1743,7 @@ aparte-conversation-list {
1578
1743
  display: flex;
1579
1744
  align-items: stretch;
1580
1745
  background: var(--aparte-surface-1);
1581
- border: var(--aparte-border-width) solid var(--aparte-border);
1746
+ border: var(--aparte-border-width) solid var(--aparte-border-control);
1582
1747
  border-radius: var(--aparte-field-radius);
1583
1748
  /* The addons' ground follows the group's corners; the focus ring is a shadow on the
1584
1749
  group itself, outside this box, so it is not clipped. */
@@ -1600,8 +1765,15 @@ aparte-conversation-list {
1600
1765
  border-radius: 0;
1601
1766
  }
1602
1767
 
1768
+ /* The GROUP draws the ring (below): the field inside it draws none. */
1603
1769
  .aparte-field-group > .aparte-field:focus-visible {
1604
- box-shadow: none;
1770
+ outline: none;
1771
+ }
1772
+
1773
+ /* A button in the group is as tall as the field it sits beside — the one row where the
1774
+ two recipes meet, and the one where a 4px difference shows. */
1775
+ .aparte-field-group > .aparte-btn {
1776
+ --aparte-btn-size: var(--aparte-field-size);
1605
1777
  }
1606
1778
 
1607
1779
  /* An addon has a ground of its own and a rule against the field: on the field's own
@@ -1628,8 +1800,8 @@ aparte-conversation-list {
1628
1800
  }
1629
1801
 
1630
1802
  .aparte-field-group:focus-within {
1631
- border-color: var(--aparte-border-focus);
1632
- box-shadow: var(--aparte-focus-ring);
1803
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
1804
+ outline-offset: var(--aparte-focus-outline-offset);
1633
1805
  }
1634
1806
 
1635
1807
  /* `aria-invalid` lives on the field itself, not the wrapper the app didn't write a
@@ -1640,7 +1812,7 @@ aparte-conversation-list {
1640
1812
  }
1641
1813
 
1642
1814
  .aparte-field-group:has(.aparte-field[aria-invalid='true']):focus-within {
1643
- box-shadow: var(--aparte-field-error-ring);
1815
+ outline-color: var(--aparte-error-solid);
1644
1816
  }
1645
1817
 
1646
1818
  /* ── Checkbox ────────────────────────────────────────────────────────────────── */
@@ -1664,7 +1836,6 @@ aparte-conversation-list {
1664
1836
  --aparte-derived-ink: var(--aparte-on-intent);
1665
1837
  /* No shared control-size token covers a checkbox yet (the button's own sizes are
1666
1838
  button-specific), so its box is declared here. */
1667
- --aparte-checkbox-size: 18px;
1668
1839
 
1669
1840
  box-sizing: border-box;
1670
1841
  appearance: none;
@@ -1675,7 +1846,7 @@ aparte-conversation-list {
1675
1846
  place-content: center;
1676
1847
  width: var(--aparte-checkbox-size);
1677
1848
  height: var(--aparte-checkbox-size);
1678
- border: var(--aparte-border-width) solid var(--aparte-border);
1849
+ border: var(--aparte-border-width) solid var(--aparte-border-control);
1679
1850
  border-radius: var(--aparte-radius-sm);
1680
1851
  background: var(--aparte-surface-1);
1681
1852
  cursor: pointer;
@@ -1760,7 +1931,7 @@ aparte-conversation-list {
1760
1931
  }
1761
1932
 
1762
1933
  .aparte-checkbox:focus-visible {
1763
- outline: var(--aparte-focus-outline-width) solid var(--aparte-checkbox-intent);
1934
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
1764
1935
  outline-offset: var(--aparte-focus-outline-offset);
1765
1936
  }
1766
1937
 
@@ -1790,8 +1961,6 @@ aparte-conversation-list {
1790
1961
  used. `middle` and not `text-bottom`: the label may wrap. */
1791
1962
  vertical-align: middle;
1792
1963
  --aparte-radio-intent: var(--aparte-primary);
1793
- --aparte-radio-size: 18px;
1794
- --aparte-radio-dot-size: 8px;
1795
1964
 
1796
1965
  box-sizing: border-box;
1797
1966
  appearance: none;
@@ -1802,7 +1971,7 @@ aparte-conversation-list {
1802
1971
  place-content: center;
1803
1972
  width: var(--aparte-radio-size);
1804
1973
  height: var(--aparte-radio-size);
1805
- border: var(--aparte-border-width) solid var(--aparte-border);
1974
+ border: var(--aparte-border-width) solid var(--aparte-border-control);
1806
1975
  border-radius: var(--aparte-radius-full);
1807
1976
  background: var(--aparte-surface-1);
1808
1977
  cursor: pointer;
@@ -1844,7 +2013,7 @@ aparte-conversation-list {
1844
2013
  }
1845
2014
 
1846
2015
  .aparte-radio:focus-visible {
1847
- outline: var(--aparte-focus-outline-width) solid var(--aparte-radio-intent);
2016
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
1848
2017
  outline-offset: var(--aparte-focus-outline-offset);
1849
2018
  }
1850
2019
 
@@ -1875,9 +2044,6 @@ aparte-conversation-list {
1875
2044
  has to live here where `:checked` can override it — a central default could never
1876
2045
  react to the class + state combo. */
1877
2046
  --aparte-switch-track: var(--aparte-surface-3);
1878
- --aparte-switch-width: 40px;
1879
- --aparte-switch-height: 22px;
1880
- --aparte-switch-thumb-inset: 2px;
1881
2047
  /* DERIVED from the track, not a fourth literal to keep in step with the other three:
1882
2048
  the thumb used to be 16px in a 20px track with a 1px border and a 2px top inset —
1883
2049
  2px of air above it and none below, visibly low ("le cercle pas centré"). The
@@ -1896,7 +2062,7 @@ aparte-conversation-list {
1896
2062
  flex-shrink: 0;
1897
2063
  inline-size: var(--aparte-switch-width);
1898
2064
  block-size: var(--aparte-switch-height);
1899
- border: var(--aparte-border-width) solid var(--aparte-border);
2065
+ border: var(--aparte-border-width) solid var(--aparte-border-control);
1900
2066
  border-radius: var(--aparte-radius-full);
1901
2067
  background: var(--aparte-switch-track);
1902
2068
  cursor: pointer;
@@ -1944,7 +2110,7 @@ aparte-conversation-list {
1944
2110
  }
1945
2111
 
1946
2112
  .aparte-switch:focus-visible {
1947
- outline: var(--aparte-focus-outline-width) solid var(--aparte-switch-intent);
2113
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
1948
2114
  outline-offset: var(--aparte-focus-outline-offset);
1949
2115
  }
1950
2116
 
@@ -1968,8 +2134,6 @@ aparte-conversation-list {
1968
2134
 
1969
2135
  .aparte-range {
1970
2136
  --aparte-range-intent: var(--aparte-primary);
1971
- --aparte-range-thumb-size: 16px;
1972
- --aparte-range-track-height: 4px;
1973
2137
 
1974
2138
  appearance: none;
1975
2139
  -webkit-appearance: none;
@@ -2039,11 +2203,11 @@ aparte-conversation-list {
2039
2203
  .aparte-range:active::-moz-range-thumb { transform: scale(1.15); }
2040
2204
 
2041
2205
  .aparte-range:focus-visible::-webkit-slider-thumb {
2042
- outline: var(--aparte-focus-outline-width) solid var(--aparte-range-intent);
2206
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
2043
2207
  outline-offset: var(--aparte-focus-outline-offset);
2044
2208
  }
2045
2209
  .aparte-range:focus-visible::-moz-range-thumb {
2046
- outline: var(--aparte-focus-outline-width) solid var(--aparte-range-intent);
2210
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
2047
2211
  outline-offset: var(--aparte-focus-outline-offset);
2048
2212
  }
2049
2213
 
@@ -2064,6 +2228,7 @@ aparte-conversation-list {
2064
2228
 
2065
2229
  .aparte-field-label {
2066
2230
  display: inline-block;
2231
+ vertical-align: middle;
2067
2232
  margin-block-end: var(--aparte-space-1);
2068
2233
  font-size: var(--aparte-font-size-sm);
2069
2234
  font-weight: var(--aparte-font-weight-medium);
@@ -2151,7 +2316,7 @@ aparte-conversation-list {
2151
2316
  inline-size: var(--aparte-color-size);
2152
2317
  block-size: var(--aparte-color-size);
2153
2318
  padding: 0;
2154
- border: var(--aparte-border-width) solid var(--aparte-border);
2319
+ border: var(--aparte-border-width) solid var(--aparte-border-control);
2155
2320
  border-radius: var(--aparte-field-radius);
2156
2321
  background: none;
2157
2322
  cursor: pointer;
@@ -2161,7 +2326,7 @@ aparte-conversation-list {
2161
2326
  .aparte-color::-moz-color-swatch { border: none; border-radius: var(--aparte-radius-sm); }
2162
2327
  .aparte-color:focus-visible {
2163
2328
  outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
2164
- outline-offset: var(--aparte-btn-focus-offset);
2329
+ outline-offset: var(--aparte-focus-outline-offset);
2165
2330
  }
2166
2331
  .aparte-color:disabled { cursor: not-allowed; opacity: var(--aparte-disabled-opacity); }
2167
2332
 
@@ -2217,6 +2382,7 @@ aparte-conversation-list {
2217
2382
  would invite a click. */
2218
2383
  .aparte-output {
2219
2384
  display: inline-block;
2385
+ vertical-align: middle;
2220
2386
  padding: var(--aparte-output-padding);
2221
2387
  border-radius: var(--aparte-radius-sm);
2222
2388
  background: var(--aparte-output-bg);
@@ -2299,19 +2465,22 @@ aparte-conversation-list {
2299
2465
  }
2300
2466
  .aparte-field-choice:has(:focus-visible) {
2301
2467
  outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
2302
- outline-offset: var(--aparte-btn-focus-offset);
2468
+ outline-offset: var(--aparte-focus-outline-offset);
2303
2469
  }
2304
2470
 
2305
2471
  .aparte-field-choice__body { flex: 1 1 auto; min-width: 0; }
2306
2472
  /* =========================================
2307
2473
  aparte-avatar — sizes, shape, image + initials fallback
2308
2474
  =========================================
2309
- --aparte-avatar-size, --aparte-avatar-radius and --aparte-avatar-font-size are
2310
- NOT invented here: they already exist centrally for the message avatar
2311
- (bubble.css), and this component deliberately reads the same three rather than
2312
- declare its own — a generic avatar and a chat-message avatar are the same
2313
- object, and a size or shape modifier applied here resizes a role-coloured
2314
- message avatar too, for free, instead of only a second, disconnected one.
2475
+ `--aparte-avatar-size` is NOT invented here: it already exists centrally for the
2476
+ message avatar (bubble.css), and this component reads the same one — a generic
2477
+ avatar and a chat-message avatar are the same object, and a size modifier applied
2478
+ here resizes a role-coloured message avatar too. The corner, the initials and the
2479
+ group's overlap are FRACTIONS of that size (`--aparte-avatar-radius-ratio`,
2480
+ `--aparte-avatar-initials-ratio`, `--aparte-avatar-overlap-ratio`), computed on the
2481
+ element so a size modifier moves them: as absolute tokens the 40px avatar drew the
2482
+ same 11px initials as the 32px one, the corner drifted from squircle to square up
2483
+ the ramp, and a 6px overlap was a fifth of a small avatar and a tenth of a large one.
2315
2484
 
2316
2485
  <span class="aparte-avatar aparte-avatar--sm">AP</span>
2317
2486
  <span class="aparte-avatar">JD</span>
@@ -2326,16 +2495,17 @@ aparte-conversation-list {
2326
2495
  .aparte-avatar {
2327
2496
  position: relative;
2328
2497
  display: inline-flex;
2498
+ vertical-align: middle;
2329
2499
  flex-shrink: 0;
2330
2500
  align-items: center;
2331
2501
  justify-content: center;
2332
2502
  overflow: hidden;
2333
2503
  width: var(--aparte-avatar-size);
2334
2504
  height: var(--aparte-avatar-size);
2335
- border-radius: var(--aparte-avatar-radius);
2505
+ border-radius: calc(var(--aparte-avatar-size) * var(--aparte-avatar-radius-ratio));
2336
2506
  background: var(--aparte-avatar-fallback-bg);
2337
2507
  color: var(--aparte-avatar-fallback-color);
2338
- font-size: var(--aparte-avatar-font-size);
2508
+ font-size: calc(var(--aparte-avatar-size) * var(--aparte-avatar-initials-ratio));
2339
2509
  font-weight: var(--aparte-font-weight-semibold);
2340
2510
  line-height: var(--aparte-line-height-none);
2341
2511
  text-transform: uppercase;
@@ -2359,13 +2529,13 @@ aparte-conversation-list {
2359
2529
 
2360
2530
  /* Square is the default shape and needs no class (it is what the message avatar
2361
2531
  already renders); named anyway so a square avatar can say so rather than omit. */
2362
- .aparte-avatar--square { border-radius: var(--aparte-avatar-radius); }
2532
+ .aparte-avatar--square { border-radius: calc(var(--aparte-avatar-size) * var(--aparte-avatar-radius-ratio)); }
2363
2533
  .aparte-avatar--circle { border-radius: var(--aparte-radius-full); }
2364
2534
 
2365
- .aparte-avatar--xs { --aparte-avatar-size: var(--aparte-avatar-size-xs); --aparte-avatar-font-size: var(--aparte-font-size-2xs); }
2366
- .aparte-avatar--sm { --aparte-avatar-size: var(--aparte-avatar-size-sm); --aparte-avatar-font-size: var(--aparte-font-size-xs); }
2367
- .aparte-avatar--lg { --aparte-avatar-size: var(--aparte-avatar-size-lg); --aparte-avatar-font-size: var(--aparte-font-size-lg); }
2368
- .aparte-avatar--xl { --aparte-avatar-size: var(--aparte-avatar-size-xl); --aparte-avatar-font-size: var(--aparte-font-size-lg); }
2535
+ .aparte-avatar--xs { --aparte-avatar-size: var(--aparte-avatar-size-xs); }
2536
+ .aparte-avatar--sm { --aparte-avatar-size: var(--aparte-avatar-size-sm); }
2537
+ .aparte-avatar--lg { --aparte-avatar-size: var(--aparte-avatar-size-lg); }
2538
+ .aparte-avatar--xl { --aparte-avatar-size: var(--aparte-avatar-size-xl); }
2369
2539
 
2370
2540
  /* ── Group — a fan of avatars that read as one party. ─────────────────────────
2371
2541
  No z-index is set: later siblings simply paint over earlier ones in normal
@@ -2373,13 +2543,14 @@ aparte-conversation-list {
2373
2543
  the one thing z-index would otherwise be needed for. */
2374
2544
  .aparte-avatar-group {
2375
2545
  display: inline-flex;
2546
+ vertical-align: middle;
2376
2547
  align-items: center;
2377
2548
  }
2378
2549
  .aparte-avatar-group > .aparte-avatar {
2379
2550
  box-shadow: 0 0 0 var(--aparte-avatar-group-ring-width) var(--aparte-avatar-group-ring-color);
2380
2551
  }
2381
2552
  .aparte-avatar-group > .aparte-avatar:not(:first-child) {
2382
- margin-inline-start: calc(var(--aparte-avatar-group-overlap) * -1);
2553
+ margin-inline-start: calc(var(--aparte-avatar-size) * var(--aparte-avatar-overlap-ratio) * -1);
2383
2554
  }
2384
2555
  .aparte-avatar-group > .aparte-avatar:hover {
2385
2556
  z-index: var(--aparte-z-raised);
@@ -2498,6 +2669,8 @@ aparte-icon {
2498
2669
  --aparte-derived-ink: var(--aparte-on-intent);
2499
2670
 
2500
2671
  display: inline-flex;
2672
+
2673
+ vertical-align: middle;
2501
2674
  align-items: center;
2502
2675
  justify-content: center;
2503
2676
  gap: var(--aparte-badge-gap);
@@ -2595,6 +2768,7 @@ aparte-icon {
2595
2768
  .aparte-tag {
2596
2769
  box-sizing: border-box;
2597
2770
  display: inline-flex;
2771
+ vertical-align: middle;
2598
2772
  align-items: center;
2599
2773
  gap: var(--aparte-tag-gap);
2600
2774
  max-width: 100%;
@@ -2657,8 +2831,8 @@ aparte-icon {
2657
2831
  * <span class="aparte-thumbnail aparte-thumbnail--lg"><img class="aparte-thumbnail__image"
2658
2832
  * src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2 3'%3E%3Crect width='2' height='3' fill='%23b07d33'/%3E%3C/svg%3E"
2659
2833
  * alt="A screenshot"></span>
2660
- * <span class="aparte-thumbnail"><span class="aparte-thumbnail__ext">PDF</span></span>
2661
- * <span class="aparte-thumbnail aparte-thumbnail--sm"><span class="aparte-thumbnail__ext">MD</span></span>
2834
+ * <span class="aparte-thumbnail"><span class="aparte-thumbnail__label">PDF</span></span>
2835
+ * <span class="aparte-thumbnail aparte-thumbnail--sm"><span class="aparte-thumbnail__label">MD</span></span>
2662
2836
  *
2663
2837
  * Distinct from `.aparte-avatar`, which is the same shape for a different reason: an
2664
2838
  * avatar stands for a PERSON and falls back to initials, so it carries type styling and
@@ -2765,14 +2939,8 @@ aparte-icon {
2765
2939
  }
2766
2940
 
2767
2941
  .aparte-thumb--image[role='button']:focus-visible {
2768
- box-shadow: var(--aparte-focus-ring);
2769
- }
2770
-
2771
- .aparte-thumb__img {
2772
- width: 100%;
2773
- height: 100%;
2774
- object-fit: cover;
2775
- display: block;
2942
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
2943
+ outline-offset: var(--aparte-focus-outline-offset);
2776
2944
  }
2777
2945
 
2778
2946
  .aparte-thumb--file {
@@ -2781,13 +2949,6 @@ aparte-icon {
2781
2949
  justify-content: center;
2782
2950
  }
2783
2951
 
2784
- .aparte-thumb__ext {
2785
- font-size: var(--aparte-font-size-xs);
2786
- font-weight: var(--aparte-font-weight-bold);
2787
- letter-spacing: 0.03em;
2788
- color: var(--aparte-text-muted);
2789
- }
2790
-
2791
2952
  .aparte-thumb__name {
2792
2953
  position: absolute;
2793
2954
  left: 0;
@@ -2818,9 +2979,13 @@ aparte-icon {
2818
2979
  */
2819
2980
 
2820
2981
  .aparte-spinner {
2821
-
2822
2982
  display: inline-block;
2983
+ vertical-align: middle;
2823
2984
  flex-shrink: 0;
2985
+ /* The token is the ring's outer size, border included: without this the ring painted
2986
+ 16/20/28 for tokens of 12/16/24 (the 2px stroke added on each side), and painted
2987
+ the token's value only on a host page with its own border-box reset. */
2988
+ box-sizing: border-box;
2824
2989
  width: var(--aparte-spinner-size);
2825
2990
  height: var(--aparte-spinner-size);
2826
2991
  border: var(--aparte-spinner-thickness) solid currentColor;
@@ -2835,6 +3000,15 @@ aparte-icon {
2835
3000
 
2836
3001
  .aparte-spinner--sm { --aparte-spinner-size: var(--aparte-spinner-size-sm); }
2837
3002
  .aparte-spinner--lg { --aparte-spinner-size: var(--aparte-spinner-size-lg); }
3003
+
3004
+ /* Under reduced motion the loop stops outright. The duration tokens are reset to
3005
+ 0.01ms in responsive.css, but that leaves `infinite` in place: a 0.01ms cycle
3006
+ is not stillness, it is a flicker. The descendant sweep there does not reach a
3007
+ recipe used outside aparté's own elements, so each looping recipe stops itself,
3008
+ the way display/icon.css already does. */
3009
+ @media (prefers-reduced-motion: reduce) {
3010
+ .aparte-spinner { animation: none; }
3011
+ }
2838
3012
  /* =========================================
2839
3013
  aparte-progress — determinate bar + indeterminate variant
2840
3014
  =========================================
@@ -2859,7 +3033,9 @@ aparte-icon {
2859
3033
  width: 100%;
2860
3034
  height: var(--aparte-progress-height);
2861
3035
  border-radius: var(--aparte-radius-full);
2862
- background: var(--aparte-surface-3);
3036
+ /* The track: a reference the value is read against, so it has to be seen. A surface
3037
+ level put it at 1.06:1 in light and 1.20:1 in dark — one token, two distances. */
3038
+ background: var(--aparte-track);
2863
3039
  }
2864
3040
 
2865
3041
  .aparte-progress__bar {
@@ -2877,6 +3053,21 @@ aparte-icon {
2877
3053
  animation: aparte-progress-slide var(--aparte-duration-pulse) var(--aparte-ease) infinite;
2878
3054
  }
2879
3055
 
3056
+ /* Under reduced motion the loop stops outright. The duration tokens are reset to
3057
+ 0.01ms in responsive.css, but that leaves `infinite` in place: a 0.01ms cycle
3058
+ is not stillness, it is a flicker. The descendant sweep there does not reach a
3059
+ recipe used outside aparté's own elements, so each looping recipe stops itself,
3060
+ the way display/icon.css already does.
3061
+ Stopped, the sliding segment would sit at one spot and read as a value; the whole
3062
+ track filled says "busy" without naming an amount. */
3063
+ @media (prefers-reduced-motion: reduce) {
3064
+ .aparte-progress--indeterminate .aparte-progress__bar {
3065
+ animation: none;
3066
+ width: 100%;
3067
+ inset-inline-start: 0;
3068
+ }
3069
+ }
3070
+
2880
3071
  /* inset-inline-start, not transform: translateX — a transform has no logical
2881
3072
  axis, so an RTL page would slide the indeterminate segment backwards. Animating
2882
3073
  a logical inset instead makes the sweep follow reading direction automatically. */
@@ -2909,10 +3100,15 @@ aparte-icon {
2909
3100
  <div>
2910
3101
  <span class="aparte-skeleton aparte-skeleton--text" aria-hidden="true"></span>
2911
3102
  <span class="aparte-skeleton aparte-skeleton--text" aria-hidden="true"></span>
2912
- <span class="aparte-skeleton aparte-skeleton--text" aria-hidden="true"></span>
3103
+ <span class="aparte-skeleton aparte-skeleton--text" style="max-inline-size: 60%" aria-hidden="true"></span>
2913
3104
  </div>
2914
- <span class="aparte-skeleton aparte-skeleton--rect" style="grid-column: 1 / -1; block-size: 64px" aria-hidden="true"></span>
3105
+ <span class="aparte-skeleton aparte-skeleton--rect" style="grid-column: 2" aria-hidden="true"></span>
2915
3106
  </div>
3107
+
3108
+ The last line is shorter because the last line of a paragraph is, and the block sits
3109
+ in the text column, under the lines it stands for — not across the avatar's. The
3110
+ rect's height is the family's token (`--aparte-skeleton-rect-height`), not an inline
3111
+ value that contradicts it.
2916
3112
  */
2917
3113
 
2918
3114
  .aparte-skeleton {
@@ -2929,6 +3125,19 @@ aparte-icon {
2929
3125
  animation: aparte-skeleton-shimmer var(--aparte-duration-pulse) linear infinite;
2930
3126
  }
2931
3127
 
3128
+ /* Under reduced motion the loop stops outright. The duration tokens are reset to
3129
+ 0.01ms in responsive.css, but that leaves `infinite` in place: a 0.01ms cycle
3130
+ is not stillness, it is a flicker. The descendant sweep there does not reach a
3131
+ recipe used outside aparté's own elements, so each looping recipe stops itself,
3132
+ the way display/icon.css already does.
3133
+ The shine goes with the shimmer: a stopped gradient would sit as a pale band. */
3134
+ @media (prefers-reduced-motion: reduce) {
3135
+ .aparte-skeleton {
3136
+ animation: none;
3137
+ background-image: none;
3138
+ }
3139
+ }
3140
+
2932
3141
  .aparte-skeleton--text {
2933
3142
  width: 100%;
2934
3143
  height: var(--aparte-skeleton-text-height);
@@ -2936,7 +3145,7 @@ aparte-icon {
2936
3145
  }
2937
3146
  .aparte-skeleton--text + .aparte-skeleton--text { margin-block-start: var(--aparte-skeleton-text-gap); }
2938
3147
  /* The ragged last line reads as "text", where a full-width block reads as a bar. */
2939
- .aparte-skeleton--text:last-child { width: var(--aparte-skeleton-text-last-width); }
3148
+ .aparte-skeleton--text:last-child:not(:first-child) { width: var(--aparte-skeleton-text-last-width); }
2940
3149
 
2941
3150
  .aparte-skeleton--circle {
2942
3151
  width: var(--aparte-skeleton-circle-size);
@@ -3024,6 +3233,7 @@ aparte-icon {
3024
3233
  <button class="aparte-alert__dismiss" aria-label="Dismiss"><aparte-icon name="close"></aparte-icon></button>
3025
3234
  </div>
3026
3235
  <div class="aparte-alert aparte-alert--danger" role="alert">
3236
+ <span class="aparte-alert__icon"><aparte-icon name="alertTriangle"></aparte-icon></span>
3027
3237
  <div class="aparte-alert__body">
3028
3238
  <p class="aparte-alert__message">The request was refused: no API key is set.</p>
3029
3239
  </div>
@@ -3114,7 +3324,7 @@ aparte-icon {
3114
3324
  }
3115
3325
  .aparte-alert__dismiss:active:not(:disabled) { filter: brightness(0.93); }
3116
3326
  .aparte-alert__dismiss:focus-visible {
3117
- outline: var(--aparte-focus-outline-width) solid var(--aparte-alert-intent);
3327
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
3118
3328
  outline-offset: var(--aparte-focus-outline-offset);
3119
3329
  }
3120
3330
  .aparte-alert__dismiss:disabled { opacity: var(--aparte-disabled-opacity); cursor: not-allowed; pointer-events: none; }
@@ -3153,6 +3363,12 @@ aparte-icon {
3153
3363
  padding: var(--aparte-card-body-padding);
3154
3364
  }
3155
3365
 
3366
+ /* The body owns its edges: a paragraph's block margins fold into the padding instead of
3367
+ stacking on it. The sheet's own example puts a <p> here, and the body band measured
3368
+ twice the height of the header and footer beside it. */
3369
+ .aparte-card__body > :first-child { margin-block-start: 0; }
3370
+ .aparte-card__body > :last-child { margin-block-end: 0; }
3371
+
3156
3372
  .aparte-card__footer {
3157
3373
  display: flex;
3158
3374
  align-items: center;
@@ -3427,7 +3643,7 @@ aparte-icon {
3427
3643
  }
3428
3644
 
3429
3645
  .aparte-tabs__tab:focus-visible {
3430
- outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
3646
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
3431
3647
  outline-offset: var(--aparte-focus-outline-offset);
3432
3648
  }
3433
3649
 
@@ -3444,10 +3660,13 @@ aparte-icon {
3444
3660
 
3445
3661
  .aparte-tabs__panel {
3446
3662
  padding-block: var(--aparte-space-6);
3663
+ /* The tab's own inline inset, so the panel's text starts under the label of the tab
3664
+ that opened it rather than 11px to its left. */
3665
+ padding-inline: var(--aparte-space-5);
3447
3666
  }
3448
3667
 
3449
3668
  .aparte-tabs__panel:focus-visible {
3450
- outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
3669
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
3451
3670
  outline-offset: var(--aparte-focus-outline-offset);
3452
3671
  }
3453
3672
 
@@ -3471,6 +3690,9 @@ aparte-icon {
3471
3690
  its own raised chip, rather than an underline beneath a plain row. Same markup,
3472
3691
  same states; only the container and the active tab's fill change. */
3473
3692
  .aparte-tabs--segmented {
3693
+ /* A control, not a bar: the track hugs its segments. As a block-level flex row it
3694
+ inherited its container's width — 1207px of track for 160px of chips at 1280. */
3695
+ display: inline-flex;
3474
3696
  gap: 0;
3475
3697
  padding: var(--aparte-space-1);
3476
3698
  background: var(--aparte-surface-2);
@@ -3490,6 +3712,11 @@ aparte-icon {
3490
3712
  .aparte-tabs--segmented .aparte-tabs__tab[aria-selected='true'] {
3491
3713
  background: var(--aparte-surface-1);
3492
3714
  color: var(--aparte-text);
3715
+ /* Raised RELATIVE to the track, not an absolute surface level: surface-1 over
3716
+ surface-2 read as raised in light and as sunken in dark, the same rule inverting
3717
+ its elevation with the theme. A 1px ring in the border colour says "on top" on
3718
+ either ground. */
3719
+ box-shadow: 0 0 0 var(--aparte-border-width) var(--aparte-border);
3493
3720
  }
3494
3721
  /* =========================================
3495
3722
  aparte-accordion — stacked disclosures
@@ -3506,7 +3733,21 @@ aparte-icon {
3506
3733
  Shipping
3507
3734
  <aparte-icon name="expand"></aparte-icon>
3508
3735
  </summary>
3509
- <div class="aparte-accordion__panel">Free above 50 €.</div>
3736
+ <div class="aparte-accordion__panel">Free above 50 €, otherwise 4,90 €. Two to four working days.</div>
3737
+ </details>
3738
+ <details class="aparte-accordion__item">
3739
+ <summary class="aparte-accordion__header">
3740
+ Returns
3741
+ <aparte-icon name="expand"></aparte-icon>
3742
+ </summary>
3743
+ <div class="aparte-accordion__panel">Thirty days, in the original packaging.</div>
3744
+ </details>
3745
+ <details class="aparte-accordion__item">
3746
+ <summary class="aparte-accordion__header">
3747
+ Payment
3748
+ <aparte-icon name="expand"></aparte-icon>
3749
+ </summary>
3750
+ <div class="aparte-accordion__panel">Card, PayPal, or an invoice for a company account.</div>
3510
3751
  </details>
3511
3752
  </div>
3512
3753
 
@@ -3574,7 +3815,7 @@ aparte-icon {
3574
3815
  }
3575
3816
 
3576
3817
  .aparte-accordion__header:focus-visible {
3577
- outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
3818
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
3578
3819
  outline-offset: var(--aparte-focus-outline-offset);
3579
3820
  }
3580
3821
 
@@ -3584,13 +3825,22 @@ aparte-icon {
3584
3825
  opacity: var(--aparte-disabled-opacity);
3585
3826
  }
3586
3827
 
3587
- .aparte-accordion__icon {
3828
+ /* Two spellings for the chevron, because the header's documented markup is a bare
3829
+ `<aparte-icon>` and a renderer that wraps a raw glyph needs a class to hang it on:
3830
+ the wrapper `.aparte-accordion__icon` (the thinking block), or an `<aparte-icon>` /
3831
+ `svg.aparte-icon` sitting directly in the header. Both are sized and both turn — the
3832
+ documented form used to match neither rule, so the kit's own example showed an open
3833
+ panel under a chevron still pointing down. */
3834
+ .aparte-accordion__icon,
3835
+ .aparte-accordion__header > aparte-icon,
3836
+ .aparte-accordion__header > .aparte-icon {
3837
+ display: inline-flex;
3588
3838
  flex-shrink: 0;
3589
- /* Sized off the same step the button recipe already uses for an icon glyph
3590
- (`--aparte-btn-icon-size: var(--aparte-space-8)`), so a disclosure chevron
3591
- reads as the same weight of icon as everywhere else in the library. */
3592
- width: var(--aparte-space-8);
3593
- height: var(--aparte-space-8);
3839
+ /* Fed to the glyph through its own knob, not drawn as a width/height: the icon
3840
+ recipe reads `--aparte-icon-size`, and a box declared here around it would leave
3841
+ the glyph at its loose default. The step is the one the button recipe uses for an
3842
+ icon glyph, so a disclosure chevron reads as the same weight of icon everywhere. */
3843
+ --aparte-icon-size: var(--aparte-space-8);
3594
3844
  color: var(--aparte-text-muted);
3595
3845
  transition: transform var(--aparte-duration-slow) var(--aparte-ease);
3596
3846
  }
@@ -3599,7 +3849,11 @@ aparte-icon {
3599
3849
  puts aria-expanded on the header itself; native <details> puts `open` on the
3600
3850
  ancestor and gives the browser no attribute to select on at all. */
3601
3851
  .aparte-accordion__header[aria-expanded='true'] .aparte-accordion__icon,
3602
- details[open] > .aparte-accordion__header .aparte-accordion__icon {
3852
+ .aparte-accordion__header[aria-expanded='true'] > aparte-icon,
3853
+ .aparte-accordion__header[aria-expanded='true'] > .aparte-icon,
3854
+ details[open] > .aparte-accordion__header .aparte-accordion__icon,
3855
+ details[open] > .aparte-accordion__header > aparte-icon,
3856
+ details[open] > .aparte-accordion__header > .aparte-icon {
3603
3857
  transform: rotate(180deg);
3604
3858
  }
3605
3859
 
@@ -3673,10 +3927,13 @@ details[open] > .aparte-accordion__header .aparte-accordion__icon {
3673
3927
  width: max-content;
3674
3928
  min-width: var(--aparte-menu-min-width);
3675
3929
  max-width: var(--aparte-menu-max-width);
3930
+ /* The two caps are the box's outer width, padding and border included — same rule
3931
+ as the popover, for the same reason. */
3932
+ box-sizing: border-box;
3676
3933
  padding: var(--aparte-space-2);
3677
3934
  background: var(--aparte-surface-1);
3678
3935
  border: var(--aparte-border-width) solid var(--aparte-border);
3679
- border-radius: var(--aparte-radius-lg);
3936
+ border-radius: var(--aparte-radius-menu);
3680
3937
  box-shadow: var(--aparte-menu-shadow);
3681
3938
  color: var(--aparte-text);
3682
3939
  z-index: var(--aparte-z-dropdown);
@@ -3722,7 +3979,7 @@ details[open] > .aparte-accordion__header .aparte-accordion__icon {
3722
3979
  }
3723
3980
 
3724
3981
  .aparte-menu__item:focus-visible {
3725
- outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
3982
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
3726
3983
  outline-offset: var(--aparte-focus-outline-offset);
3727
3984
  }
3728
3985
 
@@ -3735,14 +3992,14 @@ details[open] > .aparte-accordion__header .aparte-accordion__icon {
3735
3992
  pointer-events: none;
3736
3993
  }
3737
3994
 
3738
- /* The check mark for menuitemcheckbox / menuitemradio. Reserved as an invisible
3739
- glyph on every checkable item (matched by the presence of aria-checked, true or
3740
- false) so labels in a mixed checked/unchecked list stay aligned to one gutter;
3741
- a plain role="menuitem" without aria-checked gets no gutter at all. Text colour
3742
- rather than a fill or a box-shadow is deliberate: a glyph inherits `color`,
3743
- which forced-colors mode preserves, so the checked state survives that mode
3744
- with no separate override. */
3745
- .aparte-menu__item[aria-checked]::before {
3995
+ /* The check mark for menuitemcheckbox / menuitemradio. The gutter is reserved on EVERY
3996
+ item of a panel that holds a checkable one — as an invisible glyph — so all the
3997
+ labels of that panel share one edge. It used to be reserved per checkable item, and
3998
+ a plain role="menuitem" beside a checkable one started 16px further left: two label
3999
+ edges in one menu. Text colour rather than a fill or a box-shadow is deliberate: a
4000
+ glyph inherits `color`, which forced-colors mode preserves, so the checked state
4001
+ survives that mode with no separate override. */
4002
+ .aparte-menu:has(.aparte-menu__item[aria-checked]) .aparte-menu__item::before {
3746
4003
  content: '✓';
3747
4004
  flex-shrink: 0;
3748
4005
  visibility: hidden;
@@ -3817,11 +4074,14 @@ details[open] > .aparte-accordion__header .aparte-accordion__icon {
3817
4074
  display: flex;
3818
4075
  flex-direction: column;
3819
4076
  gap: var(--aparte-space-3);
4077
+ /* The cap is the box's outer width: it measured 342px for a documented 320 (padding
4078
+ and border outside the cap) until the recipe said so itself. */
4079
+ box-sizing: border-box;
3820
4080
  max-width: var(--aparte-popover-max-width);
3821
4081
  padding: var(--aparte-space-5);
3822
4082
  background: var(--aparte-surface-1);
3823
4083
  border: var(--aparte-border-width) solid var(--aparte-border);
3824
- border-radius: var(--aparte-radius-lg);
4084
+ border-radius: var(--aparte-radius-popover);
3825
4085
  box-shadow: var(--aparte-popover-shadow);
3826
4086
  color: var(--aparte-text);
3827
4087
  font-size: var(--aparte-font-size-md);
@@ -3884,7 +4144,7 @@ details[open] > .aparte-accordion__header .aparte-accordion__icon {
3884
4144
  background: var(--aparte-surface-1);
3885
4145
  color: var(--aparte-text);
3886
4146
  border: var(--aparte-border-width) solid var(--aparte-border);
3887
- border-radius: var(--aparte-radius-xl);
4147
+ border-radius: var(--aparte-radius-dialog);
3888
4148
  box-shadow: var(--aparte-menu-shadow);
3889
4149
  }
3890
4150
 
@@ -3960,11 +4220,20 @@ body:has(.aparte-dialog[open]) {
3960
4220
  .aparte-dialog:modal {
3961
4221
  width: 100%;
3962
4222
  max-width: 100%;
3963
- height: 100dvh;
4223
+ /* As tall as its content, capped at the screen, sitting on the bottom edge: a sheet
4224
+ stretched to 100dvh held a label, a field and two buttons over 586px of nothing.
4225
+ `fit-content`, not `auto` — on a positioned box with both block insets set, an
4226
+ auto height FILLS them (CSS 2 §10.6.4: the auto margins go to 0 and the height
4227
+ is solved for), so `auto` measured the same 720 the explicit 100dvh had. What
4228
+ the UA gives a dialog is `fit-content`, and it is what lets the auto top margin
4229
+ push the sheet onto the bottom edge. And the safe areas: the ✕ used to sit 6px
4230
+ from the physical top. */
4231
+ height: fit-content;
3964
4232
  max-height: 100dvh;
3965
- margin: 0;
4233
+ margin: auto 0 0;
4234
+ padding-block: env(safe-area-inset-top, 0px) env(safe-area-inset-bottom, 0px);
3966
4235
  border: 0;
3967
- border-radius: 0;
4236
+ border-radius: var(--aparte-radius-dialog) var(--aparte-radius-dialog) 0 0;
3968
4237
  }
3969
4238
  }
3970
4239
 
@@ -4001,23 +4270,22 @@ body:has(.aparte-dialog[open]) {
4001
4270
  and skips the dark-theme override block for that reason; the artifact "paper" tokens
4002
4271
  are the same decision made for the same reason.
4003
4272
 
4004
- <span style="position: relative; display: inline-block; margin-block-start: 2.5rem">
4005
- <div class="aparte-tooltip" role="tooltip"
4006
- style="position: absolute; bottom: calc(100% + 8px); left: 50%; translate: -50% 0">
4273
+ <span class="aparte-tooltip-anchor" style="margin-block-start: 2.5rem; margin-inline-start: 5rem">
4274
+ <div class="aparte-tooltip" data-side="top" role="tooltip">
4007
4275
  Copy to clipboard
4008
4276
  <span class="aparte-tooltip__arrow"></span>
4009
4277
  </div>
4010
4278
  <button class="aparte-btn aparte-btn--icon" aria-label="Copy"><aparte-icon name="copy"></aparte-icon></button>
4011
4279
  </span>
4012
4280
 
4013
- The two inline styles are the caller's half, shown rather than described: this file
4014
- draws the box and the arrow, and NOTHING here positions either. An example that
4015
- omitted them rendered as a black label with an arrow pointing at empty space, which
4016
- is what the preview frame beside it showed.
4017
-
4018
- The arrow points DOWN by default — the common case, a tooltip above the thing it
4019
- labels — and `data-side="bottom"` flips it. Placing the box is yours, as with the
4020
- popover; only the arrow's two positions are here.
4281
+ `data-side` places it — `top`, `bottom`, `start` or `end` — against the
4282
+ `.aparte-tooltip-anchor` that wraps the trigger, `--aparte-tooltip-gap` away, and turns
4283
+ the arrow to match. The two inline styles that used to do this in the example were a
4284
+ knob missing, not a division of labour: a demo that needs inline styles to work is a
4285
+ recipe with a parameter it forgot. What stays yours is the viewport: nothing here flips
4286
+ a tooltip that would leave the screen — that needs script, a positioning library's job.
4287
+ Without `data-side` the box is not positioned at all, as before, and its placement is
4288
+ yours (the one inline style left in the example is the demo's own margin).
4021
4289
 
4022
4290
  `pointer-events: none` is not a detail: a tooltip describes what the pointer is over,
4023
4291
  so it must never itself become that thing, or hovering it re-triggers the hover that
@@ -4034,11 +4302,17 @@ body:has(.aparte-dialog[open]) {
4034
4302
 
4035
4303
  position: relative;
4036
4304
  display: inline-block;
4305
+ vertical-align: middle;
4306
+ /* As wide as its label, up to the cap. With a `max-width` and no intrinsic width an
4307
+ absolutely-positioned chip shrinks to `min-content`: "Copy to clipboard" broke into
4308
+ two lines at every width, 71px wide — the widest word plus padding. */
4309
+ width: max-content;
4310
+ box-sizing: border-box;
4037
4311
  max-width: var(--aparte-tooltip-max-width);
4038
4312
  padding: var(--aparte-space-2) var(--aparte-space-4);
4039
4313
  background: var(--aparte-tooltip-bg);
4040
4314
  color: var(--aparte-tooltip-color);
4041
- border-radius: var(--aparte-radius-sm);
4315
+ border-radius: var(--aparte-radius-tooltip);
4042
4316
  box-shadow: var(--aparte-tooltip-shadow);
4043
4317
  font-size: var(--aparte-font-size-sm);
4044
4318
  line-height: var(--aparte-line-height-snug);
@@ -4050,12 +4324,49 @@ body:has(.aparte-dialog[open]) {
4050
4324
  pointer-events: none;
4051
4325
  }
4052
4326
 
4053
- /* Points down by default — the common case of a tooltip sitting above the thing
4054
- it labels. `[data-side="bottom"]` flips it for a tooltip placed below instead.
4055
- Horizontal centring uses inline-start + a negative inline-start margin rather
4056
- than a transform, so it re-centres correctly under `direction: rtl` without a
4057
- second rule. Placement to either side is the caller's own transform math, the
4058
- same "position is the caller's job" split as the popover above. */
4327
+ /* Where the box goes: the anchor is the trigger's wrapper and the tooltip is placed
4328
+ against it. `translate` centres the box on the axis across the side: with a logical
4329
+ inset of 50% the box's start edge sits at the anchor's middle, and moving it back by
4330
+ half its own size centres it — in both writing directions, because the inset flips
4331
+ with the direction and the translate is half the box either way. */
4332
+ .aparte-tooltip-anchor {
4333
+ position: relative;
4334
+ display: inline-block;
4335
+ vertical-align: middle;
4336
+ }
4337
+
4338
+ .aparte-tooltip[data-side] {
4339
+ position: absolute;
4340
+ }
4341
+
4342
+ .aparte-tooltip[data-side='top'] {
4343
+ inset-block-end: calc(100% + var(--aparte-tooltip-gap));
4344
+ inset-inline-start: 50%;
4345
+ translate: -50% 0;
4346
+ }
4347
+
4348
+ .aparte-tooltip[data-side='bottom'] {
4349
+ inset-block-start: calc(100% + var(--aparte-tooltip-gap));
4350
+ inset-inline-start: 50%;
4351
+ translate: -50% 0;
4352
+ }
4353
+
4354
+ .aparte-tooltip[data-side='start'] {
4355
+ inset-inline-end: calc(100% + var(--aparte-tooltip-gap));
4356
+ inset-block-start: 50%;
4357
+ translate: 0 -50%;
4358
+ }
4359
+
4360
+ .aparte-tooltip[data-side='end'] {
4361
+ inset-inline-start: calc(100% + var(--aparte-tooltip-gap));
4362
+ inset-block-start: 50%;
4363
+ translate: 0 -50%;
4364
+ }
4365
+
4366
+ /* The arrow points DOWN by default — the common case, a tooltip above the thing it
4367
+ labels — and each `data-side` turns it toward the anchor. Centring on the box's edge
4368
+ uses a logical inset plus a negative margin rather than a transform, so it re-centres
4369
+ under `direction: rtl` without a second rule. */
4059
4370
  .aparte-tooltip__arrow {
4060
4371
  position: absolute;
4061
4372
  inset-inline-start: 50%;
@@ -4074,6 +4385,26 @@ body:has(.aparte-dialog[open]) {
4074
4385
  inset-block-start: calc(var(--aparte-space-4) * -0.5);
4075
4386
  }
4076
4387
 
4388
+ /* On the two inline sides the arrow sits on the edge that faces the anchor, centred
4389
+ vertically: the box is to the START of the trigger, so its arrow is on its END edge,
4390
+ and the reverse. */
4391
+ .aparte-tooltip[data-side='start'] .aparte-tooltip__arrow {
4392
+ inset-block-end: auto;
4393
+ inset-block-start: 50%;
4394
+ inset-inline-start: auto;
4395
+ inset-inline-end: calc(var(--aparte-space-4) * -0.5);
4396
+ margin-inline-start: 0;
4397
+ margin-block-start: calc(var(--aparte-space-4) * -0.5);
4398
+ }
4399
+
4400
+ .aparte-tooltip[data-side='end'] .aparte-tooltip__arrow {
4401
+ inset-block-end: auto;
4402
+ inset-block-start: 50%;
4403
+ inset-inline-start: calc(var(--aparte-space-4) * -0.5);
4404
+ margin-inline-start: 0;
4405
+ margin-block-start: calc(var(--aparte-space-4) * -0.5);
4406
+ }
4407
+
4077
4408
  /* ── Forced colors ───────────────────────────────────────────────────────────────
4078
4409
  The menu and the popover already carry a real border by default, so those two
4079
4410
  boxes survive forced-colors mode once background and box-shadow are both
@@ -4116,10 +4447,16 @@ aparte-select {
4116
4447
  align-items: center;
4117
4448
  justify-content: space-between;
4118
4449
  gap: var(--aparte-space-4);
4450
+ box-sizing: border-box;
4451
+ min-block-size: var(--aparte-field-size);
4119
4452
  padding: var(--aparte-space-4) var(--aparte-space-6);
4453
+ /* Its own size: the trigger declared none and took the host page's, so every
4454
+ integrator saw a different select. The control step, like the field and the
4455
+ button. */
4456
+ font-size: var(--aparte-font-size-md);
4120
4457
  background: var(--aparte-select-bg, var(--aparte-surface-1));
4121
- border: var(--aparte-border-width) solid var(--aparte-select-border, var(--aparte-border));
4122
- border-radius: var(--aparte-select-radius, var(--aparte-radius-lg));
4458
+ border: var(--aparte-border-width) solid var(--aparte-select-border, var(--aparte-border-control));
4459
+ border-radius: var(--aparte-radius-select);
4123
4460
  cursor: pointer;
4124
4461
  transition: border-color var(--aparte-duration-base), box-shadow var(--aparte-duration-base);
4125
4462
  }
@@ -4128,15 +4465,16 @@ aparte-select {
4128
4465
  border-color: var(--aparte-select-border-hover, var(--aparte-primary));
4129
4466
  }
4130
4467
 
4131
- .aparte-select-trigger:focus {
4132
- outline: none;
4133
- border-color: var(--aparte-select-border-focus, var(--aparte-primary));
4134
- box-shadow: var(--aparte-select-ring, var(--aparte-focus-ring));
4468
+ .aparte-select-trigger:focus-visible {
4469
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
4470
+ outline-offset: var(--aparte-focus-outline-offset);
4135
4471
  }
4136
4472
 
4137
4473
  aparte-select[disabled] .aparte-select-trigger {
4138
- opacity: var(--aparte-disabled-opacity);
4139
4474
  cursor: not-allowed;
4475
+ background: var(--aparte-surface-2);
4476
+ color: var(--aparte-text-muted);
4477
+ border-color: var(--aparte-border);
4140
4478
  }
4141
4479
 
4142
4480
  /* The label is a grid with ONE cell: the visible text and, under it, a hidden stack of
@@ -4196,8 +4534,10 @@ aparte-select[open] .aparte-select-chevron {
4196
4534
  position: fixed;
4197
4535
  /* top / left / width are set dynamically by _updatePosition() */
4198
4536
  background: var(--aparte-select-dropdown-bg, var(--aparte-surface-1));
4199
- border: var(--aparte-border-width) solid var(--aparte-select-border, var(--aparte-border));
4200
- border-radius: var(--aparte-select-radius, var(--aparte-radius-lg));
4537
+ /* The panel is a region, so its edge is the region border; `--aparte-select-border`
4538
+ is the TRIGGER's edge, a control's, and defaults to the control border. */
4539
+ border: var(--aparte-border-width) solid var(--aparte-border);
4540
+ border-radius: var(--aparte-radius-select);
4201
4541
  box-shadow: var(--aparte-select-shadow);
4202
4542
  z-index: var(--aparte-select-z, var(--aparte-z-dropdown));
4203
4543
  max-height: var(--aparte-select-dropdown-max-height);
@@ -4221,8 +4561,11 @@ aparte-select[open] .aparte-select-chevron {
4221
4561
  font-family: inherit;
4222
4562
  }
4223
4563
 
4224
- .aparte-select-search:focus {
4225
- outline: none;
4564
+ /* Inside, not outside: the field sits at the top of a scrolling panel, and an outset
4565
+ ring would be clipped by it. The one documented exception to the shared offset. */
4566
+ .aparte-select-search:focus-visible {
4567
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
4568
+ outline-offset: calc(-1 * var(--aparte-focus-outline-width));
4226
4569
  }
4227
4570
 
4228
4571
  /* Options container */
@@ -4359,6 +4702,7 @@ aparte-optgroup[collapsed] .aparte-optgroup-chevron {
4359
4702
  }
4360
4703
 
4361
4704
  .aparte-spinner-small {
4705
+ box-sizing: border-box;
4362
4706
  width: var(--aparte-select-spinner-size);
4363
4707
  height: var(--aparte-select-spinner-size);
4364
4708
  border: 2px solid currentColor;
@@ -4366,6 +4710,12 @@ aparte-optgroup[collapsed] .aparte-optgroup-chevron {
4366
4710
  border-radius: 50%;
4367
4711
  /* `aparte-spin` is declared once, in base.css — see the shared-motion block there. */
4368
4712
  animation: aparte-spin var(--aparte-duration-spin) linear infinite;
4713
+ }
4714
+
4715
+ /* The element's own sweep in responsive.css already stops this one; the rule is here
4716
+ so the recipe stops on its own terms too, like every other looping recipe. */
4717
+ @media (prefers-reduced-motion: reduce) {
4718
+ .aparte-spinner-small { animation: none; }
4369
4719
  }/**
4370
4720
  * AparteProgressSpinner — CSS
4371
4721
  *
@@ -4391,16 +4741,23 @@ aparte-progress-spinner svg {
4391
4741
  overflow: visible;
4392
4742
  }
4393
4743
 
4744
+ /* The same stroke as the CSS ring (`.aparte-spinner`), in SCREEN pixels: without
4745
+ `vector-effect` a stroke is in viewBox units and scaled with the box — 2.5 of 24 was
4746
+ 1.67px in a 16px spinner, antialiased everywhere and 50 % lighter than its sibling.
4747
+ The track is a visible reference in the control-edge colour: at 15 % of the ink a
4748
+ determinate 62 % was the percentage of a circle nobody could see. */
4394
4749
  .aparte-spinner-track {
4395
4750
  fill: none;
4396
- stroke: var(--aparte-spinner-track, color-mix(in srgb, currentColor 15%, transparent));
4397
- stroke-width: var(--aparte-spinner-stroke, 2.5);
4751
+ stroke: var(--aparte-spinner-track, var(--aparte-border-control));
4752
+ stroke-width: var(--aparte-spinner-thickness);
4753
+ vector-effect: non-scaling-stroke;
4398
4754
  }
4399
4755
 
4400
4756
  .aparte-spinner-fill {
4401
4757
  fill: none;
4402
4758
  stroke: var(--aparte-spinner-color, currentColor);
4403
- stroke-width: var(--aparte-spinner-stroke, 2.5);
4759
+ stroke-width: var(--aparte-spinner-thickness);
4760
+ vector-effect: non-scaling-stroke;
4404
4761
  stroke-linecap: round;
4405
4762
  transition: stroke-dashoffset 0.25s var(--aparte-ease);
4406
4763
  }
@@ -4448,6 +4805,10 @@ aparte-chat,
4448
4805
  height: 100%;
4449
4806
  width: 100%;
4450
4807
  overflow: hidden;
4808
+ /* The height an author sets IS the box: with the bottom gap below outside it,
4809
+ `style="height: 24rem"` rendered 336px, and a split's seam hung 16px under both
4810
+ panels. */
4811
+ box-sizing: border-box;
4451
4812
  /* The composer used to touch the bottom edge as soon as a conversation started: the
4452
4813
  viewport gives 16px above the composer and there was nothing below it. An app
4453
4814
  cannot fix this from outside without padding the container, which also shrinks the
@@ -4519,6 +4880,14 @@ aparte-chat[overlay-composer] > :not(aparte-chat-viewport),
4519
4880
  z-index: var(--aparte-z-raised);
4520
4881
  }
4521
4882
 
4883
+ /* A thing that floats has to be seen floating. The overlaid composer's shell shares the
4884
+ transcript's ground, so a z-index alone left it flat on the words it covers. */
4885
+ aparte-chat[overlay-composer] .aparte-composer-shell,
4886
+ [data-aparte-chat][overlay-composer] .aparte-composer-shell,
4887
+ .aparte-chat-container[overlay-composer] .aparte-composer-shell {
4888
+ box-shadow: var(--aparte-composer-overlay-shadow);
4889
+ }
4890
+
4522
4891
  /* center-empty: the shell centers the composer as a welcome state until the first
4523
4892
  message, then slides to the normal layout. The component toggles [data-empty]
4524
4893
  on itself by watching its viewport — only `flex-grow` animates, so the composer
@@ -4541,6 +4910,17 @@ aparte-chat[center-empty][data-empty] > aparte-chat-viewport {
4541
4910
  height: auto;
4542
4911
  }
4543
4912
 
4913
+ /* And no block room at all: at `auto` the empty viewport still stood 32px tall — the
4914
+ rows' wrapper keeps its block padding with no row in it — so `center` centred three
4915
+ items of which the first was invisible, and the welcome group landed 16px low,
4916
+ measured. The padding goes, not the box: capping the viewport itself left a 32px
4917
+ scroll surface inside a 0px box, and "an empty transcript must not overflow" is a
4918
+ browser test every example runs. Inline geometry is kept — the composer's inset is
4919
+ measured from this element. */
4920
+ aparte-chat[center-empty][data-empty] > aparte-chat-viewport .aparte-messages-wrapper {
4921
+ padding-block: 0;
4922
+ }
4923
+
4544
4924
  /* =========================================
4545
4925
  aparte-chat-viewport
4546
4926
  ========================================= */
@@ -4607,16 +4987,16 @@ aparte-chat-viewport.aparte-viewport--framework::-webkit-scrollbar-thumb {
4607
4987
 
4608
4988
  /* ─── Scroll-to-bottom button ─────────────────────────────────────────────── */
4609
4989
 
4610
- /* The button wears `aparte-btn--lg aparte-btn--circle`, and that IS its shape: 36px is
4611
- exactly `--aparte-btn-size-lg`, and `--circle` is the round one. So the box, the
4612
- corner and the 20px glyph all come from the recipe, and what stays here is only what
4613
- is genuinely this button's — where it floats, and its own surface.
4990
+ /* The button wears `aparte-btn--lg aparte-btn--circle`: `--lg` gives it the 20px glyph
4991
+ and the lg step of the scale (36px), `--circle` the round corner, and
4992
+ `--aparte-scroll-btn-size` (the same step, named for this button so a theme can move
4993
+ it alone) is fed to the recipe below. What stays here is only what is genuinely this
4994
+ button's — where it floats, and its own surface.
4614
4995
 
4615
4996
  It used to redraw all three: its own width/height, its own `border-radius: 50%`, and
4616
4997
  an icon-size override that existed only because the box was declared OUTSIDE the size
4617
- scale, so the glyph stayed at the medium default — a 16px arrow in a 36px circle, 44%
4618
- of its box where the rest of the library reads at ~57%. Naming the size instead makes
4619
- the ratio fall out for free, and the override disappears with it.
4998
+ scale, so the glyph stayed at the medium default — a 16px arrow in a 36px circle.
4999
+ Naming the size on the recipe's own token makes the override disappear.
4620
5000
 
4621
5001
  `--aparte-scroll-btn-size` stays the public knob, fed to the recipe. */
4622
5002
  .aparte-scroll-btn {
@@ -4888,7 +5268,11 @@ aparte-chat-bubble {
4888
5268
  box-sizing: border-box;
4889
5269
  display: flex;
4890
5270
  gap: var(--aparte-message-gap);
4891
- padding: var(--aparte-message-padding);
5271
+ /* The two parts, not the composite `--aparte-message-padding` that used to join them
5272
+ on `:root`: a custom property is substituted where it is declared, so the composite
5273
+ froze both parts at the root and the narrow-container override of them in
5274
+ responsive.css never reached this row. */
5275
+ padding: var(--aparte-message-padding-block) var(--aparte-message-padding-inline);
4892
5276
  width: 100%;
4893
5277
  max-width: var(--aparte-message-max-width);
4894
5278
  margin: 0 auto;
@@ -4976,6 +5360,11 @@ aparte-chat-bubble {
4976
5360
  opacity: 0;
4977
5361
  transition: opacity var(--aparte-duration-base) var(--aparte-ease);
4978
5362
  height: var(--aparte-action-bar-btn-size);
5363
+ /* The INK meets the column, not the box: a glyph is centred in its 24px button, so
5364
+ its ink began half the slack — (24 − 12) / 2 — to the right of the paragraph above.
5365
+ Measured at +5 to +8px in 14 previews. The bar takes that slack back and the box
5366
+ overhangs into the message padding, where nothing else lives. */
5367
+ margin-inline-start: calc((var(--aparte-icon-size-sm) - var(--aparte-btn-size-sm)) / 2);
4979
5368
  }
4980
5369
 
4981
5370
  .aparte-message:hover .aparte-action-bar,
@@ -5040,8 +5429,6 @@ aparte-chat-bubble:last-of-type .aparte-message[data-role="assistant"]:not(.apar
5040
5429
  inset-block-end: calc(-1 * (var(--aparte-message-padding-block) + var(--aparte-message-gap)));
5041
5430
  margin: 0;
5042
5431
  z-index: 1;
5043
- /* Smaller than the last reply's 28px: this one lives in the padding. */
5044
- --aparte-action-bar-btn-size: 24px;
5045
5432
  }
5046
5433
  }
5047
5434
 
@@ -5090,12 +5477,11 @@ aparte-chat-bubble:last-of-type .aparte-message[data-role="assistant"]:not(.apar
5090
5477
  display: none;
5091
5478
  }
5092
5479
 
5480
+ /* The arrows wear `--sm` and the recipe draws the box: this rule used to redraw
5481
+ width/height to a 20px token over it — two sources for one measure. */
5093
5482
  .aparte-branch-prev,
5094
5483
  .aparte-branch-next {
5095
5484
  color: var(--aparte-branch-picker-btn-color);
5096
- width: var(--aparte-branch-picker-btn-size);
5097
- height: var(--aparte-branch-picker-btn-size);
5098
- font-size: var(--aparte-branch-picker-btn-icon-size);
5099
5485
  }
5100
5486
 
5101
5487
  .aparte-branch-prev:hover:not(:disabled),
@@ -5104,9 +5490,11 @@ aparte-chat-bubble:last-of-type .aparte-message[data-role="assistant"]:not(.apar
5104
5490
  background: var(--aparte-action-bar-btn-hover-bg);
5105
5491
  }
5106
5492
 
5493
+ /* The button recipe draws the disabled state (a neutral ground, the muted ink); an
5494
+ opacity over it read the arrow at 1.74:1 — as absent, where the picker means "no
5495
+ further". `default` and not `not-allowed`: the end of a list is not a refusal. */
5107
5496
  .aparte-branch-prev:disabled,
5108
5497
  .aparte-branch-next:disabled {
5109
- opacity: var(--aparte-disabled-opacity);
5110
5498
  cursor: default;
5111
5499
  }
5112
5500
 
@@ -5206,7 +5594,9 @@ aparte-chat-bubble[data-kind="compaction"] .aparte-message-content {
5206
5594
  Unscoped so it greys the composer in EVERY host (vanilla shell + all wrappers),
5207
5595
  not only a `[data-aparte-chat]` direct child. */
5208
5596
  aparte-composer[data-model-gated] {
5209
- opacity: var(--aparte-disabled-opacity);
5597
+ /* No group fade: each control draws its own disabled state (the editor's muted ink,
5598
+ the send button's neutral ground), so the text stays legible while it waits. */
5599
+ cursor: not-allowed;
5210
5600
  }
5211
5601
  aparte-composer[data-model-gated] aparte-composer-send {
5212
5602
  pointer-events: none;
@@ -5362,7 +5752,7 @@ aparte-chat-viewport.aparte-viewport--framework::-webkit-scrollbar-thumb {
5362
5752
 
5363
5753
  /* Disabled state */
5364
5754
  .aparte-editor[contenteditable="false"] {
5365
- opacity: var(--aparte-disabled-opacity);
5755
+ color: var(--aparte-text-muted);
5366
5756
  cursor: not-allowed;
5367
5757
  }
5368
5758
 
@@ -5403,9 +5793,9 @@ aparte-chat-viewport.aparte-viewport--framework::-webkit-scrollbar-thumb {
5403
5793
  }
5404
5794
 
5405
5795
  .aparte-send-button:disabled {
5406
- opacity: var(--aparte-disabled-opacity);
5407
5796
  cursor: not-allowed;
5408
- background: var(--aparte-send-disabled-bg, var(--aparte-primary));
5797
+ background: var(--aparte-send-disabled-bg, var(--aparte-surface-2));
5798
+ color: var(--aparte-text-muted);
5409
5799
  }
5410
5800
 
5411
5801
  /* =========================================
@@ -5499,11 +5889,10 @@ aparte-model-selector[hidden],
5499
5889
  border-color: var(--aparte-primary);
5500
5890
  }
5501
5891
 
5502
- .aparte-provider-select:focus,
5503
- .aparte-model-select:focus {
5504
- outline: none;
5505
- border-color: var(--aparte-primary);
5506
- box-shadow: var(--aparte-focus-ring);
5892
+ .aparte-provider-select:focus-visible,
5893
+ .aparte-model-select:focus-visible {
5894
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
5895
+ outline-offset: var(--aparte-focus-outline-offset);
5507
5896
  }
5508
5897
 
5509
5898
  /* Dark mode overrides for selects */
@@ -5546,13 +5935,14 @@ aparte-composer {
5546
5935
  it would then be the containing block of the `position: fixed` select dropdown
5547
5936
  that exists to escape `overflow: hidden` (select.css, and the same warning in
5548
5937
  split.css). So below 520px the transcript tightens to 8px and this stays at 16.
5549
- A flat 16 was measured against a step: the step wins for vanilla at phone width
5550
- and LOSES for framework-managed, which is the majority path, because the
5551
- transcript's own step targets a class framework mode never builds. Flat is a
5552
- uniform 10px worst case in both modes — which is the scrollbar gutter the
5553
- transcript reserves and the composer cannot, and which is 0 on the overlay
5554
- scrollbars every phone has. */
5555
- padding-inline: var(--aparte-viewport-padding);
5938
+ So the composer does not guess: the viewport MEASURES where its rows start — the
5939
+ transcript's padding plus the scrollbar gutter it reserves on both edges, at
5940
+ whatever step its container query has taken — and publishes it on the chat host as
5941
+ `--aparte-transcript-inset`. Padding by that puts the composer's box on the row's
5942
+ box at every width. Measured before: 10px apart at 768, the gutter's half at 1280,
5943
+ three left edges 12px apart on the reading-column demo. The fallback is for a
5944
+ composer with no viewport beside it. */
5945
+ padding-inline: var(--aparte-transcript-inset, var(--aparte-viewport-padding));
5556
5946
  }
5557
5947
 
5558
5948
  aparte-composer-input {
@@ -5565,7 +5955,7 @@ aparte-composer-input {
5565
5955
  aparte-composer-input .aparte-ci-editor {
5566
5956
  display: block;
5567
5957
  width: 100%;
5568
- min-height: var(--aparte-composer-control-size, 44px);
5958
+ min-height: var(--aparte-composer-control-size);
5569
5959
  padding: var(--aparte-input-padding-y) var(--aparte-input-padding-x);
5570
5960
  font-size: var(--aparte-input-font-size);
5571
5961
  line-height: var(--aparte-input-line-height);
@@ -5596,7 +5986,7 @@ aparte-composer-input .aparte-ci-editor:empty::before {
5596
5986
  }
5597
5987
 
5598
5988
  aparte-composer-input .aparte-ci-editor[aria-disabled="true"] {
5599
- opacity: var(--aparte-disabled-opacity);
5989
+ color: var(--aparte-text-muted);
5600
5990
  cursor: not-allowed;
5601
5991
  }
5602
5992
 
@@ -5688,7 +6078,7 @@ aparte-composer.aparte-is-dragover:not(:has(.aparte-composer-shell)) {
5688
6078
 
5689
6079
  /* Inner row — flex container for input + buttons */
5690
6080
  /* Action buttons sized to the input's single-line height — they derive from
5691
- --aparte-composer-control-size (default 44px), so a theme resizes the whole
6081
+ --aparte-composer-control-size (default: the lg step, 36px; the touch target under a coarse pointer), so a theme resizes the whole
5692
6082
  composer control set with ONE variable and alignment holds. Combined with
5693
6083
  the row's `align-items: flex-end` this makes the buttons read as vertically
5694
6084
  centered against a single line of text, and keeps them anchored to the
@@ -5720,7 +6110,11 @@ aparte-composer.aparte-is-dragover:not(:has(.aparte-composer-shell)) {
5720
6110
  align-items: flex-end;
5721
6111
  gap: var(--aparte-space-4);
5722
6112
  padding: var(--aparte-space-4) var(--aparte-space-6);
5723
- --aparte-composer-row-control-size: var(--aparte-composer-control-size, 44px);
6113
+ /* The lg step at rest — 36px, what `--aparte-send-btn-size` says — and the touch
6114
+ target under a coarse pointer (responsive.css). It was 44 everywhere, which made
6115
+ the send button the one control the coarse-pointer block could not enlarge because
6116
+ it already was, and a 62px bar around 15px of text on a desktop. */
6117
+ --aparte-composer-row-control-size: var(--aparte-composer-control-size);
5724
6118
  --aparte-btn-size: var(--aparte-composer-row-control-size);
5725
6119
  }
5726
6120
 
@@ -5838,6 +6232,10 @@ aparte-composer-cancel .aparte-cc-button:hover {
5838
6232
  `.aparte-accordion__header` / `__item` / `__panel` and adds these two lines: a
5839
6233
  quieter face than a real accordion header, and the padding the left rail needs. */
5840
6234
  .aparte-thinking-header {
6235
+ /* The chevron sits beside the label: the accordion recipe puts it at the row's far
6236
+ end, which is what an accordion is, but this is a one-line disclosure, and "Reasoning"
6237
+ and its chevron were 692px apart at 1280 (T18). */
6238
+ justify-content: flex-start;
5841
6239
  padding: var(--aparte-space-1) 0 var(--aparte-space-1) var(--aparte-space-5);
5842
6240
  font-size: var(--aparte-font-size-sm);
5843
6241
  color: var(--aparte-thinking-text);
@@ -5869,7 +6267,7 @@ aparte-composer-cancel .aparte-cc-button:hover {
5869
6267
  margin-inline-end: var(--aparte-space-4);
5870
6268
  /* Logical property */
5871
6269
  text-transform: uppercase;
5872
- opacity: 0.7;
6270
+ /* The header's ink is already the muted one; an `opacity: .7` on top muted it twice. */
5873
6271
  }
5874
6272
 
5875
6273
 
@@ -5928,11 +6326,14 @@ aparte-composer-cancel .aparte-cc-button:hover {
5928
6326
  flex: none;
5929
6327
  width: var(--aparte-space-8);
5930
6328
  height: var(--aparte-space-8);
5931
- opacity: 0.5;
5932
- transition: transform var(--aparte-duration-base);
6329
+ /* Quiet is a colour, not an opacity: at 0.5 the chevron measured 3.00:1 — on the
6330
+ WCAG floor exactly — and it is the control that reveals a `delete_file`'s
6331
+ arguments. The muted ink at rest, the full ink under the pointer. */
6332
+ color: var(--aparte-text-muted);
6333
+ transition: transform var(--aparte-duration-base), color var(--aparte-duration-base);
5933
6334
  }
5934
6335
  .aparte-tool-toggle svg { width: 100%; height: 100%; display: block; }
5935
- .aparte-tool-summary:hover .aparte-tool-toggle { opacity: 0.9; }
6336
+ .aparte-tool-summary:hover .aparte-tool-toggle { color: var(--aparte-text); }
5936
6337
  details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180deg); }
5937
6338
  /* A rule down the leading edge, so the disclosure reads as belonging to the
5938
6339
  pill above it rather than continuing the message text below. Without it
@@ -5947,7 +6348,7 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
5947
6348
  }
5948
6349
  .aparte-tool-part-label {
5949
6350
  display: block; font-size: var(--aparte-font-size-xs); text-transform: uppercase;
5950
- letter-spacing: 0.04em; opacity: 0.55; margin-bottom: var(--aparte-space-1);
6351
+ letter-spacing: 0.04em; color: var(--aparte-text-muted); margin-bottom: var(--aparte-space-1);
5951
6352
  }
5952
6353
  /* A tool's input and result WRAP. They are a message, a path, a JSON object — not
5953
6354
  code to read line by line — and a `<pre>` keeps `white-space: pre` by default, so
@@ -5969,6 +6370,7 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
5969
6370
  green, which made a finished step shout louder than the reply it belongs to. */
5970
6371
  .aparte-tool-label {
5971
6372
  display: inline-flex;
6373
+ vertical-align: middle;
5972
6374
  align-items: center;
5973
6375
  gap: var(--aparte-space-3);
5974
6376
  min-width: 0;
@@ -5985,7 +6387,7 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
5985
6387
  place-items: center;
5986
6388
  width: 1em; height: 1em;
5987
6389
  flex: none;
5988
- opacity: 0.7;
6390
+ color: var(--aparte-text-muted);
5989
6391
  }
5990
6392
  .aparte-tool-icon svg { width: 1em; height: 1em; display: block; }
5991
6393
  .aparte-tool-name { font-family: var(--aparte-code-font-family); }
@@ -5993,9 +6395,12 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
5993
6395
  Pushed to the far end by DOM order plus an auto inline-start margin, which is
5994
6396
  the placement rule this repo already settled on: no left/right in a name, so
5995
6397
  a right-to-left locale mirrors it for free. */
6398
+ /* Beside the tool's name, not at the row's far end: with `margin-inline-start: auto` the
6399
+ state word drifted 63px from the name at 375 and 596px at 1280. The state belongs to
6400
+ the label (T18). */
5996
6401
  .aparte-tool-state {
5997
- display: inline-flex; align-items: center; gap: var(--aparte-space-2);
5998
- margin-inline-start: auto;
6402
+ display: inline-flex;
6403
+ vertical-align: middle; align-items: center; gap: var(--aparte-space-2);
5999
6404
  font-size: var(--aparte-font-size-sm); font-weight: var(--aparte-font-weight-medium);
6000
6405
  color: var(--aparte-text-muted);
6001
6406
  user-select: none;
@@ -6031,44 +6436,22 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
6031
6436
  }
6032
6437
 
6033
6438
  /* Pipeline waiting */
6034
- .aparte-error-icon-wrapper {
6035
- flex-shrink: 0;
6036
- color: var(--aparte-error-solid);
6037
- display: flex;
6038
- align-items: flex-start;
6039
- padding-top: var(--aparte-space-1);
6040
- }
6041
-
6042
- .aparte-error-icon-wrapper svg {
6043
- width: var(--aparte-error-icon-size);
6044
- height: var(--aparte-error-icon-size);
6045
- }
6046
-
6047
- .aparte-error-content {
6048
- flex: 1;
6049
- min-width: 0;
6050
- }
6051
-
6052
- .aparte-error-title {
6053
- font-weight: var(--aparte-font-weight-semibold);
6054
- font-size: var(--aparte-font-size-lg);
6055
- margin-bottom: var(--aparte-space-2);
6056
- color: var(--aparte-error-title);
6057
- }
6058
-
6059
- .aparte-error-message {
6060
- font-size: var(--aparte-font-size-lg);
6061
- line-height: var(--aparte-line-height-normal);
6062
- }
6063
-
6064
- .aparte-error-details {
6065
- margin-top: var(--aparte-space-4);
6066
- padding-top: var(--aparte-space-4);
6067
- border-top: var(--aparte-border-width) solid var(--aparte-error-border);
6439
+ /* The error segment WEARS the alert recipe — frame and parts (`aparte-alert__icon`,
6440
+ `__body`, `__title`, `__message` come from display/alert.css). It used to put the
6441
+ recipe's class on its root and redraw every part under classes of its own, with a 20px
6442
+ literal for the icon among derived tokens and a title colour nothing else used. What is
6443
+ genuinely the segment's own is the one part the alert has no word for: the details
6444
+ block — a transport's stack, a status line — set as code under the message. */
6445
+ .aparte-segment-error__details {
6446
+ margin: var(--aparte-space-4) 0 0;
6447
+ padding-block-start: var(--aparte-space-4);
6448
+ border-block-start: var(--aparte-border-width) solid var(--aparte-error-border);
6068
6449
  font-family: var(--aparte-code-font-family);
6069
6450
  font-size: var(--aparte-font-size-sm);
6070
- opacity: 0.9;
6451
+ line-height: var(--aparte-line-height-normal);
6452
+ color: var(--aparte-text-secondary);
6071
6453
  white-space: pre-wrap;
6454
+ overflow-wrap: anywhere;
6072
6455
  }
6073
6456
 
6074
6457
  /* ─────────────────────────────────────────────────────────────────────────────
@@ -6182,7 +6565,7 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
6182
6565
  a consumer's screenshot, then here. The padding gives the ring that much room and
6183
6566
  the negative margin gives it back to the layout, so nothing moves; `scroll-padding`
6184
6567
  keeps a keyboard-focused option's ring inside the viewport when the list scrolls. */
6185
- --aparte-elic-ring-room: calc(var(--aparte-focus-outline-width) + var(--aparte-btn-focus-offset));
6568
+ --aparte-elic-ring-room: calc(var(--aparte-focus-outline-width) + var(--aparte-focus-outline-offset));
6186
6569
  padding: var(--aparte-elic-ring-room);
6187
6570
  margin: calc(-1 * var(--aparte-elic-ring-room));
6188
6571
  scroll-padding: var(--aparte-elic-ring-room);
@@ -6226,7 +6609,10 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
6226
6609
  .aparte-elic-options {
6227
6610
  display: flex;
6228
6611
  flex-direction: column;
6229
- gap: var(--aparte-space-1);
6612
+ /* At least the focus ring's outset: at 2px the next row painted over the bottom of
6613
+ the focused row's ring and ~8px of it survived. Any list of options that wears an
6614
+ outset ring needs a gap at least as wide as the offset. */
6615
+ gap: var(--aparte-space-3);
6230
6616
  }
6231
6617
 
6232
6618
  /* The row is `.aparte-field-choice`; the panel only tightens its padding, because a
@@ -6300,22 +6686,19 @@ textarea.aparte-elic-text {
6300
6686
  min-height: var(--aparte-elic-textarea-min-height);
6301
6687
  }
6302
6688
 
6303
- .aparte-elic-other-input:focus,
6304
- .aparte-elic-text:focus {
6305
- border-color: var(--aparte-primary);
6306
- }
6307
-
6308
6689
  .aparte-elic-option--recommended {
6309
6690
  border-color: var(--aparte-primary);
6310
6691
  background: color-mix(in srgb, var(--aparte-primary) 6%, transparent);
6311
6692
  }
6312
6693
  /* One ring at a time. The recommended option is the first to receive focus, and its
6313
6694
  tinted border under the recipe's focus ring read as a double outline (UI audit
6314
- 2026-08-28, §8.3 #9). While it holds focus the ring is the indicator; the tint
6315
- comes back when focus leaves. */
6695
+ 2026-08-28, §8.3 #9). While it holds focus the ring is the one edge, so the border
6696
+ steps aside — and ONLY the border. This rule used to clear the background too, which
6697
+ made the recommended row the only one in the list with no ground at all: sampled on
6698
+ the preview it rendered the page byte for byte while its sisters did not. The
6699
+ recommended state draws in addition to the resting state, never instead of it. */
6316
6700
  .aparte-elic-option--recommended:focus-within {
6317
- background: transparent;
6318
- border-color: var(--aparte-border-focus);
6701
+ border-color: transparent;
6319
6702
  }
6320
6703
 
6321
6704
  /* The whole-request escape, in the corner and out of the way of anything that
@@ -6424,7 +6807,7 @@ textarea.aparte-elic-text {
6424
6807
 
6425
6808
  .aparte-approval-args:focus-visible {
6426
6809
  outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
6427
- outline-offset: 1px;
6810
+ outline-offset: var(--aparte-focus-outline-offset);
6428
6811
  }
6429
6812
 
6430
6813
  /* The refusal's reason, in the user's words. Quiet by default and one line tall: it is
@@ -6444,9 +6827,9 @@ textarea.aparte-elic-text {
6444
6827
  min-height: 1.6em;
6445
6828
  }
6446
6829
 
6447
- .aparte-approval-instruction:focus {
6448
- outline: none;
6449
- border-top-color: var(--aparte-accent);
6830
+ .aparte-approval-instruction:focus-visible {
6831
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
6832
+ outline-offset: var(--aparte-focus-outline-offset);
6450
6833
  }
6451
6834
 
6452
6835
  .aparte-approval-instruction::placeholder {
@@ -6595,8 +6978,8 @@ textarea.aparte-elic-text {
6595
6978
  /* Inside the edge: the list clips horizontally (`overflow-x: hidden` in base.css), and
6596
6979
  a ring drawn outside the row was cut on both sides. */
6597
6980
  .aparte-conv-item__select:focus-visible {
6598
- outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
6599
- outline-offset: calc(-1 * var(--aparte-focus-outline-width));
6981
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
6982
+ outline-offset: var(--aparte-focus-outline-offset);
6600
6983
  }
6601
6984
 
6602
6985
  .aparte-conv-item__title {
@@ -6617,15 +7000,22 @@ textarea.aparte-elic-text {
6617
7000
  font-weight: var(--aparte-conv-item-font-weight-active);
6618
7001
  }
6619
7002
 
7003
+ /* The selection mark: a bar the width of `--aparte-mark-bar` down the row's start
7004
+ edge. Painted as the first pixels of a pseudo the SIZE of the row that inherits its
7005
+ radius, so the bar follows the curve — drawn as a 2px box of its own it stood square
7006
+ in a 9px corner and a sliver of the page's ground showed between the two. Not
7007
+ `overflow: hidden` on the row: that would clip the title button's focus ring. */
6620
7008
  .aparte-conv-item--active::before {
6621
7009
  content: "";
6622
7010
  position: absolute;
6623
- inset-block: 0;
6624
- inset-inline-start: 0;
6625
- width: var(--aparte-mark-bar);
6626
- background: var(--aparte-primary-ink);
7011
+ inset: 0;
7012
+ border-radius: inherit;
7013
+ background: linear-gradient(to right, var(--aparte-primary-ink) var(--aparte-mark-bar), transparent 0);
6627
7014
  pointer-events: none;
6628
7015
  }
7016
+ .aparte-conv-item--active:dir(rtl)::before {
7017
+ background: linear-gradient(to left, var(--aparte-primary-ink) var(--aparte-mark-bar), transparent 0);
7018
+ }
6629
7019
 
6630
7020
  .aparte-conv-item--archived .aparte-conv-item__title {
6631
7021
  font-style: italic;
@@ -6639,8 +7029,8 @@ textarea.aparte-elic-text {
6639
7029
  .aparte-conv-item__more {
6640
7030
  flex-shrink: 0;
6641
7031
  opacity: 0;
6642
- width: var(--aparte-conv-action-btn-size);
6643
- height: var(--aparte-conv-action-btn-size);
7032
+ /* Fed to the recipe, not drawn here: the token names the size, the button draws it. */
7033
+ --aparte-btn-size: var(--aparte-conv-action-btn-size);
6644
7034
  color: var(--aparte-text-muted);
6645
7035
  }
6646
7036
 
@@ -6718,6 +7108,11 @@ aparte-suggestions[data-empty] {
6718
7108
  flex-wrap: wrap;
6719
7109
  gap: var(--aparte-space-2);
6720
7110
  padding-block: var(--aparte-space-2);
7111
+ /* The composer's column: past its 800px cap the chips no longer followed the composer,
7112
+ 188px of offset on a wide host. Same measure, same centring. */
7113
+ box-sizing: border-box;
7114
+ max-inline-size: var(--aparte-message-max-width);
7115
+ margin-inline: auto;
6721
7116
  }
6722
7117
 
6723
7118
  /* A starter's label is a short phrase; it must not become a two-line chip. */
@@ -6753,6 +7148,9 @@ aparte-context[data-empty] {
6753
7148
  .aparte-context .aparte-progress {
6754
7149
  flex: 1;
6755
7150
  min-width: 4rem;
7151
+ /* A gauge has a measure: unbounded, the bar stretched to 1046×4px across a toolbar,
7152
+ 88 % of its width for a line that says one number. */
7153
+ max-inline-size: 16rem;
6756
7154
  }
6757
7155
 
6758
7156
  .aparte-context__text {
@@ -6782,10 +7180,13 @@ aparte-context[data-level="danger"] .aparte-progress__bar {
6782
7180
  transform: rotate(-90deg);
6783
7181
  }
6784
7182
 
7183
+ /* As heavy as the bar variant, in screen pixels: the same gauge was drawn at two
7184
+ weights — a 4px bar and a 2.44px antialiased ring. */
6785
7185
  .aparte-context__track,
6786
7186
  .aparte-context__value {
6787
7187
  fill: none;
6788
- stroke-width: var(--aparte-context-ring-stroke);
7188
+ stroke-width: var(--aparte-progress-height);
7189
+ vector-effect: non-scaling-stroke;
6789
7190
  }
6790
7191
 
6791
7192
  .aparte-context__track {
@@ -6865,7 +7266,9 @@ aparte-scroll-rail[data-empty] {
6865
7266
  padding: 0;
6866
7267
  border: 0;
6867
7268
  border-radius: var(--aparte-radius-full);
6868
- background: var(--aparte-border);
7269
+ /* A tick is a thing you can press; at rest in the region colour it sat at 1.15:1, a
7270
+ 14×2px dash under the perception threshold. */
7271
+ background: var(--aparte-border-control);
6869
7272
  cursor: pointer;
6870
7273
  transition:
6871
7274
  width var(--aparte-duration-fast) var(--aparte-ease),
@@ -6888,7 +7291,7 @@ aparte-scroll-rail[data-empty] {
6888
7291
  }
6889
7292
 
6890
7293
  .aparte-scroll-rail__tick:focus-visible {
6891
- outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
7294
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
6892
7295
  outline-offset: var(--aparte-focus-outline-offset);
6893
7296
  }
6894
7297
 
@@ -6971,7 +7374,10 @@ aparte-sidebar,
6971
7374
  display: flex;
6972
7375
  align-items: center;
6973
7376
  gap: var(--aparte-space-3);
6974
- padding: var(--aparte-space-4) var(--aparte-space-4) var(--aparte-space-2);
7377
+ /* Inline, every region reads `--aparte-sidebar-inset`: the header and footer padded
7378
+ 16 while the search and the body padded 12, and the column's content stood on two
7379
+ vertical axes 4px apart — measured as four left edges on the app-shell demo. */
7380
+ padding: var(--aparte-space-4) var(--aparte-sidebar-inset) var(--aparte-space-2);
6975
7381
  }
6976
7382
 
6977
7383
  .aparte-sidebar__brand {
@@ -6984,8 +7390,8 @@ aparte-sidebar,
6984
7390
  }
6985
7391
 
6986
7392
  .aparte-sidebar__search {
6987
- margin: var(--aparte-space-2) var(--aparte-space-3);
6988
- min-width: calc(var(--aparte-sidebar-width) - 2 * var(--aparte-space-3));
7393
+ margin: var(--aparte-space-2) var(--aparte-sidebar-inset);
7394
+ min-width: calc(var(--aparte-sidebar-width) - 2 * var(--aparte-sidebar-inset));
6989
7395
  }
6990
7396
 
6991
7397
  /* The body is the one region that scrolls; the list inside keeps its own gap. */
@@ -6994,7 +7400,7 @@ aparte-sidebar,
6994
7400
  min-height: 0;
6995
7401
  overflow-y: auto;
6996
7402
  overflow-x: hidden;
6997
- padding: var(--aparte-space-2) var(--aparte-space-3);
7403
+ padding: var(--aparte-space-2) var(--aparte-sidebar-inset);
6998
7404
  scrollbar-width: thin;
6999
7405
  scrollbar-color: var(--aparte-scrollbar-thumb) var(--aparte-scrollbar-track);
7000
7406
  }
@@ -7003,7 +7409,7 @@ aparte-sidebar,
7003
7409
  display: flex;
7004
7410
  align-items: center;
7005
7411
  gap: var(--aparte-space-3);
7006
- padding: var(--aparte-space-3) var(--aparte-space-4);
7412
+ padding: var(--aparte-space-3) var(--aparte-sidebar-inset);
7007
7413
  border-block-start: var(--aparte-border-width) solid var(--aparte-border);
7008
7414
  font-size: var(--aparte-font-size-sm);
7009
7415
  }
@@ -7048,7 +7454,7 @@ aparte-sidebar,
7048
7454
  * under 48rem, where the sidebar is a drawer; above it the column is simply there.
7049
7455
  *
7050
7456
  * <header class="aparte-app-header">
7051
- * <button class="aparte-btn aparte-btn--icon aparte-app-header__toggle" type="button" aria-label="Toggle the sidebar" data-aparte-sidebar-toggle>☰</button>
7457
+ * <button class="aparte-btn aparte-btn--icon aparte-app-header__toggle" type="button" aria-label="Toggle the sidebar" data-aparte-sidebar-toggle><aparte-icon name="menu"></aparte-icon></button>
7052
7458
  * <span class="aparte-app-header__title">Deploy checklist</span>
7053
7459
  * <div class="aparte-app-header__actions">
7054
7460
  * <span class="aparte-tag">gpt-4.1</span>
@@ -7109,15 +7515,38 @@ aparte-sidebar,
7109
7515
  * `height: 100dvh` for a page — and everything inside follows. Under 48rem the sidebar
7110
7516
  * leaves the grid to become a drawer, and the header's toggle appears.
7111
7517
  *
7112
- * <div class="aparte-app-shell" style="height: 18rem">
7518
+ * <div class="aparte-app-shell" style="height: 24rem">
7113
7519
  * <aside class="aparte-sidebar">
7114
- * <div class="aparte-sidebar__header"><span class="aparte-sidebar__brand">aparté</span></div>
7115
- * <div class="aparte-sidebar__body"><aparte-conversation-list></aparte-conversation-list></div>
7520
+ * <div class="aparte-sidebar__header">
7521
+ * <span class="aparte-sidebar__brand">aparté</span>
7522
+ * <button class="aparte-btn aparte-btn--icon aparte-btn--sm" type="button" aria-label="New chat"><aparte-icon name="edit"></aparte-icon></button>
7523
+ * </div>
7524
+ * <div class="aparte-sidebar__search aparte-field-group">
7525
+ * <input class="aparte-field aparte-field--sm" type="search" placeholder="Search">
7526
+ * </div>
7527
+ * <div class="aparte-sidebar__body">
7528
+ * <div class="aparte-conv-group" role="group" aria-label="Today">
7529
+ * <div class="aparte-menu__label aparte-conv-group__label" aria-hidden="true">Today</div>
7530
+ * <div class="aparte-menu__item aparte-conv-item aparte-conv-item--active"><button class="aparte-conv-item__select" type="button"><span class="aparte-conv-item__title">Deploy checklist</span></button></div>
7531
+ * <div class="aparte-menu__item aparte-conv-item"><button class="aparte-conv-item__select" type="button"><span class="aparte-conv-item__title">Rename the staging bucket</span></button></div>
7532
+ * </div>
7533
+ * <div class="aparte-conv-group" role="group" aria-label="Yesterday">
7534
+ * <div class="aparte-menu__label aparte-conv-group__label" aria-hidden="true">Yesterday</div>
7535
+ * <div class="aparte-menu__item aparte-conv-item"><button class="aparte-conv-item__select" type="button"><span class="aparte-conv-item__title">Why is total a float?</span></button></div>
7536
+ * </div>
7537
+ * </div>
7538
+ * <div class="aparte-sidebar__footer"><span class="aparte-avatar aparte-avatar--sm">P</span> Paul</div>
7116
7539
  * </aside>
7117
- * <header class="aparte-app-header"><span class="aparte-app-header__title">Deploy checklist</span></header>
7540
+ * <header class="aparte-app-header">
7541
+ * <span class="aparte-app-header__title">Deploy checklist</span>
7542
+ * <div class="aparte-app-header__actions"><span class="aparte-tag">gpt-4.1</span></div>
7543
+ * </header>
7118
7544
  * <main class="aparte-app-shell__main">
7119
7545
  * <aparte-chat>
7120
- * <aparte-chat-viewport></aparte-chat-viewport>
7546
+ * <aparte-chat-viewport>
7547
+ * <aparte-chat-bubble message-id="u1" data-role="user" content="Walk me through the deploy."></aparte-chat-bubble>
7548
+ * <aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant" content="Build, run the gate, tag, then push the tag — the workflow publishes from it."></aparte-chat-bubble>
7549
+ * </aparte-chat-viewport>
7121
7550
  * <aparte-composer>
7122
7551
  * <div class="aparte-composer-shell">
7123
7552
  * <div class="aparte-composer-row">
@@ -7174,6 +7603,28 @@ aparte-sidebar,
7174
7603
  min-height: 0;
7175
7604
  height: auto;
7176
7605
  }
7606
+
7607
+ /* A phone folds the shell to one column. The sidebar ELEMENT already leaves the grid
7608
+ under 48rem — it becomes a fixed drawer behind the header's toggle — but the grid
7609
+ kept its first column for it, and a class-only sidebar had no story at all: at 375px
7610
+ the shell gave 259 of its 303px to the sidebar and left the chat a 43px band with a
7611
+ send button cut in half. Same query as the element's own, so the recipe teaches the
7612
+ screen the element renders. */
7613
+ @media (max-width: 48rem) {
7614
+ .aparte-app-shell {
7615
+ grid-template-columns: minmax(0, 1fr);
7616
+ }
7617
+ .aparte-app-shell > .aparte-app-header,
7618
+ .aparte-app-shell__main {
7619
+ grid-column: 1;
7620
+ }
7621
+ /* A sidebar that is not (yet) a drawer — the class recipe alone, or the element
7622
+ before it upgrades — folds out of the flow rather than eating the column. */
7623
+ .aparte-app-shell > .aparte-sidebar:not([data-drawer]),
7624
+ .aparte-app-shell > aparte-sidebar:not([data-drawer]) {
7625
+ display: none;
7626
+ }
7627
+ }
7177
7628
  /*
7178
7629
  * aparté — the seam: two panes side by side, and the grid that lets one grow while
7179
7630
  * the other gives way. The fourth piece of the shell, after the sidebar, the header
@@ -7265,35 +7716,47 @@ aparte-split > [data-aparte-chat],
7265
7716
  box-sizing: border-box;
7266
7717
  border: 0;
7267
7718
  padding: 0;
7268
- background: var(--aparte-border);
7719
+ /* A transparent TRACK: the seam is the 1px line painted by `::before`, centred in it,
7720
+ and the grip is a short bar drawn on the track under the pointer. The track used
7721
+ to be the seam — 4px of paint, four times the kit's rule, with nothing to hold. */
7722
+ background: transparent;
7723
+ background-repeat: no-repeat;
7724
+ background-position: center;
7269
7725
  cursor: col-resize;
7270
7726
  touch-action: none;
7727
+ }
7728
+
7729
+ .aparte-split__handle::before {
7730
+ content: '';
7731
+ position: absolute;
7732
+ inset-block: 0;
7733
+ inset-inline-start: calc(50% - var(--aparte-split-seam-width) / 2);
7734
+ inline-size: var(--aparte-split-seam-width);
7735
+ background: var(--aparte-border);
7271
7736
  transition: background-color var(--aparte-duration-fast) var(--aparte-ease);
7272
7737
  }
7273
7738
 
7274
- .aparte-split__handle:hover,
7275
- .aparte-split__handle[data-dragging] {
7739
+ .aparte-split__handle:hover::before,
7740
+ .aparte-split__handle[data-dragging]::before {
7276
7741
  background: var(--aparte-primary);
7277
7742
  }
7278
7743
 
7279
- /* A real outline, and the ring kept as decoration.
7280
-
7281
- The seam's whole keyboard story is arrowing it, and its only focus indicator was
7282
- `--aparte-focus-ring` — a soft wash that measures 1.39:1 against the page in the
7283
- light palette and 1.83:1 in the dark one, where WCAG asks 3:1 of a focus indicator.
7284
- `outline: none` with nothing in its place is the shape of that failure: the
7285
- indicator was not weak, it was absent, on a 4px band with `border: 0` that a
7286
- keyboard user has no other way to find.
7744
+ /* The grip: the convention of every editor's split, so the seam reads as something to
7745
+ take hold of. Drawn as a background so it needs no third element. */
7746
+ .aparte-split__handle:hover,
7747
+ .aparte-split__handle[data-dragging] {
7748
+ background-image: linear-gradient(var(--aparte-primary), var(--aparte-primary));
7749
+ background-size: calc(var(--aparte-split-seam-width) * 3) var(--aparte-space-8);
7750
+ }
7287
7751
 
7288
- The outline is what carries the contrast (3.54:1 light, 7.36:1 dark, measured); the
7289
- shadow stays because it reads as a glow AROUND the seam and the two do not fight.
7290
- The forced-colors entry in responsive.css keeps overriding the colour only — it is
7291
- now overriding an outline that exists rather than substituting for one that does
7292
- not. */
7752
+ /* The one ring. The seam's whole keyboard story is arrowing it, and its only focus
7753
+ indicator used to be a soft wash at 1.39:1 against the page — not weak, absent, on a
7754
+ 4px band with `border: 0` that a keyboard user has no other way to find. The outline
7755
+ carries the contrast (3.54:1 light, 7.36:1 dark, measured), and it is the same outline
7756
+ every other control draws. */
7293
7757
  .aparte-split__handle:focus-visible {
7294
7758
  outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
7295
- outline-offset: 0;
7296
- box-shadow: var(--aparte-focus-ring);
7759
+ outline-offset: var(--aparte-focus-outline-offset);
7297
7760
  }
7298
7761
 
7299
7762
  /* The hit slop: a 4px seam is a 4px target, and a pointer misses it. The pseudo-element
@@ -7382,6 +7845,14 @@ aparte-split[orientation='vertical'][primary='end']:not([data-stacked]),
7382
7845
  aparte-split[orientation='vertical'] > .aparte-split__handle,
7383
7846
  .aparte-split--vertical > .aparte-split__handle {
7384
7847
  cursor: row-resize;
7848
+ background-size: var(--aparte-space-8) calc(var(--aparte-split-seam-width) * 3);
7849
+ }
7850
+ aparte-split[orientation='vertical'] > .aparte-split__handle::before,
7851
+ .aparte-split--vertical > .aparte-split__handle::before {
7852
+ inset-block: calc(50% - var(--aparte-split-seam-width) / 2) auto;
7853
+ inset-inline: 0;
7854
+ inline-size: auto;
7855
+ block-size: var(--aparte-split-seam-width);
7385
7856
  }
7386
7857
 
7387
7858
  aparte-split[orientation='vertical'] > .aparte-split__handle::after,
@@ -7639,6 +8110,13 @@ aparte-split[disabled] > .aparte-split__handle:hover {
7639
8110
  .aparte-code-content-wrapper pre {
7640
8111
  margin: 0;
7641
8112
  padding: var(--aparte-space-6);
8113
+ /* The code typeface, at a code size. This rule declared neither and the block fell
8114
+ into the UA's generic `monospace` (Courier New on Windows) at the PROSE's size and
8115
+ leading; only inline code read the family. On the wrapper's `pre`, whatever class
8116
+ the highlighter puts on its replacement. */
8117
+ font-family: var(--aparte-code-font-family);
8118
+ font-size: var(--aparte-code-block-font-size);
8119
+ line-height: var(--aparte-code-block-line-height);
7642
8120
  /* Wrap long lines instead of a horizontal scrollbar. `pre-wrap` honours
7643
8121
  newlines + indentation but soft-wraps overflowing lines; `overflow-wrap:
7644
8122
  anywhere` breaks unbreakable tokens (long URLs, hashes). Applies to both
@@ -7674,23 +8152,11 @@ aparte-split[disabled] > .aparte-split__handle:hover {
7674
8152
  }
7675
8153
  }
7676
8154
 
7677
- /* Windows high contrast. In forced-colors mode the UA repaints colours and
7678
- drops box-shadow entirely — so the focus indicators drawn with
7679
- --aparte-focus-ring, and the error ring on an avatar, do not merely change
7680
- colour, they VANISH. An outline is painted, and `Highlight` is the system's
7681
- own focus colour, so the indicator survives without fighting the user's
7682
- palette. Only the indicators that disappear are restated here: everything
7683
- whose affordance is text or a border already survives on its own — which the
7684
- split's seam does not, being a 4px band with `border: 0` whose entire keyboard
7685
- story is arrowing it. */
8155
+ /* Windows high contrast. In forced-colors mode the UA repaints colours and drops
8156
+ box-shadow entirely. Every focus indicator is an outline now, which forced colours
8157
+ paint on their own — the one indicator drawn with a box-shadow is the error ring on
8158
+ an avatar, restated here as an outline so it does not vanish. */
7686
8159
  @media (forced-colors: active) {
7687
- .aparte-thumb--image[role='button']:focus-visible,
7688
- .aparte-split__handle:focus-visible,
7689
- .aparte-provider-select:focus,
7690
- .aparte-model-select:focus {
7691
- outline: var(--aparte-focus-outline-width) solid Highlight;
7692
- }
7693
-
7694
8160
  .aparte-message[data-error] .aparte-avatar {
7695
8161
  outline: var(--aparte-focus-outline-width) solid CanvasText;
7696
8162
  }
@@ -7782,7 +8248,13 @@ aparte-split[disabled] > .aparte-split__handle:hover {
7782
8248
  /* Hover cannot reveal the row's ⋯ on touch — keep it visible and tappable. */
7783
8249
  .aparte-conv-item__more {
7784
8250
  opacity: 1;
7785
- --aparte-conv-action-btn-size: 28px;
8251
+ --aparte-conv-action-btn-size: var(--aparte-touch-target-size);
8252
+ }
8253
+
8254
+ /* The composer's own controls: 36 at rest, the touch target here — the send button, the
8255
+ input's action button and the editor's single-line height move together. */
8256
+ .aparte-composer-row {
8257
+ --aparte-composer-control-size: var(--aparte-touch-target-size);
7786
8258
  }
7787
8259
 
7788
8260
  /* A 4px seam is no target for a finger — the same modality rule the buttons above