@aparte/core 0.15.1 → 0.16.1

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 (143) hide show
  1. package/dist/client/aparte-client.d.ts +53 -51
  2. package/dist/client/aparte-client.d.ts.map +1 -1
  3. package/dist/client/lifecycle-events.d.ts +0 -12
  4. package/dist/client/lifecycle-events.d.ts.map +1 -1
  5. package/dist/client/stream-adapter.d.ts +2 -12
  6. package/dist/client/stream-adapter.d.ts.map +1 -1
  7. package/dist/components/bubble/aparte-chat-bubble.d.ts +45 -1
  8. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  9. package/dist/components/chat/aparte-chat.d.ts +1 -1
  10. package/dist/components/composer/aparte-composer-action.d.ts +1 -1
  11. package/dist/components/composer/aparte-composer-add-attachment.d.ts +1 -1
  12. package/dist/components/composer/aparte-composer-attachments.d.ts +1 -2
  13. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  14. package/dist/components/composer/aparte-composer-cancel.d.ts +1 -1
  15. package/dist/components/composer/aparte-composer-input.d.ts +1 -1
  16. package/dist/components/composer/aparte-composer-send.d.ts +12 -12
  17. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  18. package/dist/components/composer/aparte-composer.d.ts +29 -12
  19. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  20. package/dist/components/context/aparte-context.d.ts +31 -8
  21. package/dist/components/context/aparte-context.d.ts.map +1 -1
  22. package/dist/components/conversation-list/aparte-conversation-list.d.ts +93 -36
  23. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  24. package/dist/components/conversation-list/index.d.ts +1 -1
  25. package/dist/components/conversation-list/index.d.ts.map +1 -1
  26. package/dist/components/elicitation/aparte-elicitation.d.ts +20 -2
  27. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  28. package/dist/components/index.d.ts +7 -1
  29. package/dist/components/index.d.ts.map +1 -1
  30. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts +105 -0
  31. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -0
  32. package/dist/components/scroll-rail/index.d.ts +3 -0
  33. package/dist/components/scroll-rail/index.d.ts.map +1 -0
  34. package/dist/components/sidebar/aparte-sidebar.d.ts +186 -0
  35. package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -0
  36. package/dist/components/sidebar/index.d.ts +3 -0
  37. package/dist/components/sidebar/index.d.ts.map +1 -0
  38. package/dist/components/split/aparte-split.d.ts +354 -0
  39. package/dist/components/split/aparte-split.d.ts.map +1 -0
  40. package/dist/components/split/geometry.d.ts +65 -0
  41. package/dist/components/split/geometry.d.ts.map +1 -0
  42. package/dist/components/split/index.d.ts +3 -0
  43. package/dist/components/split/index.d.ts.map +1 -0
  44. package/dist/components/suggestions/aparte-suggestions.d.ts +7 -1
  45. package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
  46. package/dist/components/viewport/aparte-chat-viewport.d.ts +52 -2
  47. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  48. package/dist/config/aparte-config.d.ts +95 -33
  49. package/dist/config/aparte-config.d.ts.map +1 -1
  50. package/dist/config/icon-provider.d.ts +6 -0
  51. package/dist/config/icon-provider.d.ts.map +1 -1
  52. package/dist/config/index.d.ts +2 -2
  53. package/dist/config/index.d.ts.map +1 -1
  54. package/dist/config/locale.d.ts +140 -27
  55. package/dist/config/locale.d.ts.map +1 -1
  56. package/dist/config/sanitize.d.ts +9 -0
  57. package/dist/config/sanitize.d.ts.map +1 -1
  58. package/dist/conversations/conversation-controller.d.ts +13 -1
  59. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  60. package/dist/conversations/conversation-manager.d.ts +9 -0
  61. package/dist/conversations/conversation-manager.d.ts.map +1 -1
  62. package/dist/custom-elements.json +10278 -7784
  63. package/dist/{is-awaiting-reply-BZ_iJ3s3.js → dialog-triggers-CVCxqKg3.js} +718 -1182
  64. package/dist/dialog-triggers-CVCxqKg3.js.map +1 -0
  65. package/dist/elicitation/approval-panel.d.ts +1 -1
  66. package/dist/elicitation/approval-panel.d.ts.map +1 -1
  67. package/dist/elicitation/panel.d.ts +24 -16
  68. package/dist/elicitation/panel.d.ts.map +1 -1
  69. package/dist/elicitation/types.d.ts +31 -0
  70. package/dist/elicitation/types.d.ts.map +1 -1
  71. package/dist/generated/element-attributes.d.ts +47 -1
  72. package/dist/generated/element-attributes.d.ts.map +1 -1
  73. package/dist/{glyphs-B3BZ2TIb.js → glyphs-DQTeOj4w.js} +22 -13
  74. package/dist/{glyphs-B3BZ2TIb.js.map → glyphs-DQTeOj4w.js.map} +1 -1
  75. package/dist/host/aparte-chat-host.d.ts +10 -2
  76. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  77. package/dist/icons/extended.d.ts +4 -2
  78. package/dist/icons/extended.d.ts.map +1 -1
  79. package/dist/icons/glyphs.d.ts +9 -0
  80. package/dist/icons/glyphs.d.ts.map +1 -1
  81. package/dist/icons.js +16 -15
  82. package/dist/icons.js.map +1 -1
  83. package/dist/index.css +1284 -397
  84. package/dist/index.d.ts +26 -15
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +2099 -296
  87. package/dist/index.js.map +1 -1
  88. package/dist/index.node.d.ts +21 -5
  89. package/dist/index.node.d.ts.map +1 -1
  90. package/dist/index.node.js +39 -34
  91. package/dist/index.node.js.map +1 -1
  92. package/dist/interop/dialog-triggers.d.ts +27 -0
  93. package/dist/interop/dialog-triggers.d.ts.map +1 -0
  94. package/dist/interop/element-props.d.ts +16 -8
  95. package/dist/interop/element-props.d.ts.map +1 -1
  96. package/dist/parsers/aparte-stream-parser.d.ts +52 -22
  97. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  98. package/dist/parsers/index.d.ts +1 -1
  99. package/dist/parsers/index.d.ts.map +1 -1
  100. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  101. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  102. package/dist/renderers/segments/code.d.ts.map +1 -1
  103. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  104. package/dist/runtime/message-repository.d.ts.map +1 -1
  105. package/dist/transport/backend-handler.d.ts.map +1 -1
  106. package/dist/transport/backend-transport.d.ts +6 -1
  107. package/dist/transport/backend-transport.d.ts.map +1 -1
  108. package/dist/types/chat.d.ts +8 -14
  109. package/dist/types/chat.d.ts.map +1 -1
  110. package/dist/types/element-map.d.ts +54 -4
  111. package/dist/types/element-map.d.ts.map +1 -1
  112. package/dist/types/event-map.d.ts +16 -10
  113. package/dist/types/event-map.d.ts.map +1 -1
  114. package/dist/types/events.d.ts +59 -117
  115. package/dist/types/events.d.ts.map +1 -1
  116. package/dist/types/imperative-api.d.ts +3 -1
  117. package/dist/types/imperative-api.d.ts.map +1 -1
  118. package/dist/types/index.d.ts +5 -4
  119. package/dist/types/index.d.ts.map +1 -1
  120. package/dist/types/models.d.ts +11 -31
  121. package/dist/types/models.d.ts.map +1 -1
  122. package/dist/types/segments.d.ts +11 -47
  123. package/dist/types/segments.d.ts.map +1 -1
  124. package/dist/types/stream-blocks.d.ts +57 -0
  125. package/dist/types/stream-blocks.d.ts.map +1 -0
  126. package/dist/types/tools.d.ts +57 -0
  127. package/dist/types/tools.d.ts.map +1 -1
  128. package/dist/utils/copy-text.d.ts +24 -0
  129. package/dist/utils/copy-text.d.ts.map +1 -0
  130. package/dist/utils/css-escape.d.ts +5 -0
  131. package/dist/utils/css-escape.d.ts.map +1 -1
  132. package/dist/utils/tool-input.d.ts +20 -0
  133. package/dist/utils/tool-input.d.ts.map +1 -0
  134. package/package.json +10 -3
  135. package/dist/is-awaiting-reply-BZ_iJ3s3.js.map +0 -1
  136. package/dist/renderers/segments/artifact/binary-file.d.ts +0 -45
  137. package/dist/renderers/segments/artifact/binary-file.d.ts.map +0 -1
  138. package/dist/renderers/segments/artifact/card.d.ts +0 -3
  139. package/dist/renderers/segments/artifact/card.d.ts.map +0 -1
  140. package/dist/renderers/segments/artifact/preview-document.d.ts +0 -27
  141. package/dist/renderers/segments/artifact/preview-document.d.ts.map +0 -1
  142. package/dist/renderers/segments/artifact/shared.d.ts +0 -29
  143. package/dist/renderers/segments/artifact/shared.d.ts.map +0 -1
package/dist/index.css CHANGED
@@ -35,13 +35,15 @@
35
35
  --aparte-surface-1: #fffdf8;
36
36
  /* Bubbles, cards */
37
37
  --aparte-surface-2: #f1ebdf;
38
- /* Code blocks, headers */
39
- --aparte-surface-3: #e7dfce;
38
+ /* `--aparte-surface-3` (code blocks, headers) and `--aparte-text-inverse` are no
39
+ longer literals here: both are derived from the masters in the anchored layer
40
+ below, so the eight-line rebrand the theming guide documents actually covers them.
41
+ They were the two palette hexes that survived a rebrand — a consumer on a blue
42
+ brand kept the brass palette's cream for its tooltip ink and its code headers. */
40
43
 
41
44
  /* Text */
42
45
  --aparte-text: #241f2b;
43
46
  --aparte-text-muted: #6d6479;
44
- --aparte-text-inverse: #fffdf8;
45
47
 
46
48
  /* Borders */
47
49
  --aparte-border: #e5ddce;
@@ -87,8 +89,8 @@
87
89
  a large-type page grow twice over. Wanting rem spacing anyway is now a
88
90
  one-line change here — which is the point of having a master at all. */
89
91
  --aparte-space-unit: 2px;
90
- --aparte-radius-unit: 2px;
91
- --aparte-font-scale: 1;
92
+ --aparte-radius-unit: 3px;
93
+ --aparte-font-scale: 1.08;
92
94
 
93
95
  /* Not a step of the radius scale: a pill is a shape, not a size. */
94
96
  --aparte-radius-full: 9999px;
@@ -177,9 +179,9 @@
177
179
  /* Messages */
178
180
  --aparte-message-max-width: 800px;
179
181
 
180
- /* Bubbles opt-in. By default both roles render as a transparent row with
181
- no bubble background; consumers can flip these to get a chat-app look. */
182
- --aparte-message-content-bg-user: #efe7f6;
182
+ /* Bubbles. The user's bubble tint is DERIVED from the accent in the anchored layer
183
+ below (it was a frozen plum hex that outlived every rebrand); the assistant is
184
+ plain prose (AI-chat convention) — flip it to get a two-bubble chat-app look. */
183
185
  --aparte-message-content-bg-assistant: transparent;
184
186
  --aparte-message-content-radius: 14px;
185
187
 
@@ -327,6 +329,34 @@
327
329
 
328
330
  /* Conversation List */
329
331
  --aparte-conv-action-btn-size: 20px;
332
+
333
+ /* Scroll rail — the ticks beside the transcript. Off-scale on purpose: a 2px line
334
+ and a 14px mark are a minimap's measures, not spacing steps. */
335
+ --aparte-scroll-rail-width: 20px;
336
+ --aparte-scroll-rail-tick-size: 14px;
337
+ --aparte-scroll-rail-tick-thickness: 2px;
338
+
339
+ --aparte-scroll-rail-gap: 8px;
340
+
341
+ /* Context gauge, ring variant — a diameter, and a stroke in the ring's own 36-unit
342
+ box. Literals like the rail's: a 22px ring is a toolbar's measure, not a spacing step. */
343
+ --aparte-context-ring-size: 22px;
344
+ --aparte-context-ring-stroke: 4;
345
+
346
+ /* Shell — the sidebar's column, the header's row and the split's seam. */
347
+ --aparte-sidebar-width: 260px;
348
+ --aparte-app-header-height: 48px;
349
+ --aparte-scrim: rgba(0, 0, 0, 0.35);
350
+ /* The split's primary pane and the bounds it moves between. Any length or
351
+ percentage: the browser clamps them in the grid template, so nothing in JS
352
+ parses a unit. The seam is 4px because that is a seam; the grab zone around it
353
+ is 12px because that is a target, and it grows to the touch target on a coarse
354
+ pointer (responsive.css). */
355
+ --aparte-split-position: 38%;
356
+ --aparte-split-min: 20rem;
357
+ --aparte-split-max: 60%;
358
+ --aparte-split-handle-size: 4px;
359
+ --aparte-split-hit-area: 12px;
330
360
  /* Sizes that are not steps of a scale. Each is off-scale on purpose — a 40px
331
361
  file tile is a tile, not fourteen spacing units — so each gets a name rather
332
362
  than sitting as a magic number in a rule. Literal, like every other size
@@ -358,28 +388,6 @@
358
388
  /* Select. Its dropdown shadow used to live as a fallback inside select.css,
359
389
  which meant it had no dark value at all — rgba(0,0,0,0.1) over a dark
360
390
  surface is no shadow. Declared here, it themes like every other. */
361
- /* Paper. An artifact preview is a DOCUMENT shown inside the chat — a spreadsheet,
362
- a rendered page — so it stays light whatever the app theme is, the way a PDF
363
- viewer shows a white page in a dark editor. That intent was already written in a
364
- comment beside a literal `#fff`; naming it is what makes it overridable, and it
365
- is deliberately absent from the dark block so the paper does not follow. */
366
- --aparte-art-paper-bg: #fff;
367
- --aparte-art-paper-text: #1f2937;
368
- --aparte-art-paper-row-alt: #f9fafb;
369
- --aparte-art-paper-head-bg: #f3f4f6;
370
- --aparte-art-paper-head-text: #111827;
371
- --aparte-art-paper-border: rgba(0, 0, 0, 0.1);
372
-
373
- /* File-type tiles. Brand colours, so they are literals on purpose — but named,
374
- because an app with its own file-type palette has nowhere else to put it. */
375
- --aparte-art-file-icon-bg: linear-gradient(135deg, #1d6f42, #0f5132);
376
- /* Fixed, like the tiles it sits on. --aparte-text-inverse would be right for a
377
- THEMED background and is wrong here: it turns near-black in dark mode, which
378
- would put black lettering on a dark green tile. */
379
- --aparte-art-file-icon-color: #fff;
380
- --aparte-art-file-icon-bg-pdf: linear-gradient(135deg, #c0392b, #7d1f17);
381
- --aparte-art-file-icon-bg-docx: linear-gradient(135deg, #1e5288, #0f3060);
382
- --aparte-art-file-error-msg-bg: rgba(0, 0, 0, 0.04);
383
391
 
384
392
  --aparte-line-height-snug: 1.4;
385
393
 
@@ -436,19 +444,13 @@
436
444
  --aparte-skeleton-text-last-width: 60%;
437
445
  --aparte-progress-indeterminate-width: 40%;
438
446
 
439
- --aparte-btn-size-sm: 20px;
440
- --aparte-btn-size-md: 28px;
441
- --aparte-btn-size-lg: 36px;
447
+ --aparte-btn-size-sm: 24px;
448
+ --aparte-btn-size-md: 32px;
449
+ --aparte-btn-size-lg: 40px;
442
450
 
443
451
  --aparte-focus-outline-offset: -2px;
444
452
  --aparte-tool-spinner-size: 10px;
445
453
  --aparte-model-select-chevron-room: 28px;
446
- --aparte-art-card-header-min-height: 36px;
447
- --aparte-art-card-pulse-size: 8px;
448
- --aparte-art-card-btn-size: 28px;
449
- --aparte-art-file-icon-size: 40px;
450
- --aparte-art-file-preview-padding: 20px;
451
- --aparte-art-file-error-padding-inline: 18px;
452
454
  }
453
455
 
454
456
  /* =========================================
@@ -460,10 +462,11 @@
460
462
  --aparte-bg: #17141c;
461
463
  --aparte-surface-1: #211b28;
462
464
  --aparte-surface-2: #2a2333;
463
- --aparte-surface-3: #342a40;
465
+ /* `--aparte-surface-3`, `--aparte-text-inverse` and the user bubble's tint are not
466
+ re-declared here: they derive from the masters (anchored layer), so the dark
467
+ masters above are enough to move them. */
464
468
 
465
469
  /* Bubbles — user only; assistant is plain prose (AI-chat convention) */
466
- --aparte-message-content-bg-user: #2f2740;
467
470
  --aparte-message-content-bg-assistant: transparent;
468
471
 
469
472
  /* Text */
@@ -473,7 +476,6 @@
473
476
  dark for the only reason that this line was missing — which left the scrollbar
474
477
  thumb, and now a neutral badge, at the light value on a dark ground. */
475
478
  --aparte-neutral: #a89bb6;
476
- --aparte-text-inverse: #17141c;
477
479
 
478
480
  /* Borders */
479
481
  --aparte-border: #342a40;
@@ -490,9 +492,6 @@
490
492
  --aparte-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
491
493
  --aparte-switch-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
492
494
  --aparte-range-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
493
- /* Not the paper — that stays light on purpose. This is a code block inside the
494
- error panel, which does follow the theme. */
495
- --aparte-art-file-error-msg-bg: rgba(255, 255, 255, 0.06);
496
495
 
497
496
  /* Error segment */
498
497
  /* The text inks, going the other way: on a dark ground the accent gains
@@ -771,11 +770,26 @@ aparte-chat {
771
770
  --aparte-avatar-error-ring: 0 0 0 2px var(--aparte-error-solid);
772
771
 
773
772
  /* Focus ring — derived from the accent so it themes automatically. */
774
- --aparte-focus-ring: 0 0 0 3px color-mix(in srgb, var(--aparte-primary) 15%, transparent);
773
+ --aparte-focus-ring: 0 0 0 3px color-mix(in srgb, var(--aparte-primary) 30%, transparent);
775
774
 
776
775
  /* Messages */
777
776
  --aparte-message-content-text-user: var(--aparte-text);
778
777
  --aparte-message-content-text-assistant: var(--aparte-text);
778
+ /* The user's bubble: a wash of the accent over the card surface. It was a literal
779
+ (`#efe7f6` light / `#2f2740` dark) — the one palette colour a consumer's eight-line
780
+ rebrand could not reach, so a chat moved to a blue brand kept a plum bubble. Mixed
781
+ in sRGB rather than derived with `oklch(from …)`, because a wash has to keep the
782
+ SURFACE's lightness, not the accent's; 12 % is where the default palette reads as a
783
+ tint, not a fill, on both grounds. */
784
+ --aparte-message-content-bg-user: color-mix(in srgb, var(--aparte-primary) 12%, var(--aparte-surface-1));
785
+ /* The third surface: the second, pulled 6 % toward the text. Close to what the old
786
+ literals encoded (`#e7dfce` from `#f1ebdf` light, `#342a40` from `#2a2333` dark —
787
+ 5 to 9 % per channel, not one figure), now one rule that follows the masters. */
788
+ --aparte-surface-3: color-mix(in srgb, var(--aparte-text) 6%, var(--aparte-surface-2));
789
+ /* Text on the text colour used as a fill (the tooltip, the avatar). The card
790
+ surface is the lightest tone in light and the darkest-but-one in dark, which is
791
+ what the two literals were. */
792
+ --aparte-text-inverse: var(--aparte-surface-1);
779
793
  --aparte-surface: var(--aparte-surface-1);
780
794
  --aparte-surface-hover: var(--aparte-surface-2);
781
795
  --aparte-text-secondary: var(--aparte-text-muted);
@@ -855,6 +869,9 @@ aparte-chat {
855
869
  /* Input (composer) */
856
870
  --aparte-input-line-height: var(--aparte-line-height-normal);
857
871
 
872
+ /* Shell */
873
+ --aparte-sidebar-bg: var(--aparte-surface-2);
874
+
858
875
  /* Conversation List */
859
876
  --aparte-conv-item-radius: var(--aparte-radius-md);
860
877
  --aparte-conv-item-color: var(--aparte-text-muted);
@@ -862,10 +879,6 @@ aparte-chat {
862
879
  --aparte-conv-item-bg-hover: var(--aparte-surface-3);
863
880
  --aparte-conv-item-bg-active: var(--aparte-surface-3);
864
881
  --aparte-conv-item-font-weight-active: var(--aparte-font-weight-medium);
865
- --aparte-conv-delete-color: var(--aparte-text-muted);
866
- --aparte-conv-delete-bg-hover: var(--aparte-error);
867
- --aparte-conv-delete-color-hover: var(--aparte-text-inverse);
868
- --aparte-conv-delete-radius: var(--aparte-radius-sm);
869
882
  }
870
883
 
871
884
  /*
@@ -1292,6 +1305,7 @@ aparte-conversation-list {
1292
1305
  * </label>
1293
1306
  * <input id="email" class="aparte-field" type="email" placeholder="you@example.com" />
1294
1307
  * <p class="aparte-field-hint">We only use this to send a receipt.</p>
1308
+ * <p class="aparte-field-warning">Changing it signs you out of other devices.</p>
1295
1309
  *
1296
1310
  * <div class="aparte-field-group">
1297
1311
  * <span class="aparte-field-group__prefix">https://</span>
@@ -1437,6 +1451,9 @@ aparte-conversation-list {
1437
1451
  background: var(--aparte-surface-1);
1438
1452
  border: var(--aparte-border-width) solid var(--aparte-border);
1439
1453
  border-radius: var(--aparte-field-radius);
1454
+ /* The addons' ground follows the group's corners; the focus ring is a shadow on the
1455
+ group itself, outside this box, so it is not clipped. */
1456
+ overflow: hidden;
1440
1457
  transition:
1441
1458
  border-color var(--aparte-duration-fast) var(--aparte-ease),
1442
1459
  box-shadow var(--aparte-duration-fast) var(--aparte-ease);
@@ -1458,15 +1475,24 @@ aparte-conversation-list {
1458
1475
  box-shadow: none;
1459
1476
  }
1460
1477
 
1478
+ /* An addon has a ground of its own and a rule against the field: on the field's own
1479
+ ground, in muted text, "https://" read as the start of what the user had typed. */
1461
1480
  .aparte-field-group__prefix,
1462
1481
  .aparte-field-group__suffix {
1463
1482
  display: inline-flex;
1464
1483
  align-items: center;
1465
1484
  padding-inline: var(--aparte-space-3);
1485
+ background: var(--aparte-surface-2);
1466
1486
  font-size: var(--aparte-font-size-sm);
1467
1487
  color: var(--aparte-text-muted);
1468
1488
  white-space: nowrap;
1469
1489
  }
1490
+ .aparte-field-group__prefix {
1491
+ border-inline-end: var(--aparte-border-width) solid var(--aparte-border);
1492
+ }
1493
+ .aparte-field-group__suffix {
1494
+ border-inline-start: var(--aparte-border-width) solid var(--aparte-border);
1495
+ }
1470
1496
 
1471
1497
  .aparte-field-group:hover:not(:has(.aparte-field:disabled)) {
1472
1498
  background: var(--aparte-surface-hover);
@@ -1720,10 +1746,15 @@ aparte-conversation-list {
1720
1746
  has to live here where `:checked` can override it — a central default could never
1721
1747
  react to the class + state combo. */
1722
1748
  --aparte-switch-track: var(--aparte-surface-3);
1723
- --aparte-switch-width: 36px;
1724
- --aparte-switch-height: 20px;
1725
- --aparte-switch-thumb-size: 16px;
1749
+ --aparte-switch-width: 40px;
1750
+ --aparte-switch-height: 22px;
1726
1751
  --aparte-switch-thumb-inset: 2px;
1752
+ /* DERIVED from the track, not a fourth literal to keep in step with the other three:
1753
+ the thumb used to be 16px in a 20px track with a 1px border and a 2px top inset —
1754
+ 2px of air above it and none below, visibly low ("le cercle pas centré"). The
1755
+ thumb is what is left once the border and the inset are paid on both sides, so
1756
+ it is centred by construction, whatever the track's size. */
1757
+ --aparte-switch-thumb-size: calc(var(--aparte-switch-height) - 2 * var(--aparte-border-width) - 2 * var(--aparte-switch-thumb-inset));
1727
1758
  /* No shared shadow token fits a small thumb like this one, so it's a plain literal —
1728
1759
  kept the same in both themes rather than reaching for a per-theme split this file
1729
1760
  has no other precedent for. */
@@ -1926,6 +1957,19 @@ aparte-conversation-list {
1926
1957
  color: var(--aparte-error-text);
1927
1958
  }
1928
1959
 
1960
+ /* The third tone of a field's sub-text: a consequence, not a fault. "This setting
1961
+ invalidates the saved states" is neither a hint nor an error, and a shell that had
1962
+ only those two painted it as one of them. Same shape as the error line; the warning
1963
+ ink is the derived one every other warning reads. */
1964
+ .aparte-field-warning {
1965
+ display: flex;
1966
+ align-items: center;
1967
+ gap: var(--aparte-space-1);
1968
+ margin-block-start: var(--aparte-space-1);
1969
+ font-size: var(--aparte-font-size-xs);
1970
+ color: var(--aparte-warning-ink);
1971
+ }
1972
+
1929
1973
  /* A bare colour on the marker, not injected `*` content: a locale that doesn't mark
1930
1974
  required fields with an asterisk still gets a place to say so, in its own text. */
1931
1975
  .aparte-field-required {
@@ -2726,11 +2770,20 @@ aparte-icon {
2726
2770
  =========================================
2727
2771
  Decorative: give the element aria-hidden="true" (or wrap the group behind a
2728
2772
  single aria-busy region) so a screen reader does not read out an empty box.
2729
-
2730
- <span class="aparte-skeleton aparte-skeleton--circle"></span>
2731
- <span class="aparte-skeleton aparte-skeleton--text"></span>
2732
- <span class="aparte-skeleton aparte-skeleton--text" style="inline-size: 60%"></span>
2733
- <span class="aparte-skeleton aparte-skeleton--rect" style="block-size: 64px"></span>
2773
+ A skeleton is a block with no margin of its own: the recipe spaces consecutive
2774
+ text lines and nothing else, so a GROUP is laid out by its container — a grid
2775
+ with a gap, the way the real card it stands in for is. Four lines glued edge to
2776
+ edge is what the example used to show.
2777
+
2778
+ <div style="display: grid; grid-template-columns: auto 1fr; gap: var(--aparte-space-3); align-items: start" aria-busy="true">
2779
+ <span class="aparte-skeleton aparte-skeleton--circle" aria-hidden="true"></span>
2780
+ <div>
2781
+ <span class="aparte-skeleton aparte-skeleton--text" aria-hidden="true"></span>
2782
+ <span class="aparte-skeleton aparte-skeleton--text" aria-hidden="true"></span>
2783
+ <span class="aparte-skeleton aparte-skeleton--text" aria-hidden="true"></span>
2784
+ </div>
2785
+ <span class="aparte-skeleton aparte-skeleton--rect" style="grid-column: 1 / -1; block-size: 64px" aria-hidden="true"></span>
2786
+ </div>
2734
2787
  */
2735
2788
 
2736
2789
  .aparte-skeleton {
@@ -3099,9 +3152,11 @@ aparte-icon {
3099
3152
  * aparté — layered surfaces. Ready-made classes for the things that float ABOVE the
3100
3153
  * transcript: tabs, an accordion, a dropdown menu, a popover, a tooltip.
3101
3154
  *
3102
- * Dialog, drawer and toast are deliberately NOT here. A modal needs a portal and a
3103
- * stack manager — something that owns exactly one top layer for the whole page — and
3104
- * that is the consuming application's job, not a component stylesheet's.
3155
+ * Drawer and toast are deliberately NOT here: each needs a stack manager — something
3156
+ * that owns the order of what floats over the page — and that is the consuming
3157
+ * application's job, not a component stylesheet's. The dialog was on that list until
3158
+ * 0.16.0; it has its own recipe now (`dialog.css`), because the browser's `<dialog>`
3159
+ * brings the top layer, the focus trap and Escape with it.
3105
3160
  *
3106
3161
  * Everything below is the LOOK only. Where and when a surface opens is markup and
3107
3162
  * script the caller owns: a <details open> toggling itself, a positioning library placing
@@ -3439,8 +3494,28 @@ details[open] > .aparte-accordion__header .aparte-accordion__icon {
3439
3494
  <button class="aparte-menu__item" role="menuitem">Retry</button>
3440
3495
  <div class="aparte-menu__separator" role="separator"></div>
3441
3496
  <button class="aparte-menu__item" role="menuitem" disabled>Delete</button>
3497
+ <div class="aparte-menu__separator" role="separator"></div>
3498
+ <span class="aparte-menu__label">Mode</span>
3499
+ <button class="aparte-menu__item" role="menuitemradio" aria-checked="true">
3500
+ <span class="aparte-menu__body">
3501
+ Ask
3502
+ <span class="aparte-menu__description">Every tool call waits for you</span>
3503
+ </span>
3504
+ </button>
3505
+ <button class="aparte-menu__item" role="menuitemradio" aria-checked="false">
3506
+ <span class="aparte-menu__body">
3507
+ Auto
3508
+ <span class="aparte-menu__description">Runs everything, reports after</span>
3509
+ </span>
3510
+ </button>
3442
3511
  </div>
3443
3512
 
3513
+ The check mark is DRAWN HERE, not by you: an item carrying `aria-checked` (a
3514
+ `menuitemradio` or `menuitemcheckbox`) reserves a gutter, and the glyph shows when
3515
+ the value is `"true"` — so set the attribute and do not add a "✓" of your own, or the
3516
+ row shows two. A two-line item wraps its name and its `__description` in a `__body`;
3517
+ the gutter then spans both lines.
3518
+
3444
3519
  WHERE it sits is yours. There is no positioning here and no anchoring logic —
3445
3520
  floating a box next to a trigger needs collision detection and a scroll listener,
3446
3521
  which is an interaction library's job, not a stylesheet's. Same split as the popover
@@ -3548,6 +3623,30 @@ details[open] > .aparte-accordion__header .aparte-accordion__icon {
3548
3623
  visibility: visible;
3549
3624
  }
3550
3625
 
3626
+ /* A two-line item: a name and, under it, what choosing it means (a mode picker, a
3627
+ model with its size). `__body` stacks the two so the check gutter spans both lines
3628
+ — a consumer laid a grid over the item to get exactly this, and that grid was the
3629
+ only piece of specificity CSS his menu needed. */
3630
+ .aparte-menu__body {
3631
+ display: flex;
3632
+ flex-direction: column;
3633
+ align-items: flex-start;
3634
+ gap: calc(var(--aparte-space-1) / 2);
3635
+ min-width: 0;
3636
+ }
3637
+
3638
+ .aparte-menu__description {
3639
+ font-size: var(--aparte-font-size-xs);
3640
+ font-weight: var(--aparte-font-weight-normal);
3641
+ color: var(--aparte-text-muted);
3642
+ text-align: start;
3643
+ }
3644
+
3645
+ .aparte-menu__item:disabled .aparte-menu__description,
3646
+ .aparte-menu__item[aria-disabled='true'] .aparte-menu__description {
3647
+ color: inherit;
3648
+ }
3649
+
3551
3650
  .aparte-menu__separator {
3552
3651
  height: var(--aparte-border-width);
3553
3652
  margin-block: var(--aparte-space-2);
@@ -3600,6 +3699,170 @@ details[open] > .aparte-accordion__header .aparte-accordion__icon {
3600
3699
  line-height: var(--aparte-line-height-normal);
3601
3700
  z-index: var(--aparte-z-dropdown);
3602
3701
  }
3702
+ /*
3703
+ * aparté — the modal, on the browser's own <dialog>.
3704
+ *
3705
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
3706
+ * them concatenated in that same order, the way a browser does.
3707
+ */
3708
+
3709
+ /*
3710
+ * aparte-dialog — a modal, drawn on the native <dialog>.
3711
+ * The kit said for a year that a dialog was "deliberately absent — a modal needs a
3712
+ * portal and a stack manager". The browser has had both since 2022: `<dialog>` with
3713
+ * `showModal()` puts the box in the top layer above everything, traps focus, closes
3714
+ * on Escape, hands the focus back, and draws a `::backdrop`. So this recipe styles
3715
+ * THAT element, and core's one addition is the wiring — a `data-aparte-dialog-open`
3716
+ * on any control opens the dialog it names, a `data-aparte-dialog-close` inside one
3717
+ * closes it, and a click on the backdrop closes it unless the dialog carries
3718
+ * `data-aparte-dialog-static`. Nothing here reaches for position:fixed on your behalf:
3719
+ * the top layer is the browser's.
3720
+ *
3721
+ * <dialog class="aparte-dialog" id="settings" aria-labelledby="settings-title">
3722
+ * <div class="aparte-dialog__header">
3723
+ * <h2 class="aparte-dialog__title" id="settings-title">Settings</h2>
3724
+ * <button class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-dialog__close" type="button" aria-label="Close" data-aparte-dialog-close>
3725
+ * <aparte-icon name="close"></aparte-icon>
3726
+ * </button>
3727
+ * </div>
3728
+ * <div class="aparte-dialog__body">
3729
+ * <label class="aparte-field-label" for="endpoint">Endpoint</label>
3730
+ * <input class="aparte-field" id="endpoint" value="http://localhost:11434/v1">
3731
+ * </div>
3732
+ * <div class="aparte-dialog__footer">
3733
+ * <button class="aparte-btn aparte-btn--ghost" type="button" data-aparte-dialog-close>Cancel</button>
3734
+ * <button class="aparte-btn aparte-btn--primary aparte-btn--solid" type="button" data-aparte-dialog-close="saved">Save</button>
3735
+ * </div>
3736
+ * </dialog>
3737
+ * <button class="aparte-btn aparte-btn--surface" type="button" data-aparte-dialog-open="settings">Open settings</button>
3738
+ *
3739
+ * Sizes: `--sm` (24rem), the default (32rem), `--lg` (48rem); under 30rem of window
3740
+ * every size becomes a full-screen sheet, which is what a settings hub is on a phone.
3741
+ * The value passed to `data-aparte-dialog-close="…"` becomes the dialog's
3742
+ * `returnValue`, read in its native `close` event.
3743
+ */
3744
+ .aparte-dialog {
3745
+ --aparte-dialog-width: 32rem;
3746
+
3747
+ box-sizing: border-box;
3748
+ width: min(var(--aparte-dialog-width), calc(100vw - 2 * var(--aparte-space-8)));
3749
+ max-height: calc(100dvh - 2 * var(--aparte-space-8));
3750
+ margin: auto;
3751
+ padding: 0;
3752
+ display: none;
3753
+ flex-direction: column;
3754
+ overflow: hidden;
3755
+ background: var(--aparte-surface-1);
3756
+ color: var(--aparte-text);
3757
+ border: var(--aparte-border-width) solid var(--aparte-border);
3758
+ border-radius: var(--aparte-radius-xl);
3759
+ box-shadow: var(--aparte-menu-shadow);
3760
+ }
3761
+
3762
+ /* `display: flex` only while open: a closed <dialog> must stay `display: none`, and a
3763
+ flex declaration on the bare class would show it. */
3764
+ .aparte-dialog[open] {
3765
+ display: flex;
3766
+ }
3767
+
3768
+ .aparte-dialog--sm {
3769
+ --aparte-dialog-width: 24rem;
3770
+ }
3771
+
3772
+ .aparte-dialog--lg {
3773
+ --aparte-dialog-width: 48rem;
3774
+ }
3775
+
3776
+ .aparte-dialog::backdrop {
3777
+ background: var(--aparte-scrim);
3778
+ }
3779
+
3780
+ .aparte-dialog__header {
3781
+ display: flex;
3782
+ align-items: center;
3783
+ gap: var(--aparte-space-3);
3784
+ padding: var(--aparte-space-5) var(--aparte-space-6) var(--aparte-space-3);
3785
+ }
3786
+
3787
+ .aparte-dialog__title {
3788
+ flex: 1;
3789
+ min-width: 0;
3790
+ margin: 0;
3791
+ font-size: var(--aparte-font-size-lg);
3792
+ font-weight: var(--aparte-font-weight-semibold);
3793
+ line-height: var(--aparte-line-height-snug);
3794
+ }
3795
+
3796
+ .aparte-dialog__close {
3797
+ flex-shrink: 0;
3798
+ color: var(--aparte-text-muted);
3799
+ }
3800
+
3801
+ /* The body is the one region that scrolls, so a long form never pushes the footer
3802
+ off screen. */
3803
+ .aparte-dialog__body {
3804
+ flex: 1 1 auto;
3805
+ min-height: 0;
3806
+ overflow-y: auto;
3807
+ padding: var(--aparte-space-3) var(--aparte-space-6);
3808
+ display: flex;
3809
+ flex-direction: column;
3810
+ gap: var(--aparte-space-4);
3811
+ }
3812
+
3813
+ .aparte-dialog__footer {
3814
+ display: flex;
3815
+ justify-content: flex-end;
3816
+ gap: var(--aparte-space-3);
3817
+ padding: var(--aparte-space-3) var(--aparte-space-6) var(--aparte-space-5);
3818
+ }
3819
+
3820
+ /* The page behind a modal does not scroll — the browser inerts it, but does not
3821
+ freeze it. Scoped to this class, so a host's own dialogs keep their behaviour. */
3822
+ body:has(.aparte-dialog[open]) {
3823
+ overflow: hidden;
3824
+ }
3825
+
3826
+ /* A phone: every size is the whole screen — when shown MODALLY. `:modal` and not the
3827
+ bare class: a dialog opened with `show()` sits in the flow of its container (the
3828
+ kit's own preview does that), and a sheet the height of the viewport inside a
3829
+ narrow column was the wrong reading of a rule meant for a phone. */
3830
+ @media (max-width: 30rem) {
3831
+ .aparte-dialog:modal {
3832
+ width: 100%;
3833
+ max-width: 100%;
3834
+ height: 100dvh;
3835
+ max-height: 100dvh;
3836
+ margin: 0;
3837
+ border: 0;
3838
+ border-radius: 0;
3839
+ }
3840
+ }
3841
+
3842
+ /* Open and close with a short fade and lift; the reader who prefers reduced motion
3843
+ gets the box where it lands. */
3844
+ @media (prefers-reduced-motion: no-preference) {
3845
+ .aparte-dialog[open] {
3846
+ animation: aparte-dialog-in var(--aparte-duration-slow) var(--aparte-ease);
3847
+ }
3848
+
3849
+ .aparte-dialog[open]::backdrop {
3850
+ animation: aparte-dialog-backdrop-in var(--aparte-duration-slow) var(--aparte-ease);
3851
+ }
3852
+ }
3853
+
3854
+ @keyframes aparte-dialog-in {
3855
+ from {
3856
+ opacity: 0;
3857
+ transform: translateY(var(--aparte-slide-distance));
3858
+ }
3859
+ }
3860
+
3861
+ @keyframes aparte-dialog-backdrop-in {
3862
+ from {
3863
+ opacity: 0;
3864
+ }
3865
+ }
3603
3866
  /* =========================================
3604
3867
  aparte-tooltip — a short label, with an arrow
3605
3868
  =========================================
@@ -4122,6 +4385,11 @@ aparte-chat-viewport {
4122
4385
  overscroll-behavior: contain;
4123
4386
  scrollbar-width: thin;
4124
4387
  scrollbar-color: var(--aparte-scrollbar-thumb) var(--aparte-scrollbar-track);
4388
+ /* The gutter is reserved on BOTH edges whether or not the bar is showing, so the
4389
+ centred column does not shift by half a scrollbar the moment the first reply
4390
+ overflows — the same rule the reference kits' scroll surfaces carry. Symmetric,
4391
+ because the column is centred: a gutter on one side alone moves it. */
4392
+ scrollbar-gutter: stable both-edges;
4125
4393
  }
4126
4394
 
4127
4395
  .aparte-viewport-container::-webkit-scrollbar {
@@ -4186,6 +4454,15 @@ aparte-chat-viewport.aparte-viewport--framework::-webkit-scrollbar-thumb {
4186
4454
  so it can't collide with the framework's children). */
4187
4455
  aparte-chat-viewport.aparte-viewport--framework,
4188
4456
  aparte-chat-viewport[framework-managed] {
4457
+ /* The base rule gives this element `width: 100%`, and core ships no global
4458
+ border-box reset — every `box-sizing` in these sheets is per-element, on purpose.
4459
+ So the padding below was ADDED to 100% and the host overflowed its own chat by
4460
+ 32px, which `aparte-chat`'s `overflow: hidden` then clipped: measured on a 1500px
4461
+ chat, the host was 1532 wide and hung 32px past the end edge. An app with its own
4462
+ `* { box-sizing: border-box }` never saw it, which is exactly why it survived —
4463
+ every example in this repo has one. The composer cannot share a column with a
4464
+ transcript that is off-centre, so it is fixed here rather than compensated there. */
4465
+ box-sizing: border-box;
4189
4466
  overflow-y: auto;
4190
4467
  overflow-x: hidden;
4191
4468
  overscroll-behavior: contain;
@@ -4196,6 +4473,9 @@ aparte-chat-viewport[framework-managed] {
4196
4473
  padding-bottom: calc(var(--aparte-viewport-padding) + var(--aparte-fw-spacer, 0px));
4197
4474
  scrollbar-width: thin;
4198
4475
  scrollbar-color: var(--aparte-scrollbar-thumb) var(--aparte-scrollbar-track);
4476
+ /* Same gutter rule as `.aparte-viewport-container` above: the host is the scroll
4477
+ surface here, so it is the host that reserves it. */
4478
+ scrollbar-gutter: stable both-edges;
4199
4479
  }
4200
4480
 
4201
4481
  /* The framework wrapper renders bubbles as DIRECT flex children of the viewport
@@ -4631,6 +4911,31 @@ aparte-chat-bubble:last-of-type .aparte-message[data-role="assistant"]:not(.apar
4631
4911
  .aparte-message[data-empty] {
4632
4912
  display: none;
4633
4913
  }
4914
+
4915
+ /* ─── The compaction notice ────────────────────────────────────────────────────
4916
+ A message with `compaction: true` — the summary `compact()` injected in place of
4917
+ the turns it replaced — is a notice, not a reply: centred, muted, without avatar,
4918
+ name, time or actions. Its role on the wire is `user` (context handed to the
4919
+ model), and the bubble would otherwise draw it as something the user said. */
4920
+ aparte-chat-bubble[data-kind="compaction"] .aparte-avatar,
4921
+ aparte-chat-bubble[data-kind="compaction"] .aparte-header,
4922
+ aparte-chat-bubble[data-kind="compaction"] .aparte-footer,
4923
+ aparte-chat-bubble[data-kind="compaction"] .aparte-action-bar,
4924
+ aparte-chat-bubble[data-kind="compaction"] .aparte-branch-picker {
4925
+ display: none;
4926
+ }
4927
+
4928
+ aparte-chat-bubble[data-kind="compaction"] .aparte-message {
4929
+ justify-content: center;
4930
+ }
4931
+
4932
+ aparte-chat-bubble[data-kind="compaction"] .aparte-message-content {
4933
+ max-width: 100%;
4934
+ background: var(--aparte-surface-2);
4935
+ color: var(--aparte-text-muted);
4936
+ border: var(--aparte-border-width) dashed var(--aparte-border);
4937
+ font-size: var(--aparte-font-size-sm);
4938
+ }
4634
4939
  /*
4635
4940
  * aparté — The composer and everything it holds: editor, send, attachments, model picker.
4636
4941
  *
@@ -4715,7 +5020,14 @@ aparte-chat-viewport.aparte-viewport--framework::-webkit-scrollbar-thumb {
4715
5020
  border-radius: 50%;
4716
5021
  opacity: 0;
4717
5022
  }
4718
- .aparte-thumb:hover .aparte-thumb__remove {
5023
+ /* Hover is not the only way in. The ✕ is the ONLY way to drop a pending attachment,
5024
+ and at `opacity: 0` it was invisible to a keyboard user standing on it — a focus
5025
+ ring drawn around nothing. `:focus-within` on the tile is the same pair the message
5026
+ action bar (bubble.css) and the conversation row (conversation.css) already use;
5027
+ this one had been written with the hover half alone. The touch half is in
5028
+ responsive.css, beside the row's ⋯, for the same reason: a finger cannot hover. */
5029
+ .aparte-thumb:hover .aparte-thumb__remove,
5030
+ .aparte-thumb:focus-within .aparte-thumb__remove {
4719
5031
  opacity: 1;
4720
5032
  }
4721
5033
  .aparte-thumb__remove:hover {
@@ -4956,6 +5268,34 @@ aparte-composer {
4956
5268
  display: block;
4957
5269
  /* Container-query root — see aparte-chat-viewport. */
4958
5270
  container-type: inline-size;
5271
+ /* The transcript's own inline gutter, read from the token that produces it.
5272
+ `.aparte-composer-shell` and `.aparte-message` both cap at
5273
+ `--aparte-message-max-width` and centre with `margin: 0 auto`, so on a container
5274
+ WIDER than the cap they lined up by construction. Below it the cap stops applying,
5275
+ each becomes 100% of its own parent — and this element had no padding, so the
5276
+ composer went flush to the chat's edges while the transcript kept its inset.
5277
+ Measured at a 512px chat: message column 26/26, composer 0/0. Every chat narrower
5278
+ than 800px was hit — a phone, an embedded widget, either pane of `<aparte-split>`,
5279
+ an app shell whose docked sidebar leaves the chat narrow on a wide window.
5280
+
5281
+ INLINE ONLY, and that is not shorthand-avoidance: the block axis is already spaced
5282
+ deliberately, 16px above from the viewport and 16px below from
5283
+ `--aparte-chat-bottom-gap` (shell.css), which were tuned to match. The `padding`
5284
+ shorthand would add a second 16px to each.
5285
+
5286
+ No 520px step to follow the transcript's, and it is not an oversight: a container
5287
+ query cannot match its own container, and this element IS one (the line above).
5288
+ Nothing above it is a container either, and `aparte-chat` may never become one —
5289
+ it would then be the containing block of the `position: fixed` select dropdown
5290
+ that exists to escape `overflow: hidden` (select.css, and the same warning in
5291
+ split.css). So below 520px the transcript tightens to 8px and this stays at 16.
5292
+ A flat 16 was measured against a step: the step wins for vanilla at phone width
5293
+ and LOSES for framework-managed, which is the majority path, because the
5294
+ transcript's own step targets a class framework mode never builds. Flat is a
5295
+ uniform 10px worst case in both modes — which is the scrollbar gutter the
5296
+ transcript reserves and the composer cannot, and which is 0 on the overlay
5297
+ scrollbars every phone has. */
5298
+ padding-inline: var(--aparte-viewport-padding);
4959
5299
  }
4960
5300
 
4961
5301
  aparte-composer-input {
@@ -5290,7 +5630,16 @@ aparte-composer-cancel .aparte-cc-button:hover {
5290
5630
  reply text beside it for attention it did not deserve. */
5291
5631
  .aparte-tool-summary {
5292
5632
  display: flex; align-items: center; gap: var(--aparte-space-4);
5293
- padding: var(--aparte-space-2) var(--aparte-space-3); margin-inline-start: calc(var(--aparte-space-3) * -1);
5633
+ /* The negative margin is what puts the row's CONTENT on the message column rather
5634
+ than one padding in from it — the padding here is the hover surface, not the
5635
+ column. So it has to be given back on BOTH sides. It used to be
5636
+ `margin-inline-start` alone: the name, the icon and the chevron landed on the
5637
+ column, and the trailing state stopped `--aparte-space-3` short of the right edge
5638
+ every line of prose reaches. Measured at a 512px chat: text L26/R26, the state's
5639
+ right edge at R32 — six pixels, on the one line of a turn whose whole job is to
5640
+ read as a quiet aside beside the text. `margin-inline` puts it at R26 and makes
5641
+ the hover surface symmetric with it. */
5642
+ padding: var(--aparte-space-2) var(--aparte-space-3); margin-inline: calc(var(--aparte-space-3) * -1);
5294
5643
  border-radius: var(--aparte-tool-row-radius);
5295
5644
  cursor: pointer; list-style: none;
5296
5645
  }
@@ -5525,256 +5874,6 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
5525
5874
  border-collapse: collapse;
5526
5875
  }
5527
5876
 
5528
- /*
5529
- * aparté — The artifact card and the artifact file preview.
5530
- *
5531
- * One of the sheets split out of aparte.css. Order matters: src/index.ts imports
5532
- * them in the order listed there, and check:derived-vars reads them concatenated in
5533
- * that same order, the way a browser does.
5534
- */
5535
- /* Artifact card */
5536
- /* The shell is `.aparte-card`; what stays here is what a card in a TRANSCRIPT needs
5537
- and a card in general does not — the vertical rhythm between messages, and `font:
5538
- inherit` so a segment never picks up a host's form-control font. */
5539
- .aparte-segment-artifact-card,
5540
- .aparte-segment-artifact-file {
5541
- margin: var(--aparte-space-4) 0;
5542
- font: inherit;
5543
- }
5544
- .aparte-art-card__header {
5545
- display: flex; align-items: center; justify-content: space-between;
5546
- padding: var(--aparte-space-4) var(--aparte-space-5);
5547
- border-bottom: var(--aparte-border-width) solid var(--aparte-border);
5548
- background: var(--aparte-surface-2);
5549
- min-height: var(--aparte-art-card-header-min-height);
5550
- }
5551
- .aparte-art-card__title-block { display: flex; align-items: center; gap: var(--aparte-space-4); min-width: 0; }
5552
- /* An outline `.aparte-badge`. What stays is the lettering: a language tag reads as a
5553
- code marker, so it is upper-cased and tracked out — which a badge in general is not. */
5554
- .aparte-art-card__kind {
5555
- text-transform: uppercase;
5556
- letter-spacing: 0.04em;
5557
- --aparte-badge-font-size: var(--aparte-font-size-xs);
5558
- --aparte-badge-radius: var(--aparte-radius-sm);
5559
- }
5560
- .aparte-art-card__title {
5561
- font-size: var(--aparte-font-size-lg); font-weight: var(--aparte-font-weight-medium);
5562
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
5563
- }
5564
- /* The geometry is `.aparte-dot`; a streaming card breathes on the accent rather than
5565
- the status colour, and larger, because it marks a whole card and not a line. */
5566
- .aparte-art-card__pulse {
5567
- --aparte-status-dot-size: var(--aparte-art-card-pulse-size);
5568
- --aparte-status-color: var(--aparte-accent);
5569
- }
5570
- .aparte-art-card__actions { display: flex; gap: var(--aparte-space-2); }
5571
- .aparte-art-card__btn {
5572
- /* Same as .aparte-action-btn: declare the size, let the recipe draw. */
5573
- --aparte-btn-size: var(--aparte-art-card-btn-size);
5574
- }
5575
- .aparte-art-card__btn:hover:not(:disabled) {
5576
- background: var(--aparte-surface-hover);
5577
- border-color: var(--aparte-border);
5578
- color: var(--aparte-text);
5579
- }
5580
- .aparte-art-card__btn:disabled { opacity: var(--aparte-disabled-opacity); cursor: not-allowed; }
5581
- .aparte-art-card__tabs {
5582
- /* justify-content and the padding are DECLARED, not left to a default.
5583
- Core is light DOM on purpose - no shadow root, no ::part(), any selector
5584
- reaches in - and the corollary is that a component must state what its
5585
- layout depends on, because an undeclared property has nothing to override
5586
- a host rule with. A consuming page with a bare nav selector setting
5587
- justify-content: space-between and padding-top (this library's own docs
5588
- site had exactly that) otherwise pushes these two tabs to opposite ends of
5589
- the card and pads the row out.
5590
-
5591
- flex-end, and Code first in the DOM. The card OPENS on Code - mounting the
5592
- preview would execute model-authored code with no gesture (ratified
5593
- decision #8) - and a selected tab sitting second reads backwards. Right,
5594
- because the header above puts the artifact's identity on the left and its
5595
- copy/download buttons on the right, so this keeps every control in one
5596
- column. DOM order is also keyboard order, so the tab a reader lands on
5597
- first is the one already showing. */
5598
- display: flex; justify-content: flex-end; align-items: stretch; gap: var(--aparte-space-1);
5599
- padding: var(--aparte-space-2) var(--aparte-space-4) 0;
5600
- border-bottom: var(--aparte-border-width) solid var(--aparte-border);
5601
- background: var(--aparte-surface-2);
5602
- }
5603
- /* The tabs are `.aparte-tabs--underline`, like the elicitation panel's steps — one
5604
- tab in this library, not one per component. Only the row's own business stays. */
5605
- .aparte-art-card__tabs button { font-size: var(--aparte-font-size-sm); }
5606
- /* The card's heights, as variables with ONE owner each.
5607
- They were four hardcoded numbers in the only part of this card that did not
5608
- use a variable - everything else here already reads var(--aparte-code-bg) and
5609
- friends - and two of them had to agree while a third contradicted a fourth:
5610
- the code pane repeated the body's 600px, and the "press Preview" placeholder
5611
- was 120px tall inside a body whose min-height said 80, so that minimum applied
5612
- to nothing.
5613
- The frame stays a FIXED height rather than an aspect ratio, which is what
5614
- embeds of arbitrary HTML actually do - CodeSandbox documents 500px, StackBlitz
5615
- takes a height parameter - because a frame with an opaque origin cannot be
5616
- measured and a 16/10 ratio on a wide card is enormous. The vh cap is the part
5617
- that was missing: a fixed 480px should not own a phone screen.
5618
- Each default lives in its read, as var(--x, literal), the way every other
5619
- value in this file already does - not in a declaration block on top of the
5620
- fallbacks, which would be two owners of one number again. It also means the
5621
- docs' CSS-variable generator finds them: its sweep looks for reads that
5622
- carry a fallback, so a read without one is a public knob nobody documents. */
5623
- .aparte-art-card__body {
5624
- position: relative;
5625
- /* The placeholder is the tallest thing this box can hold while empty, so it
5626
- IS the minimum - one number instead of two that disagreed. */
5627
- min-height: var(--aparte-artifact-pending-height, 120px);
5628
- max-height: var(--aparte-artifact-body-max, 600px);
5629
- overflow: hidden;
5630
- }
5631
- .aparte-art-card__pane { display: none; height: 100%; }
5632
- .aparte-segment-artifact-card[data-tab="code"] .aparte-art-card__pane[data-pane="code"] { display: block; }
5633
- .aparte-segment-artifact-card[data-tab="preview"] .aparte-art-card__pane[data-pane="preview"] { display: block; }
5634
- .aparte-art-card__pane[data-pane="code"] {
5635
- /* The body already caps this; repeating the number was the second owner. */
5636
- max-height: var(--aparte-artifact-body-max, 600px); overflow: auto;
5637
- }
5638
- .aparte-art-card__frame {
5639
- display: block;
5640
- width: 100%;
5641
- height: min(var(--aparte-artifact-frame-height, 480px), var(--aparte-artifact-frame-max, 70vh));
5642
- border: 0;
5643
- background: var(--aparte-art-paper-bg);
5644
- }
5645
- .aparte-art-card__pending {
5646
- display: flex; align-items: center; justify-content: center;
5647
- height: var(--aparte-artifact-pending-height, 120px);
5648
- color: var(--aparte-text-muted);
5649
- font-size: var(--aparte-font-size-lg);
5650
- font-style: italic;
5651
- }
5652
- /* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */
5653
- .aparte-art-file__card {
5654
- display: flex; align-items: center; gap: var(--aparte-space-6);
5655
- padding: var(--aparte-space-6) var(--aparte-space-7);
5656
- background: var(--aparte-surface-2);
5657
- border-bottom: var(--aparte-border-width) solid var(--aparte-border);
5658
- }
5659
- .aparte-art-file__body {
5660
- position: relative;
5661
- }
5662
- .aparte-art-file__code-pane {
5663
- max-height: var(--aparte-artifact-file-code-max, 360px); overflow: auto;
5664
- background: var(--aparte-code-bg);
5665
- }
5666
- .aparte-art-file__preview-pane {
5667
- max-height: var(--aparte-artifact-file-preview-max, 460px);
5668
- overflow: auto;
5669
- background: var(--aparte-art-paper-bg);
5670
- /* Preview is a document view — force light scheme regardless of
5671
- the app theme, with a dark text colour so cells stay readable. */
5672
- color: var(--aparte-art-paper-text);
5673
- }
5674
- .aparte-art-file__icon {
5675
- width: var(--aparte-art-file-icon-size); height: var(--aparte-art-file-icon-size);
5676
- border-radius: var(--aparte-radius-lg);
5677
- display: flex; align-items: center; justify-content: center;
5678
- background: var(--aparte-art-file-icon-bg);
5679
- color: var(--aparte-art-file-icon-color);
5680
- font-weight: var(--aparte-font-weight-bold);
5681
- font-size: var(--aparte-font-size-sm);
5682
- letter-spacing: 0.04em;
5683
- flex-shrink: 0;
5684
- }
5685
- .aparte-art-file__icon[data-kind="pdf"] { background: var(--aparte-art-file-icon-bg-pdf); }
5686
- .aparte-art-file__icon[data-kind="docx"] { background: var(--aparte-art-file-icon-bg-docx); }
5687
- .aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }
5688
- .aparte-art-file__meta-name {
5689
- font-weight: var(--aparte-font-weight-semibold); font-size: var(--aparte-font-size-base);
5690
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
5691
- }
5692
- .aparte-art-file__meta-sub {
5693
- font-size: var(--aparte-font-size-sm);
5694
- color: var(--aparte-text-muted);
5695
- }
5696
- .aparte-art-file__actions { display: flex; gap: var(--aparte-space-3); flex-shrink: 0; }
5697
- /* The chrome is `.aparte-btn`; every one of these is a `--primary --solid`, so the
5698
- fill comes from the recipe. Its own measurements stay: a labelled button in a card
5699
- is roomier than an icon in a row. */
5700
- .aparte-art-file__btn {
5701
- padding: var(--aparte-space-3) var(--aparte-space-5);
5702
- font-size: var(--aparte-font-size-md);
5703
- }
5704
- /* NOTHING here repaints the button, and the comment above is why. These seven lines
5705
- re-declared `background`, `color`, `border-color` and a hover the recipe already
5706
- paints — a migration leftover that contradicted the sentence directly above it.
5707
-
5708
- Five of them were inert duplicates. The sixth was not: `color: var(--aparte-text-inverse)`
5709
- overrode `--aparte-btn-on-intent`, which the recipe derives from the fill. Measured in a
5710
- browser on the built stylesheet: 3.54:1 in the light theme against the recipe's 5.27 —
5711
- an AA failure on a visible button label, in the default theme. It was also the last
5712
- place in `styles/` forcing `--aparte-text-inverse` as ink on a coloured fill; badge and
5713
- field had already stopped.
5714
-
5715
- The consumer cost was the sharper one: the one-attribute rebrand this library documents
5716
- re-derives the ink on every other solid-primary button and, here alone, kept a token
5717
- bound to core's own palette. `.aparte-art-file__btn:hover` went with them — at 0,2,0 it
5718
- was always beaten by `.aparte-btn--solid:hover:not(:disabled)` at 0,3,0, so it never
5719
- applied either. */
5720
- /* Hide the download button until the sandbox has produced the buffer.
5721
- Avoids confusing the user with a disabled-but-styled-primary button
5722
- during streaming / compiling. */
5723
- .aparte-segment-artifact-file:not([data-state="ready"]) .aparte-art-file__btn[data-action="download"] {
5724
- display: none;
5725
- }
5726
- .aparte-art-file__preview-pane th,
5727
- .aparte-art-file__preview-pane td {
5728
- border: var(--aparte-border-width) solid var(--aparte-art-paper-border);
5729
- padding: var(--aparte-space-3) var(--aparte-space-5);
5730
- text-align: left;
5731
- white-space: nowrap;
5732
- color: var(--aparte-art-paper-text);
5733
- background: var(--aparte-art-paper-bg);
5734
- }
5735
- .aparte-art-file__preview-pane tr:nth-child(odd) td { background: var(--aparte-art-paper-row-alt); }
5736
- .aparte-art-file__preview-pane tr:first-child td {
5737
- background: var(--aparte-art-paper-head-bg);
5738
- font-weight: var(--aparte-font-weight-semibold);
5739
- position: sticky; top: 0;
5740
- color: var(--aparte-art-paper-head-text);
5741
- }
5742
- .aparte-art-file__preview-empty {
5743
- padding: var(--aparte-art-file-preview-padding); text-align: center;
5744
- color: var(--aparte-text-muted);
5745
- font-size: var(--aparte-font-size-lg); font-style: italic;
5746
- }
5747
- .aparte-segment-artifact-file[data-state="error"] .aparte-art-file__icon {
5748
- background: var(--aparte-art-file-icon-bg-pdf);
5749
- }
5750
- .aparte-art-file__error {
5751
- padding: var(--aparte-space-8) var(--aparte-art-file-error-padding-inline);
5752
- background: var(--aparte-error-bg);
5753
- border-top: var(--aparte-border-width) solid var(--aparte-error-border);
5754
- color: var(--aparte-text);
5755
- }
5756
- .aparte-art-file__error-title {
5757
- font-weight: var(--aparte-font-weight-semibold);
5758
- font-size: var(--aparte-font-size-lg);
5759
- margin-bottom: var(--aparte-space-3);
5760
- color: var(--aparte-error-title);
5761
- }
5762
- /* A computed value shown back, which is `.aparte-output`. What stays is that this
5763
- one is an ERROR — tinted text on its own quiet ground — and that a raw message has
5764
- no spaces to wrap on, so it must be allowed to break mid-word. */
5765
- .aparte-art-file__error-msg {
5766
- display: block;
5767
- padding: var(--aparte-space-3) var(--aparte-space-5);
5768
- margin-bottom: var(--aparte-space-4);
5769
- background: var(--aparte-art-file-error-msg-bg);
5770
- color: var(--aparte-error-text);
5771
- word-break: break-word;
5772
- }
5773
- .aparte-art-file__error-hint {
5774
- font-size: var(--aparte-font-size-sm);
5775
- color: var(--aparte-text-muted);
5776
- font-style: italic;
5777
- }
5778
5877
  /*
5779
5878
  * aparté — The elicitation panel and the approval gate.
5780
5879
  *
@@ -6023,6 +6122,43 @@ textarea.aparte-elic-text {
6023
6122
  recipe. See `buildApprovalPanel` for why colour lost: an alert's vocabulary on a
6024
6123
  control, and 2.19:1 on the dark palette when it was tried as a fill instead. */
6025
6124
 
6125
+ /* What is being decided, between the question and the options.
6126
+
6127
+ The panel used to carry the tool's NAME and nothing else, so the arguments — the
6128
+ whole of what a person approves — were only in the transcript row, behind a closed
6129
+ disclosure. Capped and scrollable rather than unbounded: the panel itself is capped
6130
+ at half the viewport, and a plan or a diff arriving as a tool argument would
6131
+ otherwise push the Approve button off the bottom of the surface it is asked on.
6132
+
6133
+ Monospace, because it is JSON: the model's own bytes, not prose about them. */
6134
+ .aparte-approval-args-label {
6135
+ margin: var(--aparte-space-3) var(--aparte-space-3) 0;
6136
+ font-size: var(--aparte-font-size-xs);
6137
+ text-transform: uppercase;
6138
+ letter-spacing: 0.04em;
6139
+ color: var(--aparte-text-muted);
6140
+ }
6141
+
6142
+ .aparte-approval-args {
6143
+ margin: var(--aparte-space-1) var(--aparte-space-3) 0;
6144
+ padding: var(--aparte-space-3);
6145
+ max-height: var(--aparte-approval-args-max-height, 8.5rem);
6146
+ overflow: auto;
6147
+ font-family: var(--aparte-code-font-family);
6148
+ font-size: var(--aparte-approval-args-size, var(--aparte-font-size-sm));
6149
+ line-height: var(--aparte-line-height-snug);
6150
+ color: var(--aparte-text);
6151
+ background: var(--aparte-surface-2);
6152
+ border-radius: var(--aparte-radius-sm);
6153
+ white-space: pre-wrap;
6154
+ overflow-wrap: anywhere;
6155
+ }
6156
+
6157
+ .aparte-approval-args:focus-visible {
6158
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
6159
+ outline-offset: 1px;
6160
+ }
6161
+
6026
6162
  /* The refusal's reason, in the user's words. Quiet by default and one line tall: it is
6027
6163
  the exception, not the expected act, and a box sized for a paragraph invites one. */
6028
6164
  .aparte-approval-instruction {
@@ -6088,11 +6224,27 @@ textarea.aparte-elic-text {
6088
6224
  margin-bottom: calc(-1 * var(--aparte-border-width));
6089
6225
  }
6090
6226
 
6091
- /* Answered: readable, but it is not where you are. */
6227
+ /* Answered: readable, but it is not where you are — and marked, because a colour is
6228
+ what the current chip has too. The mark is the `check` glyph, sized by the chip. */
6092
6229
  .aparte-elic-step[data-answered] {
6093
6230
  color: var(--aparte-text);
6094
6231
  }
6095
6232
 
6233
+ .aparte-elic-step__mark {
6234
+ display: inline-flex;
6235
+ align-items: center;
6236
+ margin-inline-end: var(--aparte-space-1);
6237
+ color: var(--aparte-success);
6238
+ --aparte-icon-size: 0.85em;
6239
+ }
6240
+
6241
+ /* The tag beside a recommended option's label: the recipe draws it, this only keeps it
6242
+ from stretching the title line. */
6243
+ .aparte-elic-option-badge {
6244
+ vertical-align: middle;
6245
+ margin-inline-start: var(--aparte-space-1);
6246
+ }
6247
+
6096
6248
  .aparte-elic-step[aria-selected='true'] {
6097
6249
  color: var(--aparte-text);
6098
6250
  font-weight: var(--aparte-font-weight-semibold);
@@ -6110,16 +6262,37 @@ textarea.aparte-elic-text {
6110
6262
  * them in the order listed there, and check:derived-vars reads them concatenated in
6111
6263
  * that same order, the way a browser does.
6112
6264
  */
6265
+
6266
+ /* A date group: its heading wears the menu's label recipe (`.aparte-menu__label`),
6267
+ because that is what a heading over a list of rows is. Only the rhythm is its own. */
6268
+ .aparte-conv-group {
6269
+ display: flex;
6270
+ flex-direction: column;
6271
+ gap: var(--aparte-conv-list-gap);
6272
+ }
6273
+
6274
+ .aparte-conv-group + .aparte-conv-group {
6275
+ margin-block-start: var(--aparte-space-3);
6276
+ }
6277
+
6113
6278
  /* A row in a list, which is what `.aparte-menu__item` is. What stays is what belongs
6114
6279
  to a CONVERSATION row: it truncates, so it needs `min-width: 0`; and its own
6115
- measurements, because a sidebar row is tighter than a dropdown row. */
6280
+ measurements, because a sidebar row is tighter than a dropdown row.
6281
+
6282
+ The row is a flex box around two real buttons — the title, which selects, and the
6283
+ `⋯`. The padding sits on the title button, not the row, so that the button's focus
6284
+ ring is the row's shape and a click on the row's inner margin lands on the button
6285
+ rather than on nothing. */
6116
6286
  .aparte-conv-item {
6287
+ position: relative;
6117
6288
  min-width: 0;
6118
6289
  gap: var(--aparte-conv-item-gap);
6119
- padding: var(--aparte-conv-item-padding);
6290
+ padding: 0;
6291
+ padding-inline-end: var(--aparte-space-2);
6120
6292
  border-radius: var(--aparte-conv-item-radius);
6121
6293
  color: var(--aparte-conv-item-color);
6122
6294
  font-size: var(--aparte-conv-item-font-size);
6295
+ cursor: default;
6123
6296
  }
6124
6297
 
6125
6298
  /* Hover moves the ground UP, so the label has to move with it.
@@ -6134,31 +6307,36 @@ textarea.aparte-elic-text {
6134
6307
  color: var(--aparte-conv-item-color-active);
6135
6308
  }
6136
6309
 
6137
- .aparte-conv-item:focus-visible {
6138
- outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
6139
- outline-offset: var(--aparte-focus-outline-offset);
6140
- }
6141
-
6142
- .aparte-conv-item:hover .aparte-conv-item__delete {
6143
- opacity: 1;
6310
+ .aparte-conv-item__select {
6311
+ appearance: none;
6312
+ -webkit-appearance: none;
6313
+ display: flex;
6314
+ align-items: center;
6315
+ flex: 1;
6316
+ min-width: 0;
6317
+ padding: var(--aparte-conv-item-padding);
6318
+ border: none;
6319
+ border-radius: inherit;
6320
+ background: transparent;
6321
+ color: inherit;
6322
+ font: inherit;
6323
+ text-align: start;
6324
+ cursor: pointer;
6144
6325
  }
6145
6326
 
6146
- .aparte-conv-item:hover .aparte-conv-item__archive {
6147
- opacity: 1;
6327
+ /* Inside the edge: the list clips horizontally (`overflow-x: hidden` in base.css), and
6328
+ a ring drawn outside the row was cut on both sides. */
6329
+ .aparte-conv-item__select:focus-visible {
6330
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
6331
+ outline-offset: calc(-1 * var(--aparte-focus-outline-width));
6148
6332
  }
6149
6333
 
6150
- /* Keyboard access (WCAG 2.1.1): the archive/delete actions are opacity:0 until
6151
- hover, which would hide keyboard focus. Reveal + ring them on focus. */
6152
- .aparte-conv-item:focus-within .aparte-conv-item__archive,
6153
- .aparte-conv-item:focus-within .aparte-conv-item__delete {
6154
- opacity: 1;
6155
- }
6156
-
6157
- .aparte-conv-item__archive:focus-visible,
6158
- .aparte-conv-item__delete:focus-visible {
6159
- opacity: 1;
6160
- outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
6161
- outline-offset: var(--aparte-focus-outline-offset);
6334
+ .aparte-conv-item__title {
6335
+ flex: 1;
6336
+ white-space: nowrap;
6337
+ overflow: hidden;
6338
+ text-overflow: ellipsis;
6339
+ min-width: 0;
6162
6340
  }
6163
6341
 
6164
6342
  /* The conversation you are in wears the mark's bar (display/mark.css) on its start
@@ -6166,7 +6344,6 @@ textarea.aparte-elic-text {
6166
6344
  a checked field choice carry. Its ground keeps the list's own tokens: a navigation
6167
6345
  list is read for a long time, and the mark's tint on every visit was too loud. */
6168
6346
  .aparte-conv-item--active {
6169
- position: relative;
6170
6347
  background: var(--aparte-conv-item-bg-active);
6171
6348
  color: var(--aparte-conv-item-color-active);
6172
6349
  font-weight: var(--aparte-conv-item-font-weight-active);
@@ -6182,61 +6359,73 @@ textarea.aparte-elic-text {
6182
6359
  pointer-events: none;
6183
6360
  }
6184
6361
 
6185
- .aparte-conv-item--active .aparte-conv-item__delete {
6186
- opacity: 0;
6362
+ .aparte-conv-item--archived .aparte-conv-item__title {
6363
+ font-style: italic;
6364
+ opacity: 0.85;
6187
6365
  }
6188
6366
 
6189
- .aparte-conv-item--active .aparte-conv-item__archive {
6367
+ /* The `⋯`: one quiet button per row, shown by hover, by focus, on the active row and
6368
+ while its menu is open — never a permanent control, and never a red one. What the
6369
+ recipe gives (shape, states, focus ring) it keeps; only the size and the resting
6370
+ colour are the row's. */
6371
+ .aparte-conv-item__more {
6372
+ flex-shrink: 0;
6190
6373
  opacity: 0;
6374
+ width: var(--aparte-conv-action-btn-size);
6375
+ height: var(--aparte-conv-action-btn-size);
6376
+ color: var(--aparte-text-muted);
6191
6377
  }
6192
6378
 
6193
- .aparte-conv-item__title {
6379
+ .aparte-conv-item:hover .aparte-conv-item__more,
6380
+ .aparte-conv-item:focus-within .aparte-conv-item__more,
6381
+ .aparte-conv-item--active .aparte-conv-item__more,
6382
+ .aparte-conv-item__more[aria-expanded="true"] {
6383
+ opacity: 1;
6384
+ }
6385
+
6386
+ .aparte-conv-item__more:hover {
6387
+ color: var(--aparte-text);
6388
+ }
6389
+
6390
+ /* The rename input takes the title button's place, so it inherits the row's slot and
6391
+ only needs the field recipe plus a little air. */
6392
+ .aparte-conv-item__input {
6194
6393
  flex: 1;
6195
- white-space: nowrap;
6196
- overflow: hidden;
6197
- text-overflow: ellipsis;
6198
6394
  min-width: 0;
6395
+ margin: var(--aparte-space-1) 0 var(--aparte-space-1) var(--aparte-space-1);
6199
6396
  }
6200
6397
 
6201
- .aparte-conv-item__delete {
6202
- --aparte-btn-radius: var(--aparte-conv-delete-radius);
6203
- opacity: 0;
6204
- width: var(--aparte-conv-action-btn-size);
6205
- height: var(--aparte-conv-action-btn-size);
6206
- color: var(--aparte-conv-delete-color);
6207
- /* DECLARED, not out-specified. `.aparte-btn:hover:not(:disabled)` weighs 0,3,0 and
6208
- `.aparte-conv-item__delete:hover` only 0,2,0, so the recipe won the background and
6209
- this button's red never applied — while the `color` below DID, because the recipe's
6210
- hover rule sets no colour. The result was the ink for a solid red fill painted on a
6211
- neutral surface: 1.17:1 in the light theme, 1.20 in the dark, measured in a browser.
6212
- The cross was invisible in both. Feeding the recipe its own token is the rule this
6213
- file's neighbours already follow, and it makes the pair agree again — 3.70 and 4.84,
6214
- both clear of the 3:1 a graphical object needs. */
6215
- --aparte-btn-bg-hover: var(--aparte-conv-delete-bg-hover);
6398
+ /* The row's menu: the menu recipe draws it, the element places it (`position: fixed`,
6399
+ `top`/`left` computed at open time, closed on any scroll). Fixed rather than
6400
+ absolute because the list scrolls, and an absolute menu on the last rows was clipped
6401
+ by that overflow. */
6402
+ .aparte-conv-menu {
6403
+ position: fixed;
6404
+ --aparte-icon-size: var(--aparte-icon-size-sm);
6216
6405
  }
6217
6406
 
6218
- .aparte-conv-item__delete:hover {
6219
- color: var(--aparte-conv-delete-color-hover);
6407
+ .aparte-conv-menu__item--danger {
6408
+ color: var(--aparte-error);
6220
6409
  }
6221
6410
 
6222
- .aparte-conv-item__archive {
6223
- --aparte-btn-radius: var(--aparte-conv-archive-radius);
6224
- opacity: 0;
6225
- width: var(--aparte-conv-action-btn-size);
6226
- height: var(--aparte-conv-action-btn-size);
6227
- color: var(--aparte-conv-archive-color, var(--aparte-text-muted));
6228
- /* Same reason as the delete button above: the recipe out-weighs a plain `:hover` here,
6229
- so this surface has to be declared as its token or it never applies. */
6230
- --aparte-btn-bg-hover: var(--aparte-conv-archive-bg-hover, var(--aparte-surface-4, var(--aparte-surface-3)));
6411
+ /* The one question the menu asks. */
6412
+ .aparte-conv-menu__confirm {
6413
+ display: flex;
6414
+ flex-direction: column;
6415
+ gap: var(--aparte-space-3);
6416
+ padding: var(--aparte-space-2) var(--aparte-space-3);
6231
6417
  }
6232
6418
 
6233
- .aparte-conv-item__archive:hover {
6234
- color: var(--aparte-conv-archive-color-hover, var(--aparte-text));
6419
+ .aparte-conv-menu__question {
6420
+ margin: 0;
6421
+ font-size: var(--aparte-font-size-sm);
6422
+ line-height: var(--aparte-line-height-snug);
6235
6423
  }
6236
6424
 
6237
- .aparte-conv-item--archived .aparte-conv-item__title {
6238
- font-style: italic;
6239
- opacity: 0.85;
6425
+ .aparte-conv-menu__actions {
6426
+ display: flex;
6427
+ justify-content: flex-end;
6428
+ gap: var(--aparte-space-2);
6240
6429
  }
6241
6430
  /*
6242
6431
  * aparté — <aparte-suggestions>: the row of prompt starters.
@@ -6309,6 +6498,682 @@ aparte-context[data-level="warn"] .aparte-progress__bar {
6309
6498
  aparte-context[data-level="danger"] .aparte-progress__bar {
6310
6499
  background: var(--aparte-error);
6311
6500
  }
6501
+
6502
+ /* The ring variant: a track and a value circle on a 36-unit box, sized by the two tokens.
6503
+ `pathLength="100"` on the value circle (set by the element) makes the dash the
6504
+ percentage itself, so the sheet needs no circumference. Rotated a quarter turn so the
6505
+ value starts at twelve o'clock. */
6506
+ .aparte-context--ring {
6507
+ gap: var(--aparte-space-1);
6508
+ }
6509
+
6510
+ .aparte-context__ring {
6511
+ flex: none;
6512
+ width: var(--aparte-context-ring-size);
6513
+ height: var(--aparte-context-ring-size);
6514
+ transform: rotate(-90deg);
6515
+ }
6516
+
6517
+ .aparte-context__track,
6518
+ .aparte-context__value {
6519
+ fill: none;
6520
+ stroke-width: var(--aparte-context-ring-stroke);
6521
+ }
6522
+
6523
+ .aparte-context__track {
6524
+ stroke: var(--aparte-surface-3);
6525
+ }
6526
+
6527
+ .aparte-context__value {
6528
+ --aparte-context-ratio: 0;
6529
+ stroke: var(--aparte-progress-fill);
6530
+ stroke-linecap: round;
6531
+ stroke-dasharray: var(--aparte-context-ratio) 100;
6532
+ transition: stroke-dasharray var(--aparte-duration-fast) var(--aparte-ease);
6533
+ }
6534
+
6535
+ .aparte-context__value--empty {
6536
+ visibility: hidden;
6537
+ }
6538
+
6539
+ aparte-context[data-level="warn"] .aparte-context__value {
6540
+ stroke: var(--aparte-warning);
6541
+ }
6542
+ aparte-context[data-level="danger"] .aparte-context__value {
6543
+ stroke: var(--aparte-error);
6544
+ }
6545
+ /*
6546
+ * aparté — <aparte-scroll-rail>: the ticks beside the transcript.
6547
+ *
6548
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6549
+ * them concatenated in that same order, the way a browser does.
6550
+ *
6551
+ * The rail floats on the transcript's end edge, vertically centred, as a child of the
6552
+ * chat host. `:has()` gives the host its containing block only while a rail is inside
6553
+ * it — a bare `position: relative` on every chat would have moved any absolutely
6554
+ * positioned child a consumer already placed there.
6555
+ */
6556
+ aparte-chat:has(> aparte-scroll-rail),
6557
+ [data-aparte-chat]:has(> aparte-scroll-rail) {
6558
+ position: relative;
6559
+ }
6560
+
6561
+ aparte-scroll-rail {
6562
+ position: absolute;
6563
+ inset-inline-end: var(--aparte-space-2);
6564
+ top: 50%;
6565
+ transform: translateY(-50%);
6566
+ z-index: var(--aparte-z-floating);
6567
+ display: block;
6568
+ width: var(--aparte-scroll-rail-width);
6569
+ max-height: calc(100% - 2 * var(--aparte-space-8));
6570
+ overflow: hidden;
6571
+ }
6572
+
6573
+ aparte-scroll-rail[hidden],
6574
+ aparte-scroll-rail[data-empty] {
6575
+ display: none;
6576
+ }
6577
+
6578
+ .aparte-scroll-rail__list {
6579
+ list-style: none;
6580
+ margin: 0;
6581
+ padding: 0;
6582
+ display: flex;
6583
+ flex-direction: column;
6584
+ align-items: flex-end;
6585
+ gap: var(--aparte-scroll-rail-gap);
6586
+ }
6587
+
6588
+ /* A tick is a real button, drawn as a line. The hit area is the gap around it, drawn
6589
+ by the pseudo-element so the line itself can stay two pixels. */
6590
+ .aparte-scroll-rail__tick {
6591
+ appearance: none;
6592
+ -webkit-appearance: none;
6593
+ position: relative;
6594
+ display: block;
6595
+ width: var(--aparte-scroll-rail-tick-size);
6596
+ height: var(--aparte-scroll-rail-tick-thickness);
6597
+ padding: 0;
6598
+ border: 0;
6599
+ border-radius: var(--aparte-radius-full);
6600
+ background: var(--aparte-border);
6601
+ cursor: pointer;
6602
+ transition:
6603
+ width var(--aparte-duration-fast) var(--aparte-ease),
6604
+ background var(--aparte-duration-fast) var(--aparte-ease);
6605
+ }
6606
+
6607
+ .aparte-scroll-rail__tick::before {
6608
+ content: "";
6609
+ position: absolute;
6610
+ inset: calc(-1 * var(--aparte-scroll-rail-gap) / 2) calc(-1 * var(--aparte-space-2));
6611
+ }
6612
+
6613
+ .aparte-scroll-rail__tick:hover {
6614
+ background: var(--aparte-text-muted);
6615
+ }
6616
+
6617
+ .aparte-scroll-rail__tick[aria-current="true"] {
6618
+ width: calc(var(--aparte-scroll-rail-tick-size) * 1.6);
6619
+ background: var(--aparte-primary);
6620
+ }
6621
+
6622
+ .aparte-scroll-rail__tick:focus-visible {
6623
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
6624
+ outline-offset: var(--aparte-focus-outline-offset);
6625
+ }
6626
+
6627
+ @media (prefers-reduced-motion: reduce) {
6628
+ .aparte-scroll-rail__tick {
6629
+ transition: none;
6630
+ }
6631
+ }
6632
+ /*
6633
+ * aparté — the shell: the chrome a whole application puts around the chat. A sidebar
6634
+ * of conversations, a header, and the grid that holds them beside the transcript —
6635
+ * what a ChatGPT-style page is made of, so a site can be built on aparté alone.
6636
+ *
6637
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6638
+ * them concatenated in that same order, the way a browser does.
6639
+ */
6640
+
6641
+ /*
6642
+ * aparte-sidebar — the column beside the chat.
6643
+ * A sized column that becomes a drawer under 48rem of window. The recipe draws the
6644
+ * column and its four regions; `<aparte-sidebar>` adds the behaviour (collapse, the
6645
+ * drawer, the search filter) and sets `data-drawer` on itself from the media query,
6646
+ * which is the one attribute the rules below read.
6647
+ *
6648
+ * <aside class="aparte-sidebar">
6649
+ * <div class="aparte-sidebar__header">
6650
+ * <span class="aparte-sidebar__brand">aparté</span>
6651
+ * <button class="aparte-btn aparte-btn--icon aparte-btn--sm" type="button" aria-label="New chat">
6652
+ * <aparte-icon name="edit"></aparte-icon>
6653
+ * </button>
6654
+ * </div>
6655
+ * <div class="aparte-sidebar__search aparte-field-group">
6656
+ * <input class="aparte-field aparte-field--sm" type="search" placeholder="Search" data-aparte-sidebar-search>
6657
+ * </div>
6658
+ * <div class="aparte-sidebar__body">
6659
+ * <aparte-conversation-list></aparte-conversation-list>
6660
+ * </div>
6661
+ * <div class="aparte-sidebar__footer">
6662
+ * <span class="aparte-avatar aparte-avatar--sm">P</span> Paul
6663
+ * </div>
6664
+ * </aside>
6665
+ *
6666
+ * The element wears the same class and adds the behaviour; `<aside class="aparte-sidebar">`
6667
+ * is the look alone, which is what this page shows.
6668
+ */
6669
+ aparte-sidebar,
6670
+ .aparte-sidebar {
6671
+ box-sizing: border-box;
6672
+ display: flex;
6673
+ flex-direction: column;
6674
+ width: var(--aparte-sidebar-width);
6675
+ min-width: 0;
6676
+ height: 100%;
6677
+ overflow: hidden;
6678
+ background: var(--aparte-sidebar-bg);
6679
+ border-inline-end: var(--aparte-border-width) solid var(--aparte-border);
6680
+ color: var(--aparte-text);
6681
+ transition: width var(--aparte-duration-slow) var(--aparte-ease);
6682
+ }
6683
+
6684
+ /* In the flow, collapsed means gone: the column folds to nothing and the chat takes
6685
+ the room. The children keep their width so the fold is a slide, not a squash. */
6686
+ .aparte-sidebar[collapsed]:not([data-drawer]) {
6687
+ width: 0;
6688
+ border-inline-end-width: 0;
6689
+ }
6690
+
6691
+ /* The children keep the column's width while it collapses, so nothing reflows during
6692
+ the transition. BORDER-box, or a padded child is the column plus its padding: the
6693
+ header's button sat flush against the edge and the search field ran off it, clipped
6694
+ — measured at 292px in a 260px column. The search field carries margins as well,
6695
+ which border-box does not cover; its own rule subtracts them. */
6696
+ .aparte-sidebar > * {
6697
+ box-sizing: border-box;
6698
+ flex-shrink: 0;
6699
+ min-width: var(--aparte-sidebar-width);
6700
+ }
6701
+
6702
+ .aparte-sidebar__header {
6703
+ display: flex;
6704
+ align-items: center;
6705
+ gap: var(--aparte-space-3);
6706
+ padding: var(--aparte-space-4) var(--aparte-space-4) var(--aparte-space-2);
6707
+ }
6708
+
6709
+ .aparte-sidebar__brand {
6710
+ flex: 1;
6711
+ min-width: 0;
6712
+ font-weight: var(--aparte-font-weight-semibold);
6713
+ white-space: nowrap;
6714
+ overflow: hidden;
6715
+ text-overflow: ellipsis;
6716
+ }
6717
+
6718
+ .aparte-sidebar__search {
6719
+ margin: var(--aparte-space-2) var(--aparte-space-3);
6720
+ min-width: calc(var(--aparte-sidebar-width) - 2 * var(--aparte-space-3));
6721
+ }
6722
+
6723
+ /* The body is the one region that scrolls; the list inside keeps its own gap. */
6724
+ .aparte-sidebar__body {
6725
+ flex: 1 1 auto;
6726
+ min-height: 0;
6727
+ overflow-y: auto;
6728
+ overflow-x: hidden;
6729
+ padding: var(--aparte-space-2) var(--aparte-space-3);
6730
+ scrollbar-width: thin;
6731
+ scrollbar-color: var(--aparte-scrollbar-thumb) var(--aparte-scrollbar-track);
6732
+ }
6733
+
6734
+ .aparte-sidebar__footer {
6735
+ display: flex;
6736
+ align-items: center;
6737
+ gap: var(--aparte-space-3);
6738
+ padding: var(--aparte-space-3) var(--aparte-space-4);
6739
+ border-block-start: var(--aparte-border-width) solid var(--aparte-border);
6740
+ font-size: var(--aparte-font-size-sm);
6741
+ }
6742
+
6743
+ /* The drawer: the same element, out of the flow, over the page. `inset-inline-start`
6744
+ rather than a transform, so the scrim — a fixed child — stays relative to the
6745
+ viewport and can cover the page from inside the element. */
6746
+ .aparte-sidebar[data-drawer] {
6747
+ position: fixed;
6748
+ inset-block: 0;
6749
+ inset-inline-start: 0;
6750
+ z-index: var(--aparte-z-dropdown);
6751
+ width: var(--aparte-sidebar-width);
6752
+ box-shadow: var(--aparte-menu-shadow);
6753
+ transition: inset-inline-start var(--aparte-duration-slower) var(--aparte-ease);
6754
+ }
6755
+
6756
+ .aparte-sidebar[data-drawer][collapsed] {
6757
+ inset-inline-start: calc(-1 * var(--aparte-sidebar-width) - var(--aparte-space-4));
6758
+ box-shadow: none;
6759
+ }
6760
+
6761
+ .aparte-sidebar__scrim {
6762
+ position: fixed;
6763
+ inset: 0;
6764
+ z-index: -1;
6765
+ background: var(--aparte-scrim);
6766
+ }
6767
+ /*
6768
+ * aparté — the header row of an application shell.
6769
+ *
6770
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6771
+ * them concatenated in that same order, the way a browser does.
6772
+ */
6773
+
6774
+ /*
6775
+ * aparte-app-header — the bar above the chat.
6776
+ * A recipe and nothing more, because a header has no behaviour of its own: a toggle
6777
+ * for the sidebar (which `<aparte-sidebar>` wires from the `data-aparte-sidebar-toggle`
6778
+ * attribute), the conversation's title, and an actions zone pushed to the end — a
6779
+ * model selector, a share button, whatever the page needs. The toggle shows only
6780
+ * under 48rem, where the sidebar is a drawer; above it the column is simply there.
6781
+ *
6782
+ * <header class="aparte-app-header">
6783
+ * <button class="aparte-btn aparte-btn--icon aparte-app-header__toggle" type="button" aria-label="Toggle the sidebar" data-aparte-sidebar-toggle>☰</button>
6784
+ * <span class="aparte-app-header__title">Deploy checklist</span>
6785
+ * <div class="aparte-app-header__actions">
6786
+ * <span class="aparte-tag">gpt-4.1</span>
6787
+ * <button class="aparte-btn aparte-btn--sm aparte-btn--ghost" type="button">Share</button>
6788
+ * </div>
6789
+ * </header>
6790
+ */
6791
+ .aparte-app-header {
6792
+ box-sizing: border-box;
6793
+ display: flex;
6794
+ align-items: center;
6795
+ gap: var(--aparte-space-3);
6796
+ min-height: var(--aparte-app-header-height);
6797
+ padding: var(--aparte-space-2) var(--aparte-space-4);
6798
+ background: var(--aparte-surface-1);
6799
+ border-block-end: var(--aparte-border-width) solid var(--aparte-border);
6800
+ color: var(--aparte-text);
6801
+ }
6802
+
6803
+ .aparte-app-header__title {
6804
+ flex: 1;
6805
+ min-width: 0;
6806
+ font-weight: var(--aparte-font-weight-medium);
6807
+ white-space: nowrap;
6808
+ overflow: hidden;
6809
+ text-overflow: ellipsis;
6810
+ }
6811
+
6812
+ .aparte-app-header__actions {
6813
+ display: flex;
6814
+ align-items: center;
6815
+ gap: var(--aparte-space-2);
6816
+ margin-inline-start: auto;
6817
+ }
6818
+
6819
+ .aparte-app-header__toggle {
6820
+ display: none;
6821
+ }
6822
+
6823
+ /* The same 48rem the sidebar turns into a drawer at (`sidebar.css`). */
6824
+ @media (max-width: 48rem) {
6825
+ .aparte-app-header__toggle {
6826
+ display: inline-flex;
6827
+ }
6828
+ }
6829
+ /*
6830
+ * aparté — the grid that holds a sidebar, a header and the chat.
6831
+ *
6832
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6833
+ * them concatenated in that same order, the way a browser does.
6834
+ */
6835
+
6836
+ /*
6837
+ * aparte-app-shell — sidebar beside, header above, the chat in the rest.
6838
+ * A recipe: two columns and two rows, the sidebar spanning both rows, the header and
6839
+ * the main area stacked in the second column. The chat fills `__main`, which is what
6840
+ * gives it the height it needs to scroll (see the layout guide). Size the shell —
6841
+ * `height: 100dvh` for a page — and everything inside follows. Under 48rem the sidebar
6842
+ * leaves the grid to become a drawer, and the header's toggle appears.
6843
+ *
6844
+ * <div class="aparte-app-shell" style="height: 18rem">
6845
+ * <aside class="aparte-sidebar">
6846
+ * <div class="aparte-sidebar__header"><span class="aparte-sidebar__brand">aparté</span></div>
6847
+ * <div class="aparte-sidebar__body"><aparte-conversation-list></aparte-conversation-list></div>
6848
+ * </aside>
6849
+ * <header class="aparte-app-header"><span class="aparte-app-header__title">Deploy checklist</span></header>
6850
+ * <main class="aparte-app-shell__main">
6851
+ * <aparte-chat>
6852
+ * <aparte-chat-viewport></aparte-chat-viewport>
6853
+ * <aparte-composer>
6854
+ * <div class="aparte-composer-shell">
6855
+ * <div class="aparte-composer-row">
6856
+ * <aparte-composer-input></aparte-composer-input>
6857
+ * <aparte-composer-send></aparte-composer-send>
6858
+ * </div>
6859
+ * </div>
6860
+ * </aparte-composer>
6861
+ * </aparte-chat>
6862
+ * </main>
6863
+ * </div>
6864
+ */
6865
+ .aparte-app-shell {
6866
+ box-sizing: border-box;
6867
+ display: grid;
6868
+ grid-template-columns: auto minmax(0, 1fr);
6869
+ grid-template-rows: auto minmax(0, 1fr);
6870
+ height: 100%;
6871
+ overflow: hidden;
6872
+ background: var(--aparte-surface);
6873
+ }
6874
+
6875
+ .aparte-app-shell > aparte-sidebar,
6876
+ .aparte-app-shell > .aparte-sidebar {
6877
+ grid-row: 1 / -1;
6878
+ }
6879
+
6880
+ /* A drawer is out of the flow, so the grid closes the column it left. */
6881
+ .aparte-app-shell > .aparte-sidebar[data-drawer] {
6882
+ grid-row: auto;
6883
+ }
6884
+
6885
+ .aparte-app-shell > .aparte-app-header {
6886
+ grid-column: 2;
6887
+ }
6888
+
6889
+ .aparte-app-shell__main {
6890
+ grid-column: 2;
6891
+ min-height: 0;
6892
+ min-width: 0;
6893
+ display: flex;
6894
+ flex-direction: column;
6895
+ }
6896
+
6897
+ /* A chat — or a split holding one — fills the main area. Stated for the direct child
6898
+ because that is where the height comes from: `<aparte-chat>` is `height: 100%` of a
6899
+ parent that has none until this rule gives it one, and a split between the two is
6900
+ the same case one level out. A pane CONTAINS a chat; a chat never contains a split. */
6901
+ .aparte-app-shell__main > aparte-chat,
6902
+ .aparte-app-shell__main > [data-aparte-chat],
6903
+ .aparte-app-shell__main > aparte-split,
6904
+ .aparte-app-shell__main > .aparte-split {
6905
+ flex: 1;
6906
+ min-height: 0;
6907
+ height: auto;
6908
+ }
6909
+ /*
6910
+ * aparté — the seam: two panes side by side, and the grid that lets one grow while
6911
+ * the other gives way. The fourth piece of the shell, after the sidebar, the header
6912
+ * and the grid that holds them.
6913
+ *
6914
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6915
+ * them concatenated in that same order, the way a browser does.
6916
+ */
6917
+
6918
+ /*
6919
+ * aparte-split — two panes and a draggable seam. The chat in one, your pane in the
6920
+ * other: a preview frame, an editor, an artifact card.
6921
+ *
6922
+ * The grid is the whole mechanism. `--aparte-split-position` is the primary pane's
6923
+ * size; `--aparte-split-min` and `--aparte-split-max` are clamp bounds, so the
6924
+ * browser does the clamping and nothing in JS parses a unit — px, %, rem and ch all
6925
+ * work. Used without <aparte-split> this is a static split: set the position from
6926
+ * your own media query and there is no drag, which is the Canvas shape.
6927
+ *
6928
+ * A pane CONTAINS a chat; a chat never contains a split.
6929
+ *
6930
+ * <div class="aparte-split" style="height: 18rem">
6931
+ * <div>the chat</div>
6932
+ * <div class="aparte-split__handle"></div>
6933
+ * <div class="aparte-split__pane">the pane</div>
6934
+ * </div>
6935
+ *
6936
+ * The element wears the same class and adds the behaviour (drag, arrow keys, the APG
6937
+ * separator's ARIA, one pane at a time under a breakpoint); `<div class="aparte-split">`
6938
+ * is the look alone, which is what this page shows.
6939
+ */
6940
+
6941
+ /* The outer clamp is a guard, not decoration: a `--aparte-split-min` wider than the
6942
+ container would otherwise eat the free pane whole and push the handle off the end.
6943
+ Three tracks, always — the primary pane, the seam, the rest. */
6944
+ aparte-split,
6945
+ .aparte-split {
6946
+ box-sizing: border-box;
6947
+ display: grid;
6948
+ grid-template-columns:
6949
+ clamp(
6950
+ 0px,
6951
+ clamp(var(--aparte-split-min), var(--aparte-split-position), var(--aparte-split-max)),
6952
+ calc(100% - var(--aparte-split-handle-size))
6953
+ )
6954
+ var(--aparte-split-handle-size)
6955
+ minmax(0, 1fr);
6956
+ height: 100%;
6957
+ min-width: 0;
6958
+ overflow: hidden;
6959
+ }
6960
+
6961
+ /* A grid item's automatic minimum size is its content, so one long line in either
6962
+ pane refuses to shrink and the seam stops tracking the pointer.
6963
+
6964
+ The tag is listed beside the class here and below for the reason the grid rule
6965
+ lists it: the element adds the class at connect, so between parse and upgrade a
6966
+ server-rendered split would be a grid whose items keep their content minimum. */
6967
+ aparte-split > *,
6968
+ .aparte-split > * {
6969
+ min-inline-size: 0;
6970
+ min-block-size: 0;
6971
+ }
6972
+
6973
+ /* The outside-the-shell half of the direct-child trap: <aparte-chat> is `height: 100%`
6974
+ of a parent that has none unless the grid track gives it one. Inside the app shell
6975
+ the same fix lives on `.aparte-app-shell__main`; split.css is imported after
6976
+ app-shell.css, so a chat in a pane inside a shell reads this one.
6977
+
6978
+ `box-sizing` with it, because the chat's own sheet gives it a bottom padding — the
6979
+ 16px of breathing room under the composer that core owns on purpose. Left in the
6980
+ default content-box that padding lands OUTSIDE the 100%, so the chat's border box
6981
+ overflows its track by exactly the gap and `.aparte-split`'s `overflow: hidden`
6982
+ eats it: the composer sits flush on the seam's bottom edge and the gap is gone. */
6983
+ aparte-split > aparte-chat,
6984
+ aparte-split > [data-aparte-chat],
6985
+ .aparte-split > aparte-chat,
6986
+ .aparte-split > [data-aparte-chat] {
6987
+ box-sizing: border-box;
6988
+ height: 100%;
6989
+ min-height: 0;
6990
+ }
6991
+
6992
+ /* The seam. `touch-action: none` so a drag on a touch screen resizes rather than
6993
+ scrolling the page — without it the browser claims the gesture at the first move
6994
+ and the pointer events stop arriving. */
6995
+ .aparte-split__handle {
6996
+ position: relative;
6997
+ box-sizing: border-box;
6998
+ border: 0;
6999
+ padding: 0;
7000
+ background: var(--aparte-border);
7001
+ cursor: col-resize;
7002
+ touch-action: none;
7003
+ transition: background-color var(--aparte-duration-fast) var(--aparte-ease);
7004
+ }
7005
+
7006
+ .aparte-split__handle:hover,
7007
+ .aparte-split__handle[data-dragging] {
7008
+ background: var(--aparte-primary);
7009
+ }
7010
+
7011
+ /* A real outline, and the ring kept as decoration.
7012
+
7013
+ The seam's whole keyboard story is arrowing it, and its only focus indicator was
7014
+ `--aparte-focus-ring` — a soft wash that measures 1.39:1 against the page in the
7015
+ light palette and 1.83:1 in the dark one, where WCAG asks 3:1 of a focus indicator.
7016
+ `outline: none` with nothing in its place is the shape of that failure: the
7017
+ indicator was not weak, it was absent, on a 4px band with `border: 0` that a
7018
+ keyboard user has no other way to find.
7019
+
7020
+ The outline is what carries the contrast (3.54:1 light, 7.36:1 dark, measured); the
7021
+ shadow stays because it reads as a glow AROUND the seam and the two do not fight.
7022
+ The forced-colors entry in responsive.css keeps overriding the colour only — it is
7023
+ now overriding an outline that exists rather than substituting for one that does
7024
+ not. */
7025
+ .aparte-split__handle:focus-visible {
7026
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
7027
+ outline-offset: 0;
7028
+ box-shadow: var(--aparte-focus-ring);
7029
+ }
7030
+
7031
+ /* The hit slop: a 4px seam is a 4px target, and a pointer misses it. The pseudo-element
7032
+ is wider than the seam and centred on it, so the grab zone reaches into both panes
7033
+ without the seam itself getting bigger. */
7034
+ .aparte-split__handle::after {
7035
+ content: '';
7036
+ position: absolute;
7037
+ inset-block: 0;
7038
+ inset-inline-start: calc(var(--aparte-split-hit-area) / -2 + var(--aparte-split-handle-size) / 2);
7039
+ inline-size: var(--aparte-split-hit-area);
7040
+ }
7041
+
7042
+ /* The drag overlay, a child of the HANDLE for the duration of one gesture. Being the
7043
+ topmost hit target it swallows an iframe pane's hit-testing (a drag over a frame is
7044
+ otherwise lost to the frame's document), stops a text selection from starting, and
7045
+ paints the drag cursor across the whole page. A child of the handle rather than of
7046
+ the body so `click` — and therefore `dblclick` — still dispatches at the handle,
7047
+ their nearest common ancestor.
7048
+
7049
+ It is `position: fixed`, so nothing between it and the viewport may establish a
7050
+ containing block: `.aparte-split` must never get `container-type`, `transform`,
7051
+ `filter` or `backdrop-filter`, or the overlay covers the split alone. */
7052
+ .aparte-split__scrim {
7053
+ position: fixed;
7054
+ inset: 0;
7055
+ z-index: var(--aparte-z-dropdown);
7056
+ cursor: col-resize;
7057
+ }
7058
+
7059
+ /* An optional wrapper for the pane that is not a chat — the pane equivalent of
7060
+ `.aparte-sidebar__body`. The chat pane does not use it: the viewport scrolls itself. */
7061
+ .aparte-split__pane {
7062
+ box-sizing: border-box;
7063
+ min-inline-size: 0;
7064
+ min-block-size: 0;
7065
+ overflow: auto;
7066
+ background: var(--aparte-surface-1);
7067
+ }
7068
+
7069
+ /* `primary="end"` sizes the LAST pane instead of the first: the same three tracks,
7070
+ read the other way round. */
7071
+ aparte-split[primary='end'],
7072
+ .aparte-split--primary-end {
7073
+ grid-template-columns:
7074
+ minmax(0, 1fr)
7075
+ var(--aparte-split-handle-size)
7076
+ clamp(
7077
+ 0px,
7078
+ clamp(var(--aparte-split-min), var(--aparte-split-position), var(--aparte-split-max)),
7079
+ calc(100% - var(--aparte-split-handle-size))
7080
+ );
7081
+ }
7082
+
7083
+ /* The other axis. The attribute and the class are selected in the same rule so the
7084
+ static recipe and the live element can never draw two different things. */
7085
+ aparte-split[orientation='vertical'],
7086
+ .aparte-split--vertical {
7087
+ grid-template-columns: none;
7088
+ grid-template-rows:
7089
+ clamp(
7090
+ 0px,
7091
+ clamp(var(--aparte-split-min), var(--aparte-split-position), var(--aparte-split-max)),
7092
+ calc(100% - var(--aparte-split-handle-size))
7093
+ )
7094
+ var(--aparte-split-handle-size)
7095
+ minmax(0, 1fr);
7096
+ }
7097
+
7098
+ /* The one rule in this sheet that out-specifies the stacked block below — two
7099
+ attributes against its one — so it has to opt out of the stacked state by hand or a
7100
+ vertical `primary="end"` split keeps three rows while showing one pane, and the two
7101
+ empty tracks take the height off the pane that is left. */
7102
+ aparte-split[orientation='vertical'][primary='end']:not([data-stacked]),
7103
+ .aparte-split--vertical.aparte-split--primary-end:not(.aparte-split--only-start):not(.aparte-split--only-end) {
7104
+ grid-template-rows:
7105
+ minmax(0, 1fr)
7106
+ var(--aparte-split-handle-size)
7107
+ clamp(
7108
+ 0px,
7109
+ clamp(var(--aparte-split-min), var(--aparte-split-position), var(--aparte-split-max)),
7110
+ calc(100% - var(--aparte-split-handle-size))
7111
+ );
7112
+ }
7113
+
7114
+ aparte-split[orientation='vertical'] > .aparte-split__handle,
7115
+ .aparte-split--vertical > .aparte-split__handle {
7116
+ cursor: row-resize;
7117
+ }
7118
+
7119
+ aparte-split[orientation='vertical'] > .aparte-split__handle::after,
7120
+ .aparte-split--vertical > .aparte-split__handle::after {
7121
+ inset-block: calc(var(--aparte-split-hit-area) / -2 + var(--aparte-split-handle-size) / 2) auto;
7122
+ inset-inline: 0;
7123
+ inline-size: auto;
7124
+ block-size: var(--aparte-split-hit-area);
7125
+ }
7126
+
7127
+ aparte-split[orientation='vertical'] .aparte-split__scrim,
7128
+ .aparte-split--vertical .aparte-split__scrim {
7129
+ cursor: row-resize;
7130
+ }
7131
+
7132
+ /* One pane at a time. `data-stacked` is written by the element from its breakpoint;
7133
+ the two modifier classes are the byte-identical CSS route for a host that owns its
7134
+ own breakpoint. The shown pane spans all three tracks and the seam goes with the
7135
+ pane it no longer separates — including its tab stop, which the element removes.
7136
+
7137
+ Not "position: 100% and a zero-size handle": that leaves a focusable separator of
7138
+ no width sitting in the tab order. */
7139
+ aparte-split[data-stacked],
7140
+ .aparte-split--only-start,
7141
+ .aparte-split--only-end {
7142
+ grid-template-columns: minmax(0, 1fr);
7143
+ grid-template-rows: minmax(0, 1fr);
7144
+ }
7145
+
7146
+ aparte-split[data-stacked] > .aparte-split__handle,
7147
+ .aparte-split--only-start > .aparte-split__handle,
7148
+ .aparte-split--only-end > .aparte-split__handle {
7149
+ display: none;
7150
+ }
7151
+
7152
+ aparte-split[data-stacked]:not([pane='end']) > :nth-child(3),
7153
+ .aparte-split--only-start > :nth-child(3) {
7154
+ display: none;
7155
+ }
7156
+
7157
+ aparte-split[data-stacked][pane='end'] > :nth-child(1),
7158
+ .aparte-split--only-end > :nth-child(1) {
7159
+ display: none;
7160
+ }
7161
+
7162
+ /* `disabled` is inert, so it must not look grabbable: the element takes the tab stop
7163
+ away and swallows the pointer, and a resize cursor plus a hover highlight on a seam
7164
+ that answers nothing is exactly the half-rendered affordance the house rule forbids.
7165
+ The seam itself stays drawn — it still separates two panes.
7166
+
7167
+ Last in the sheet because it must out-rank the vertical `cursor: row-resize` above,
7168
+ which it ties with on specificity. Attribute only: the static recipe has no
7169
+ behaviour, so there is no live-versus-static pair to keep in step. */
7170
+ aparte-split[disabled] > .aparte-split__handle {
7171
+ cursor: default;
7172
+ }
7173
+
7174
+ aparte-split[disabled] > .aparte-split__handle:hover {
7175
+ background: var(--aparte-border);
7176
+ }
6312
7177
  /*
6313
7178
  * aparté — Reset-agnostic typography for model-authored markdown.
6314
7179
  *
@@ -6476,6 +7341,26 @@ aparte-context[data-level="danger"] .aparte-progress__bar {
6476
7341
  overflow-y: auto;
6477
7342
  }
6478
7343
 
7344
+ /* A fenced block that came through MARKDOWN, not through the `code` segment.
7345
+ `@aparte/plugin-marked` renders ``` as a bare <pre><code>, and the recipe below is
7346
+ scoped to `.aparte-code-content-wrapper` — a class only the code SEGMENT renderer
7347
+ emits, which marked cannot produce. So a markdown block matched no `pre` rule at all
7348
+ and kept the UA's `white-space: pre`: it never wrapped, laid out at its own intrinsic
7349
+ width, and `aparte-chat-bubble`'s `overflow: hidden` amputated the tail. No scrollbar,
7350
+ no ellipsis — the code was simply gone. Measured on the vanilla example: one block's
7351
+ `scrollWidth` was a CONSTANT 963px at chat widths 1500, 800, 600, 512 and 380, against
7352
+ client widths 776 / 724 / 524 / 460 / 328. It overflowed even at 1500.
7353
+
7354
+ Only the two wrapping declarations are shared, not the surface: the padding and the
7355
+ background below are the code CARD's, and a markdown block has no card. Giving it one
7356
+ is a look decision, not this fix. */
7357
+ .aparte-content pre,
7358
+ .aparte-segment-content pre,
7359
+ .aparte-thinking-content pre {
7360
+ white-space: pre-wrap;
7361
+ overflow-wrap: anywhere;
7362
+ }
7363
+
6479
7364
  .aparte-code-content-wrapper pre {
6480
7365
  margin: 0;
6481
7366
  padding: var(--aparte-space-6);
@@ -6489,22 +7374,6 @@ aparte-context[data-level="danger"] .aparte-progress__bar {
6489
7374
  overflow-x: hidden;
6490
7375
  background: var(--aparte-code-bg);
6491
7376
  }
6492
- .aparte-art-card__pane[data-pane="code"] pre {
6493
- margin: 0; padding: var(--aparte-space-6);
6494
- font-size: var(--aparte-font-size-md);
6495
- background: var(--aparte-code-bg);
6496
- }
6497
- .aparte-art-file__code-pane pre {
6498
- margin: 0; padding: var(--aparte-space-6);
6499
- font-size: var(--aparte-font-size-md);
6500
- font-family: var(--aparte-code-font-family);
6501
- }
6502
- .aparte-art-file__preview-pane table {
6503
- border-collapse: collapse;
6504
- width: 100%;
6505
- font-size: var(--aparte-font-size-md);
6506
- font-family: inherit;
6507
- }
6508
7377
  /*
6509
7378
  * aparté — Container-width and pointer-modality adjustments. LAST on purpose.
6510
7379
  *
@@ -6531,14 +7400,17 @@ aparte-context[data-level="danger"] .aparte-progress__bar {
6531
7400
  }
6532
7401
 
6533
7402
  /* Windows high contrast. In forced-colors mode the UA repaints colours and
6534
- drops box-shadow entirely — so the two focus indicators drawn with
7403
+ drops box-shadow entirely — so the focus indicators drawn with
6535
7404
  --aparte-focus-ring, and the error ring on an avatar, do not merely change
6536
7405
  colour, they VANISH. An outline is painted, and `Highlight` is the system's
6537
7406
  own focus colour, so the indicator survives without fighting the user's
6538
7407
  palette. Only the indicators that disappear are restated here: everything
6539
- whose affordance is text or a border already survives on its own. */
7408
+ whose affordance is text or a border already survives on its own — which the
7409
+ split's seam does not, being a 4px band with `border: 0` whose entire keyboard
7410
+ story is arrowing it. */
6540
7411
  @media (forced-colors: active) {
6541
7412
  .aparte-thumb--image[role='button']:focus-visible,
7413
+ .aparte-split__handle:focus-visible,
6542
7414
  .aparte-provider-select:focus,
6543
7415
  .aparte-model-select:focus {
6544
7416
  outline: var(--aparte-focus-outline-width) solid Highlight;
@@ -6626,11 +7498,26 @@ aparte-context[data-level="danger"] .aparte-progress__bar {
6626
7498
  opacity: 1;
6627
7499
  }
6628
7500
 
6629
- /* Hover cannot reveal these on touch keep them visible and tappable. */
6630
- .aparte-conv-item__delete,
6631
- .aparte-conv-item__archive {
7501
+ /* Nor the on a pending attachment, which is the only way to remove one. */
7502
+ .aparte-thumb__remove {
7503
+ opacity: 1;
7504
+ }
7505
+
7506
+ /* Hover cannot reveal the row's ⋯ on touch — keep it visible and tappable. */
7507
+ .aparte-conv-item__more {
6632
7508
  opacity: 1;
6633
7509
  --aparte-conv-action-btn-size: 28px;
6634
7510
  }
7511
+
7512
+ /* A 4px seam is no target for a finger — the same modality rule the buttons above
7513
+ follow. The seam keeps its width; only the invisible grab zone grows. */
7514
+ .aparte-split__handle {
7515
+ --aparte-split-hit-area: var(--aparte-touch-target-size);
7516
+ }
7517
+
7518
+ /* A 2px tick is no target for a finger; the minimaps of the market hide on touch too. */
7519
+ aparte-scroll-rail {
7520
+ display: none;
7521
+ }
6635
7522
  }
6636
7523