@aparte/core 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) 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/composer/aparte-composer-attachments.d.ts +0 -1
  10. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  11. package/dist/components/composer/aparte-composer-send.d.ts +11 -11
  12. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  13. package/dist/components/composer/aparte-composer.d.ts +20 -11
  14. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  15. package/dist/components/context/aparte-context.d.ts +31 -8
  16. package/dist/components/context/aparte-context.d.ts.map +1 -1
  17. package/dist/components/conversation-list/aparte-conversation-list.d.ts +93 -36
  18. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  19. package/dist/components/conversation-list/index.d.ts +1 -1
  20. package/dist/components/conversation-list/index.d.ts.map +1 -1
  21. package/dist/components/elicitation/aparte-elicitation.d.ts +19 -1
  22. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  23. package/dist/components/index.d.ts +7 -1
  24. package/dist/components/index.d.ts.map +1 -1
  25. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts +105 -0
  26. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -0
  27. package/dist/components/scroll-rail/index.d.ts +3 -0
  28. package/dist/components/scroll-rail/index.d.ts.map +1 -0
  29. package/dist/components/sidebar/aparte-sidebar.d.ts +186 -0
  30. package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -0
  31. package/dist/components/sidebar/index.d.ts +3 -0
  32. package/dist/components/sidebar/index.d.ts.map +1 -0
  33. package/dist/components/split/aparte-split.d.ts +354 -0
  34. package/dist/components/split/aparte-split.d.ts.map +1 -0
  35. package/dist/components/split/geometry.d.ts +65 -0
  36. package/dist/components/split/geometry.d.ts.map +1 -0
  37. package/dist/components/split/index.d.ts +3 -0
  38. package/dist/components/split/index.d.ts.map +1 -0
  39. package/dist/components/suggestions/aparte-suggestions.d.ts +7 -1
  40. package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
  41. package/dist/components/viewport/aparte-chat-viewport.d.ts +52 -2
  42. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  43. package/dist/config/aparte-config.d.ts +95 -33
  44. package/dist/config/aparte-config.d.ts.map +1 -1
  45. package/dist/config/icon-provider.d.ts +6 -0
  46. package/dist/config/icon-provider.d.ts.map +1 -1
  47. package/dist/config/index.d.ts +2 -2
  48. package/dist/config/index.d.ts.map +1 -1
  49. package/dist/config/locale.d.ts +140 -27
  50. package/dist/config/locale.d.ts.map +1 -1
  51. package/dist/config/sanitize.d.ts +9 -0
  52. package/dist/config/sanitize.d.ts.map +1 -1
  53. package/dist/conversations/conversation-controller.d.ts +13 -1
  54. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  55. package/dist/conversations/conversation-manager.d.ts +9 -0
  56. package/dist/conversations/conversation-manager.d.ts.map +1 -1
  57. package/dist/custom-elements.json +9838 -7333
  58. package/dist/{is-awaiting-reply-BZ_iJ3s3.js → dialog-triggers-CVCxqKg3.js} +718 -1182
  59. package/dist/dialog-triggers-CVCxqKg3.js.map +1 -0
  60. package/dist/elicitation/approval-panel.d.ts +1 -1
  61. package/dist/elicitation/approval-panel.d.ts.map +1 -1
  62. package/dist/elicitation/panel.d.ts +24 -16
  63. package/dist/elicitation/panel.d.ts.map +1 -1
  64. package/dist/elicitation/types.d.ts +31 -0
  65. package/dist/elicitation/types.d.ts.map +1 -1
  66. package/dist/generated/element-attributes.d.ts +47 -1
  67. package/dist/generated/element-attributes.d.ts.map +1 -1
  68. package/dist/{glyphs-B3BZ2TIb.js → glyphs-DQTeOj4w.js} +22 -13
  69. package/dist/{glyphs-B3BZ2TIb.js.map → glyphs-DQTeOj4w.js.map} +1 -1
  70. package/dist/host/aparte-chat-host.d.ts +10 -2
  71. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  72. package/dist/icons/extended.d.ts +4 -2
  73. package/dist/icons/extended.d.ts.map +1 -1
  74. package/dist/icons/glyphs.d.ts +9 -0
  75. package/dist/icons/glyphs.d.ts.map +1 -1
  76. package/dist/icons.js +16 -15
  77. package/dist/icons.js.map +1 -1
  78. package/dist/index.css +1217 -396
  79. package/dist/index.d.ts +26 -15
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +2097 -295
  82. package/dist/index.js.map +1 -1
  83. package/dist/index.node.d.ts +21 -5
  84. package/dist/index.node.d.ts.map +1 -1
  85. package/dist/index.node.js +39 -34
  86. package/dist/index.node.js.map +1 -1
  87. package/dist/interop/dialog-triggers.d.ts +27 -0
  88. package/dist/interop/dialog-triggers.d.ts.map +1 -0
  89. package/dist/interop/element-props.d.ts +16 -8
  90. package/dist/interop/element-props.d.ts.map +1 -1
  91. package/dist/parsers/aparte-stream-parser.d.ts +52 -22
  92. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  93. package/dist/parsers/index.d.ts +1 -1
  94. package/dist/parsers/index.d.ts.map +1 -1
  95. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  96. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  97. package/dist/renderers/segments/code.d.ts.map +1 -1
  98. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  99. package/dist/runtime/message-repository.d.ts.map +1 -1
  100. package/dist/transport/backend-handler.d.ts.map +1 -1
  101. package/dist/transport/backend-transport.d.ts +6 -1
  102. package/dist/transport/backend-transport.d.ts.map +1 -1
  103. package/dist/types/chat.d.ts +8 -14
  104. package/dist/types/chat.d.ts.map +1 -1
  105. package/dist/types/element-map.d.ts +54 -4
  106. package/dist/types/element-map.d.ts.map +1 -1
  107. package/dist/types/event-map.d.ts +16 -10
  108. package/dist/types/event-map.d.ts.map +1 -1
  109. package/dist/types/events.d.ts +59 -117
  110. package/dist/types/events.d.ts.map +1 -1
  111. package/dist/types/imperative-api.d.ts +3 -1
  112. package/dist/types/imperative-api.d.ts.map +1 -1
  113. package/dist/types/index.d.ts +5 -4
  114. package/dist/types/index.d.ts.map +1 -1
  115. package/dist/types/models.d.ts +11 -31
  116. package/dist/types/models.d.ts.map +1 -1
  117. package/dist/types/segments.d.ts +11 -47
  118. package/dist/types/segments.d.ts.map +1 -1
  119. package/dist/types/stream-blocks.d.ts +57 -0
  120. package/dist/types/stream-blocks.d.ts.map +1 -0
  121. package/dist/types/tools.d.ts +57 -0
  122. package/dist/types/tools.d.ts.map +1 -1
  123. package/dist/utils/copy-text.d.ts +24 -0
  124. package/dist/utils/copy-text.d.ts.map +1 -0
  125. package/dist/utils/css-escape.d.ts +5 -0
  126. package/dist/utils/css-escape.d.ts.map +1 -1
  127. package/dist/utils/tool-input.d.ts +20 -0
  128. package/dist/utils/tool-input.d.ts.map +1 -0
  129. package/package.json +10 -3
  130. package/dist/is-awaiting-reply-BZ_iJ3s3.js.map +0 -1
  131. package/dist/renderers/segments/artifact/binary-file.d.ts +0 -45
  132. package/dist/renderers/segments/artifact/binary-file.d.ts.map +0 -1
  133. package/dist/renderers/segments/artifact/card.d.ts +0 -3
  134. package/dist/renderers/segments/artifact/card.d.ts.map +0 -1
  135. package/dist/renderers/segments/artifact/preview-document.d.ts +0 -27
  136. package/dist/renderers/segments/artifact/preview-document.d.ts.map +0 -1
  137. package/dist/renderers/segments/artifact/shared.d.ts +0 -29
  138. 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 {
@@ -4196,6 +4464,9 @@ aparte-chat-viewport[framework-managed] {
4196
4464
  padding-bottom: calc(var(--aparte-viewport-padding) + var(--aparte-fw-spacer, 0px));
4197
4465
  scrollbar-width: thin;
4198
4466
  scrollbar-color: var(--aparte-scrollbar-thumb) var(--aparte-scrollbar-track);
4467
+ /* Same gutter rule as `.aparte-viewport-container` above: the host is the scroll
4468
+ surface here, so it is the host that reserves it. */
4469
+ scrollbar-gutter: stable both-edges;
4199
4470
  }
4200
4471
 
4201
4472
  /* The framework wrapper renders bubbles as DIRECT flex children of the viewport
@@ -4631,6 +4902,31 @@ aparte-chat-bubble:last-of-type .aparte-message[data-role="assistant"]:not(.apar
4631
4902
  .aparte-message[data-empty] {
4632
4903
  display: none;
4633
4904
  }
4905
+
4906
+ /* ─── The compaction notice ────────────────────────────────────────────────────
4907
+ A message with `compaction: true` — the summary `compact()` injected in place of
4908
+ the turns it replaced — is a notice, not a reply: centred, muted, without avatar,
4909
+ name, time or actions. Its role on the wire is `user` (context handed to the
4910
+ model), and the bubble would otherwise draw it as something the user said. */
4911
+ aparte-chat-bubble[data-kind="compaction"] .aparte-avatar,
4912
+ aparte-chat-bubble[data-kind="compaction"] .aparte-header,
4913
+ aparte-chat-bubble[data-kind="compaction"] .aparte-footer,
4914
+ aparte-chat-bubble[data-kind="compaction"] .aparte-action-bar,
4915
+ aparte-chat-bubble[data-kind="compaction"] .aparte-branch-picker {
4916
+ display: none;
4917
+ }
4918
+
4919
+ aparte-chat-bubble[data-kind="compaction"] .aparte-message {
4920
+ justify-content: center;
4921
+ }
4922
+
4923
+ aparte-chat-bubble[data-kind="compaction"] .aparte-message-content {
4924
+ max-width: 100%;
4925
+ background: var(--aparte-surface-2);
4926
+ color: var(--aparte-text-muted);
4927
+ border: var(--aparte-border-width) dashed var(--aparte-border);
4928
+ font-size: var(--aparte-font-size-sm);
4929
+ }
4634
4930
  /*
4635
4931
  * aparté — The composer and everything it holds: editor, send, attachments, model picker.
4636
4932
  *
@@ -4715,7 +5011,14 @@ aparte-chat-viewport.aparte-viewport--framework::-webkit-scrollbar-thumb {
4715
5011
  border-radius: 50%;
4716
5012
  opacity: 0;
4717
5013
  }
4718
- .aparte-thumb:hover .aparte-thumb__remove {
5014
+ /* Hover is not the only way in. The ✕ is the ONLY way to drop a pending attachment,
5015
+ and at `opacity: 0` it was invisible to a keyboard user standing on it — a focus
5016
+ ring drawn around nothing. `:focus-within` on the tile is the same pair the message
5017
+ action bar (bubble.css) and the conversation row (conversation.css) already use;
5018
+ this one had been written with the hover half alone. The touch half is in
5019
+ responsive.css, beside the row's ⋯, for the same reason: a finger cannot hover. */
5020
+ .aparte-thumb:hover .aparte-thumb__remove,
5021
+ .aparte-thumb:focus-within .aparte-thumb__remove {
4719
5022
  opacity: 1;
4720
5023
  }
4721
5024
  .aparte-thumb__remove:hover {
@@ -5525,256 +5828,6 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
5525
5828
  border-collapse: collapse;
5526
5829
  }
5527
5830
 
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
5831
  /*
5779
5832
  * aparté — The elicitation panel and the approval gate.
5780
5833
  *
@@ -6023,6 +6076,43 @@ textarea.aparte-elic-text {
6023
6076
  recipe. See `buildApprovalPanel` for why colour lost: an alert's vocabulary on a
6024
6077
  control, and 2.19:1 on the dark palette when it was tried as a fill instead. */
6025
6078
 
6079
+ /* What is being decided, between the question and the options.
6080
+
6081
+ The panel used to carry the tool's NAME and nothing else, so the arguments — the
6082
+ whole of what a person approves — were only in the transcript row, behind a closed
6083
+ disclosure. Capped and scrollable rather than unbounded: the panel itself is capped
6084
+ at half the viewport, and a plan or a diff arriving as a tool argument would
6085
+ otherwise push the Approve button off the bottom of the surface it is asked on.
6086
+
6087
+ Monospace, because it is JSON: the model's own bytes, not prose about them. */
6088
+ .aparte-approval-args-label {
6089
+ margin: var(--aparte-space-3) var(--aparte-space-3) 0;
6090
+ font-size: var(--aparte-font-size-xs);
6091
+ text-transform: uppercase;
6092
+ letter-spacing: 0.04em;
6093
+ color: var(--aparte-text-muted);
6094
+ }
6095
+
6096
+ .aparte-approval-args {
6097
+ margin: var(--aparte-space-1) var(--aparte-space-3) 0;
6098
+ padding: var(--aparte-space-3);
6099
+ max-height: var(--aparte-approval-args-max-height, 8.5rem);
6100
+ overflow: auto;
6101
+ font-family: var(--aparte-code-font-family);
6102
+ font-size: var(--aparte-approval-args-size, var(--aparte-font-size-sm));
6103
+ line-height: var(--aparte-line-height-snug);
6104
+ color: var(--aparte-text);
6105
+ background: var(--aparte-surface-2);
6106
+ border-radius: var(--aparte-radius-sm);
6107
+ white-space: pre-wrap;
6108
+ overflow-wrap: anywhere;
6109
+ }
6110
+
6111
+ .aparte-approval-args:focus-visible {
6112
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
6113
+ outline-offset: 1px;
6114
+ }
6115
+
6026
6116
  /* The refusal's reason, in the user's words. Quiet by default and one line tall: it is
6027
6117
  the exception, not the expected act, and a box sized for a paragraph invites one. */
6028
6118
  .aparte-approval-instruction {
@@ -6088,11 +6178,27 @@ textarea.aparte-elic-text {
6088
6178
  margin-bottom: calc(-1 * var(--aparte-border-width));
6089
6179
  }
6090
6180
 
6091
- /* Answered: readable, but it is not where you are. */
6181
+ /* Answered: readable, but it is not where you are — and marked, because a colour is
6182
+ what the current chip has too. The mark is the `check` glyph, sized by the chip. */
6092
6183
  .aparte-elic-step[data-answered] {
6093
6184
  color: var(--aparte-text);
6094
6185
  }
6095
6186
 
6187
+ .aparte-elic-step__mark {
6188
+ display: inline-flex;
6189
+ align-items: center;
6190
+ margin-inline-end: var(--aparte-space-1);
6191
+ color: var(--aparte-success);
6192
+ --aparte-icon-size: 0.85em;
6193
+ }
6194
+
6195
+ /* The tag beside a recommended option's label: the recipe draws it, this only keeps it
6196
+ from stretching the title line. */
6197
+ .aparte-elic-option-badge {
6198
+ vertical-align: middle;
6199
+ margin-inline-start: var(--aparte-space-1);
6200
+ }
6201
+
6096
6202
  .aparte-elic-step[aria-selected='true'] {
6097
6203
  color: var(--aparte-text);
6098
6204
  font-weight: var(--aparte-font-weight-semibold);
@@ -6110,16 +6216,37 @@ textarea.aparte-elic-text {
6110
6216
  * them in the order listed there, and check:derived-vars reads them concatenated in
6111
6217
  * that same order, the way a browser does.
6112
6218
  */
6219
+
6220
+ /* A date group: its heading wears the menu's label recipe (`.aparte-menu__label`),
6221
+ because that is what a heading over a list of rows is. Only the rhythm is its own. */
6222
+ .aparte-conv-group {
6223
+ display: flex;
6224
+ flex-direction: column;
6225
+ gap: var(--aparte-conv-list-gap);
6226
+ }
6227
+
6228
+ .aparte-conv-group + .aparte-conv-group {
6229
+ margin-block-start: var(--aparte-space-3);
6230
+ }
6231
+
6113
6232
  /* A row in a list, which is what `.aparte-menu__item` is. What stays is what belongs
6114
6233
  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. */
6234
+ measurements, because a sidebar row is tighter than a dropdown row.
6235
+
6236
+ The row is a flex box around two real buttons — the title, which selects, and the
6237
+ `⋯`. The padding sits on the title button, not the row, so that the button's focus
6238
+ ring is the row's shape and a click on the row's inner margin lands on the button
6239
+ rather than on nothing. */
6116
6240
  .aparte-conv-item {
6241
+ position: relative;
6117
6242
  min-width: 0;
6118
6243
  gap: var(--aparte-conv-item-gap);
6119
- padding: var(--aparte-conv-item-padding);
6244
+ padding: 0;
6245
+ padding-inline-end: var(--aparte-space-2);
6120
6246
  border-radius: var(--aparte-conv-item-radius);
6121
6247
  color: var(--aparte-conv-item-color);
6122
6248
  font-size: var(--aparte-conv-item-font-size);
6249
+ cursor: default;
6123
6250
  }
6124
6251
 
6125
6252
  /* Hover moves the ground UP, so the label has to move with it.
@@ -6134,31 +6261,36 @@ textarea.aparte-elic-text {
6134
6261
  color: var(--aparte-conv-item-color-active);
6135
6262
  }
6136
6263
 
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;
6264
+ .aparte-conv-item__select {
6265
+ appearance: none;
6266
+ -webkit-appearance: none;
6267
+ display: flex;
6268
+ align-items: center;
6269
+ flex: 1;
6270
+ min-width: 0;
6271
+ padding: var(--aparte-conv-item-padding);
6272
+ border: none;
6273
+ border-radius: inherit;
6274
+ background: transparent;
6275
+ color: inherit;
6276
+ font: inherit;
6277
+ text-align: start;
6278
+ cursor: pointer;
6144
6279
  }
6145
6280
 
6146
- .aparte-conv-item:hover .aparte-conv-item__archive {
6147
- opacity: 1;
6281
+ /* Inside the edge: the list clips horizontally (`overflow-x: hidden` in base.css), and
6282
+ a ring drawn outside the row was cut on both sides. */
6283
+ .aparte-conv-item__select:focus-visible {
6284
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
6285
+ outline-offset: calc(-1 * var(--aparte-focus-outline-width));
6148
6286
  }
6149
6287
 
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);
6288
+ .aparte-conv-item__title {
6289
+ flex: 1;
6290
+ white-space: nowrap;
6291
+ overflow: hidden;
6292
+ text-overflow: ellipsis;
6293
+ min-width: 0;
6162
6294
  }
6163
6295
 
6164
6296
  /* The conversation you are in wears the mark's bar (display/mark.css) on its start
@@ -6166,7 +6298,6 @@ textarea.aparte-elic-text {
6166
6298
  a checked field choice carry. Its ground keeps the list's own tokens: a navigation
6167
6299
  list is read for a long time, and the mark's tint on every visit was too loud. */
6168
6300
  .aparte-conv-item--active {
6169
- position: relative;
6170
6301
  background: var(--aparte-conv-item-bg-active);
6171
6302
  color: var(--aparte-conv-item-color-active);
6172
6303
  font-weight: var(--aparte-conv-item-font-weight-active);
@@ -6182,61 +6313,73 @@ textarea.aparte-elic-text {
6182
6313
  pointer-events: none;
6183
6314
  }
6184
6315
 
6185
- .aparte-conv-item--active .aparte-conv-item__delete {
6186
- opacity: 0;
6316
+ .aparte-conv-item--archived .aparte-conv-item__title {
6317
+ font-style: italic;
6318
+ opacity: 0.85;
6187
6319
  }
6188
6320
 
6189
- .aparte-conv-item--active .aparte-conv-item__archive {
6321
+ /* The `⋯`: one quiet button per row, shown by hover, by focus, on the active row and
6322
+ while its menu is open — never a permanent control, and never a red one. What the
6323
+ recipe gives (shape, states, focus ring) it keeps; only the size and the resting
6324
+ colour are the row's. */
6325
+ .aparte-conv-item__more {
6326
+ flex-shrink: 0;
6190
6327
  opacity: 0;
6328
+ width: var(--aparte-conv-action-btn-size);
6329
+ height: var(--aparte-conv-action-btn-size);
6330
+ color: var(--aparte-text-muted);
6191
6331
  }
6192
6332
 
6193
- .aparte-conv-item__title {
6333
+ .aparte-conv-item:hover .aparte-conv-item__more,
6334
+ .aparte-conv-item:focus-within .aparte-conv-item__more,
6335
+ .aparte-conv-item--active .aparte-conv-item__more,
6336
+ .aparte-conv-item__more[aria-expanded="true"] {
6337
+ opacity: 1;
6338
+ }
6339
+
6340
+ .aparte-conv-item__more:hover {
6341
+ color: var(--aparte-text);
6342
+ }
6343
+
6344
+ /* The rename input takes the title button's place, so it inherits the row's slot and
6345
+ only needs the field recipe plus a little air. */
6346
+ .aparte-conv-item__input {
6194
6347
  flex: 1;
6195
- white-space: nowrap;
6196
- overflow: hidden;
6197
- text-overflow: ellipsis;
6198
6348
  min-width: 0;
6349
+ margin: var(--aparte-space-1) 0 var(--aparte-space-1) var(--aparte-space-1);
6199
6350
  }
6200
6351
 
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);
6352
+ /* The row's menu: the menu recipe draws it, the element places it (`position: fixed`,
6353
+ `top`/`left` computed at open time, closed on any scroll). Fixed rather than
6354
+ absolute because the list scrolls, and an absolute menu on the last rows was clipped
6355
+ by that overflow. */
6356
+ .aparte-conv-menu {
6357
+ position: fixed;
6358
+ --aparte-icon-size: var(--aparte-icon-size-sm);
6216
6359
  }
6217
6360
 
6218
- .aparte-conv-item__delete:hover {
6219
- color: var(--aparte-conv-delete-color-hover);
6361
+ .aparte-conv-menu__item--danger {
6362
+ color: var(--aparte-error);
6220
6363
  }
6221
6364
 
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)));
6365
+ /* The one question the menu asks. */
6366
+ .aparte-conv-menu__confirm {
6367
+ display: flex;
6368
+ flex-direction: column;
6369
+ gap: var(--aparte-space-3);
6370
+ padding: var(--aparte-space-2) var(--aparte-space-3);
6231
6371
  }
6232
6372
 
6233
- .aparte-conv-item__archive:hover {
6234
- color: var(--aparte-conv-archive-color-hover, var(--aparte-text));
6373
+ .aparte-conv-menu__question {
6374
+ margin: 0;
6375
+ font-size: var(--aparte-font-size-sm);
6376
+ line-height: var(--aparte-line-height-snug);
6235
6377
  }
6236
6378
 
6237
- .aparte-conv-item--archived .aparte-conv-item__title {
6238
- font-style: italic;
6239
- opacity: 0.85;
6379
+ .aparte-conv-menu__actions {
6380
+ display: flex;
6381
+ justify-content: flex-end;
6382
+ gap: var(--aparte-space-2);
6240
6383
  }
6241
6384
  /*
6242
6385
  * aparté — <aparte-suggestions>: the row of prompt starters.
@@ -6309,6 +6452,682 @@ aparte-context[data-level="warn"] .aparte-progress__bar {
6309
6452
  aparte-context[data-level="danger"] .aparte-progress__bar {
6310
6453
  background: var(--aparte-error);
6311
6454
  }
6455
+
6456
+ /* The ring variant: a track and a value circle on a 36-unit box, sized by the two tokens.
6457
+ `pathLength="100"` on the value circle (set by the element) makes the dash the
6458
+ percentage itself, so the sheet needs no circumference. Rotated a quarter turn so the
6459
+ value starts at twelve o'clock. */
6460
+ .aparte-context--ring {
6461
+ gap: var(--aparte-space-1);
6462
+ }
6463
+
6464
+ .aparte-context__ring {
6465
+ flex: none;
6466
+ width: var(--aparte-context-ring-size);
6467
+ height: var(--aparte-context-ring-size);
6468
+ transform: rotate(-90deg);
6469
+ }
6470
+
6471
+ .aparte-context__track,
6472
+ .aparte-context__value {
6473
+ fill: none;
6474
+ stroke-width: var(--aparte-context-ring-stroke);
6475
+ }
6476
+
6477
+ .aparte-context__track {
6478
+ stroke: var(--aparte-surface-3);
6479
+ }
6480
+
6481
+ .aparte-context__value {
6482
+ --aparte-context-ratio: 0;
6483
+ stroke: var(--aparte-progress-fill);
6484
+ stroke-linecap: round;
6485
+ stroke-dasharray: var(--aparte-context-ratio) 100;
6486
+ transition: stroke-dasharray var(--aparte-duration-fast) var(--aparte-ease);
6487
+ }
6488
+
6489
+ .aparte-context__value--empty {
6490
+ visibility: hidden;
6491
+ }
6492
+
6493
+ aparte-context[data-level="warn"] .aparte-context__value {
6494
+ stroke: var(--aparte-warning);
6495
+ }
6496
+ aparte-context[data-level="danger"] .aparte-context__value {
6497
+ stroke: var(--aparte-error);
6498
+ }
6499
+ /*
6500
+ * aparté — <aparte-scroll-rail>: the ticks beside the transcript.
6501
+ *
6502
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6503
+ * them concatenated in that same order, the way a browser does.
6504
+ *
6505
+ * The rail floats on the transcript's end edge, vertically centred, as a child of the
6506
+ * chat host. `:has()` gives the host its containing block only while a rail is inside
6507
+ * it — a bare `position: relative` on every chat would have moved any absolutely
6508
+ * positioned child a consumer already placed there.
6509
+ */
6510
+ aparte-chat:has(> aparte-scroll-rail),
6511
+ [data-aparte-chat]:has(> aparte-scroll-rail) {
6512
+ position: relative;
6513
+ }
6514
+
6515
+ aparte-scroll-rail {
6516
+ position: absolute;
6517
+ inset-inline-end: var(--aparte-space-2);
6518
+ top: 50%;
6519
+ transform: translateY(-50%);
6520
+ z-index: var(--aparte-z-floating);
6521
+ display: block;
6522
+ width: var(--aparte-scroll-rail-width);
6523
+ max-height: calc(100% - 2 * var(--aparte-space-8));
6524
+ overflow: hidden;
6525
+ }
6526
+
6527
+ aparte-scroll-rail[hidden],
6528
+ aparte-scroll-rail[data-empty] {
6529
+ display: none;
6530
+ }
6531
+
6532
+ .aparte-scroll-rail__list {
6533
+ list-style: none;
6534
+ margin: 0;
6535
+ padding: 0;
6536
+ display: flex;
6537
+ flex-direction: column;
6538
+ align-items: flex-end;
6539
+ gap: var(--aparte-scroll-rail-gap);
6540
+ }
6541
+
6542
+ /* A tick is a real button, drawn as a line. The hit area is the gap around it, drawn
6543
+ by the pseudo-element so the line itself can stay two pixels. */
6544
+ .aparte-scroll-rail__tick {
6545
+ appearance: none;
6546
+ -webkit-appearance: none;
6547
+ position: relative;
6548
+ display: block;
6549
+ width: var(--aparte-scroll-rail-tick-size);
6550
+ height: var(--aparte-scroll-rail-tick-thickness);
6551
+ padding: 0;
6552
+ border: 0;
6553
+ border-radius: var(--aparte-radius-full);
6554
+ background: var(--aparte-border);
6555
+ cursor: pointer;
6556
+ transition:
6557
+ width var(--aparte-duration-fast) var(--aparte-ease),
6558
+ background var(--aparte-duration-fast) var(--aparte-ease);
6559
+ }
6560
+
6561
+ .aparte-scroll-rail__tick::before {
6562
+ content: "";
6563
+ position: absolute;
6564
+ inset: calc(-1 * var(--aparte-scroll-rail-gap) / 2) calc(-1 * var(--aparte-space-2));
6565
+ }
6566
+
6567
+ .aparte-scroll-rail__tick:hover {
6568
+ background: var(--aparte-text-muted);
6569
+ }
6570
+
6571
+ .aparte-scroll-rail__tick[aria-current="true"] {
6572
+ width: calc(var(--aparte-scroll-rail-tick-size) * 1.6);
6573
+ background: var(--aparte-primary);
6574
+ }
6575
+
6576
+ .aparte-scroll-rail__tick:focus-visible {
6577
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-primary);
6578
+ outline-offset: var(--aparte-focus-outline-offset);
6579
+ }
6580
+
6581
+ @media (prefers-reduced-motion: reduce) {
6582
+ .aparte-scroll-rail__tick {
6583
+ transition: none;
6584
+ }
6585
+ }
6586
+ /*
6587
+ * aparté — the shell: the chrome a whole application puts around the chat. A sidebar
6588
+ * of conversations, a header, and the grid that holds them beside the transcript —
6589
+ * what a ChatGPT-style page is made of, so a site can be built on aparté alone.
6590
+ *
6591
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6592
+ * them concatenated in that same order, the way a browser does.
6593
+ */
6594
+
6595
+ /*
6596
+ * aparte-sidebar — the column beside the chat.
6597
+ * A sized column that becomes a drawer under 48rem of window. The recipe draws the
6598
+ * column and its four regions; `<aparte-sidebar>` adds the behaviour (collapse, the
6599
+ * drawer, the search filter) and sets `data-drawer` on itself from the media query,
6600
+ * which is the one attribute the rules below read.
6601
+ *
6602
+ * <aside class="aparte-sidebar">
6603
+ * <div class="aparte-sidebar__header">
6604
+ * <span class="aparte-sidebar__brand">aparté</span>
6605
+ * <button class="aparte-btn aparte-btn--icon aparte-btn--sm" type="button" aria-label="New chat">
6606
+ * <aparte-icon name="edit"></aparte-icon>
6607
+ * </button>
6608
+ * </div>
6609
+ * <div class="aparte-sidebar__search aparte-field-group">
6610
+ * <input class="aparte-field aparte-field--sm" type="search" placeholder="Search" data-aparte-sidebar-search>
6611
+ * </div>
6612
+ * <div class="aparte-sidebar__body">
6613
+ * <aparte-conversation-list></aparte-conversation-list>
6614
+ * </div>
6615
+ * <div class="aparte-sidebar__footer">
6616
+ * <span class="aparte-avatar aparte-avatar--sm">P</span> Paul
6617
+ * </div>
6618
+ * </aside>
6619
+ *
6620
+ * The element wears the same class and adds the behaviour; `<aside class="aparte-sidebar">`
6621
+ * is the look alone, which is what this page shows.
6622
+ */
6623
+ aparte-sidebar,
6624
+ .aparte-sidebar {
6625
+ box-sizing: border-box;
6626
+ display: flex;
6627
+ flex-direction: column;
6628
+ width: var(--aparte-sidebar-width);
6629
+ min-width: 0;
6630
+ height: 100%;
6631
+ overflow: hidden;
6632
+ background: var(--aparte-sidebar-bg);
6633
+ border-inline-end: var(--aparte-border-width) solid var(--aparte-border);
6634
+ color: var(--aparte-text);
6635
+ transition: width var(--aparte-duration-slow) var(--aparte-ease);
6636
+ }
6637
+
6638
+ /* In the flow, collapsed means gone: the column folds to nothing and the chat takes
6639
+ the room. The children keep their width so the fold is a slide, not a squash. */
6640
+ .aparte-sidebar[collapsed]:not([data-drawer]) {
6641
+ width: 0;
6642
+ border-inline-end-width: 0;
6643
+ }
6644
+
6645
+ /* The children keep the column's width while it collapses, so nothing reflows during
6646
+ the transition. BORDER-box, or a padded child is the column plus its padding: the
6647
+ header's button sat flush against the edge and the search field ran off it, clipped
6648
+ — measured at 292px in a 260px column. The search field carries margins as well,
6649
+ which border-box does not cover; its own rule subtracts them. */
6650
+ .aparte-sidebar > * {
6651
+ box-sizing: border-box;
6652
+ flex-shrink: 0;
6653
+ min-width: var(--aparte-sidebar-width);
6654
+ }
6655
+
6656
+ .aparte-sidebar__header {
6657
+ display: flex;
6658
+ align-items: center;
6659
+ gap: var(--aparte-space-3);
6660
+ padding: var(--aparte-space-4) var(--aparte-space-4) var(--aparte-space-2);
6661
+ }
6662
+
6663
+ .aparte-sidebar__brand {
6664
+ flex: 1;
6665
+ min-width: 0;
6666
+ font-weight: var(--aparte-font-weight-semibold);
6667
+ white-space: nowrap;
6668
+ overflow: hidden;
6669
+ text-overflow: ellipsis;
6670
+ }
6671
+
6672
+ .aparte-sidebar__search {
6673
+ margin: var(--aparte-space-2) var(--aparte-space-3);
6674
+ min-width: calc(var(--aparte-sidebar-width) - 2 * var(--aparte-space-3));
6675
+ }
6676
+
6677
+ /* The body is the one region that scrolls; the list inside keeps its own gap. */
6678
+ .aparte-sidebar__body {
6679
+ flex: 1 1 auto;
6680
+ min-height: 0;
6681
+ overflow-y: auto;
6682
+ overflow-x: hidden;
6683
+ padding: var(--aparte-space-2) var(--aparte-space-3);
6684
+ scrollbar-width: thin;
6685
+ scrollbar-color: var(--aparte-scrollbar-thumb) var(--aparte-scrollbar-track);
6686
+ }
6687
+
6688
+ .aparte-sidebar__footer {
6689
+ display: flex;
6690
+ align-items: center;
6691
+ gap: var(--aparte-space-3);
6692
+ padding: var(--aparte-space-3) var(--aparte-space-4);
6693
+ border-block-start: var(--aparte-border-width) solid var(--aparte-border);
6694
+ font-size: var(--aparte-font-size-sm);
6695
+ }
6696
+
6697
+ /* The drawer: the same element, out of the flow, over the page. `inset-inline-start`
6698
+ rather than a transform, so the scrim — a fixed child — stays relative to the
6699
+ viewport and can cover the page from inside the element. */
6700
+ .aparte-sidebar[data-drawer] {
6701
+ position: fixed;
6702
+ inset-block: 0;
6703
+ inset-inline-start: 0;
6704
+ z-index: var(--aparte-z-dropdown);
6705
+ width: var(--aparte-sidebar-width);
6706
+ box-shadow: var(--aparte-menu-shadow);
6707
+ transition: inset-inline-start var(--aparte-duration-slower) var(--aparte-ease);
6708
+ }
6709
+
6710
+ .aparte-sidebar[data-drawer][collapsed] {
6711
+ inset-inline-start: calc(-1 * var(--aparte-sidebar-width) - var(--aparte-space-4));
6712
+ box-shadow: none;
6713
+ }
6714
+
6715
+ .aparte-sidebar__scrim {
6716
+ position: fixed;
6717
+ inset: 0;
6718
+ z-index: -1;
6719
+ background: var(--aparte-scrim);
6720
+ }
6721
+ /*
6722
+ * aparté — the header row of an application shell.
6723
+ *
6724
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6725
+ * them concatenated in that same order, the way a browser does.
6726
+ */
6727
+
6728
+ /*
6729
+ * aparte-app-header — the bar above the chat.
6730
+ * A recipe and nothing more, because a header has no behaviour of its own: a toggle
6731
+ * for the sidebar (which `<aparte-sidebar>` wires from the `data-aparte-sidebar-toggle`
6732
+ * attribute), the conversation's title, and an actions zone pushed to the end — a
6733
+ * model selector, a share button, whatever the page needs. The toggle shows only
6734
+ * under 48rem, where the sidebar is a drawer; above it the column is simply there.
6735
+ *
6736
+ * <header class="aparte-app-header">
6737
+ * <button class="aparte-btn aparte-btn--icon aparte-app-header__toggle" type="button" aria-label="Toggle the sidebar" data-aparte-sidebar-toggle>☰</button>
6738
+ * <span class="aparte-app-header__title">Deploy checklist</span>
6739
+ * <div class="aparte-app-header__actions">
6740
+ * <span class="aparte-tag">gpt-4.1</span>
6741
+ * <button class="aparte-btn aparte-btn--sm aparte-btn--ghost" type="button">Share</button>
6742
+ * </div>
6743
+ * </header>
6744
+ */
6745
+ .aparte-app-header {
6746
+ box-sizing: border-box;
6747
+ display: flex;
6748
+ align-items: center;
6749
+ gap: var(--aparte-space-3);
6750
+ min-height: var(--aparte-app-header-height);
6751
+ padding: var(--aparte-space-2) var(--aparte-space-4);
6752
+ background: var(--aparte-surface-1);
6753
+ border-block-end: var(--aparte-border-width) solid var(--aparte-border);
6754
+ color: var(--aparte-text);
6755
+ }
6756
+
6757
+ .aparte-app-header__title {
6758
+ flex: 1;
6759
+ min-width: 0;
6760
+ font-weight: var(--aparte-font-weight-medium);
6761
+ white-space: nowrap;
6762
+ overflow: hidden;
6763
+ text-overflow: ellipsis;
6764
+ }
6765
+
6766
+ .aparte-app-header__actions {
6767
+ display: flex;
6768
+ align-items: center;
6769
+ gap: var(--aparte-space-2);
6770
+ margin-inline-start: auto;
6771
+ }
6772
+
6773
+ .aparte-app-header__toggle {
6774
+ display: none;
6775
+ }
6776
+
6777
+ /* The same 48rem the sidebar turns into a drawer at (`sidebar.css`). */
6778
+ @media (max-width: 48rem) {
6779
+ .aparte-app-header__toggle {
6780
+ display: inline-flex;
6781
+ }
6782
+ }
6783
+ /*
6784
+ * aparté — the grid that holds a sidebar, a header and the chat.
6785
+ *
6786
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6787
+ * them concatenated in that same order, the way a browser does.
6788
+ */
6789
+
6790
+ /*
6791
+ * aparte-app-shell — sidebar beside, header above, the chat in the rest.
6792
+ * A recipe: two columns and two rows, the sidebar spanning both rows, the header and
6793
+ * the main area stacked in the second column. The chat fills `__main`, which is what
6794
+ * gives it the height it needs to scroll (see the layout guide). Size the shell —
6795
+ * `height: 100dvh` for a page — and everything inside follows. Under 48rem the sidebar
6796
+ * leaves the grid to become a drawer, and the header's toggle appears.
6797
+ *
6798
+ * <div class="aparte-app-shell" style="height: 18rem">
6799
+ * <aside class="aparte-sidebar">
6800
+ * <div class="aparte-sidebar__header"><span class="aparte-sidebar__brand">aparté</span></div>
6801
+ * <div class="aparte-sidebar__body"><aparte-conversation-list></aparte-conversation-list></div>
6802
+ * </aside>
6803
+ * <header class="aparte-app-header"><span class="aparte-app-header__title">Deploy checklist</span></header>
6804
+ * <main class="aparte-app-shell__main">
6805
+ * <aparte-chat>
6806
+ * <aparte-chat-viewport></aparte-chat-viewport>
6807
+ * <aparte-composer>
6808
+ * <div class="aparte-composer-shell">
6809
+ * <div class="aparte-composer-row">
6810
+ * <aparte-composer-input style="flex: 1"></aparte-composer-input>
6811
+ * <aparte-composer-send></aparte-composer-send>
6812
+ * </div>
6813
+ * </div>
6814
+ * </aparte-composer>
6815
+ * </aparte-chat>
6816
+ * </main>
6817
+ * </div>
6818
+ */
6819
+ .aparte-app-shell {
6820
+ box-sizing: border-box;
6821
+ display: grid;
6822
+ grid-template-columns: auto minmax(0, 1fr);
6823
+ grid-template-rows: auto minmax(0, 1fr);
6824
+ height: 100%;
6825
+ overflow: hidden;
6826
+ background: var(--aparte-surface);
6827
+ }
6828
+
6829
+ .aparte-app-shell > aparte-sidebar,
6830
+ .aparte-app-shell > .aparte-sidebar {
6831
+ grid-row: 1 / -1;
6832
+ }
6833
+
6834
+ /* A drawer is out of the flow, so the grid closes the column it left. */
6835
+ .aparte-app-shell > .aparte-sidebar[data-drawer] {
6836
+ grid-row: auto;
6837
+ }
6838
+
6839
+ .aparte-app-shell > .aparte-app-header {
6840
+ grid-column: 2;
6841
+ }
6842
+
6843
+ .aparte-app-shell__main {
6844
+ grid-column: 2;
6845
+ min-height: 0;
6846
+ min-width: 0;
6847
+ display: flex;
6848
+ flex-direction: column;
6849
+ }
6850
+
6851
+ /* A chat — or a split holding one — fills the main area. Stated for the direct child
6852
+ because that is where the height comes from: `<aparte-chat>` is `height: 100%` of a
6853
+ parent that has none until this rule gives it one, and a split between the two is
6854
+ the same case one level out. A pane CONTAINS a chat; a chat never contains a split. */
6855
+ .aparte-app-shell__main > aparte-chat,
6856
+ .aparte-app-shell__main > [data-aparte-chat],
6857
+ .aparte-app-shell__main > aparte-split,
6858
+ .aparte-app-shell__main > .aparte-split {
6859
+ flex: 1;
6860
+ min-height: 0;
6861
+ height: auto;
6862
+ }
6863
+ /*
6864
+ * aparté — the seam: two panes side by side, and the grid that lets one grow while
6865
+ * the other gives way. The fourth piece of the shell, after the sidebar, the header
6866
+ * and the grid that holds them.
6867
+ *
6868
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6869
+ * them concatenated in that same order, the way a browser does.
6870
+ */
6871
+
6872
+ /*
6873
+ * aparte-split — two panes and a draggable seam. The chat in one, your pane in the
6874
+ * other: a preview frame, an editor, an artifact card.
6875
+ *
6876
+ * The grid is the whole mechanism. `--aparte-split-position` is the primary pane's
6877
+ * size; `--aparte-split-min` and `--aparte-split-max` are clamp bounds, so the
6878
+ * browser does the clamping and nothing in JS parses a unit — px, %, rem and ch all
6879
+ * work. Used without <aparte-split> this is a static split: set the position from
6880
+ * your own media query and there is no drag, which is the Canvas shape.
6881
+ *
6882
+ * A pane CONTAINS a chat; a chat never contains a split.
6883
+ *
6884
+ * <div class="aparte-split" style="height: 18rem">
6885
+ * <div>the chat</div>
6886
+ * <div class="aparte-split__handle"></div>
6887
+ * <div class="aparte-split__pane">the pane</div>
6888
+ * </div>
6889
+ *
6890
+ * The element wears the same class and adds the behaviour (drag, arrow keys, the APG
6891
+ * separator's ARIA, one pane at a time under a breakpoint); `<div class="aparte-split">`
6892
+ * is the look alone, which is what this page shows.
6893
+ */
6894
+
6895
+ /* The outer clamp is a guard, not decoration: a `--aparte-split-min` wider than the
6896
+ container would otherwise eat the free pane whole and push the handle off the end.
6897
+ Three tracks, always — the primary pane, the seam, the rest. */
6898
+ aparte-split,
6899
+ .aparte-split {
6900
+ box-sizing: border-box;
6901
+ display: grid;
6902
+ grid-template-columns:
6903
+ clamp(
6904
+ 0px,
6905
+ clamp(var(--aparte-split-min), var(--aparte-split-position), var(--aparte-split-max)),
6906
+ calc(100% - var(--aparte-split-handle-size))
6907
+ )
6908
+ var(--aparte-split-handle-size)
6909
+ minmax(0, 1fr);
6910
+ height: 100%;
6911
+ min-width: 0;
6912
+ overflow: hidden;
6913
+ }
6914
+
6915
+ /* A grid item's automatic minimum size is its content, so one long line in either
6916
+ pane refuses to shrink and the seam stops tracking the pointer.
6917
+
6918
+ The tag is listed beside the class here and below for the reason the grid rule
6919
+ lists it: the element adds the class at connect, so between parse and upgrade a
6920
+ server-rendered split would be a grid whose items keep their content minimum. */
6921
+ aparte-split > *,
6922
+ .aparte-split > * {
6923
+ min-inline-size: 0;
6924
+ min-block-size: 0;
6925
+ }
6926
+
6927
+ /* The outside-the-shell half of the direct-child trap: <aparte-chat> is `height: 100%`
6928
+ of a parent that has none unless the grid track gives it one. Inside the app shell
6929
+ the same fix lives on `.aparte-app-shell__main`; split.css is imported after
6930
+ app-shell.css, so a chat in a pane inside a shell reads this one.
6931
+
6932
+ `box-sizing` with it, because the chat's own sheet gives it a bottom padding — the
6933
+ 16px of breathing room under the composer that core owns on purpose. Left in the
6934
+ default content-box that padding lands OUTSIDE the 100%, so the chat's border box
6935
+ overflows its track by exactly the gap and `.aparte-split`'s `overflow: hidden`
6936
+ eats it: the composer sits flush on the seam's bottom edge and the gap is gone. */
6937
+ aparte-split > aparte-chat,
6938
+ aparte-split > [data-aparte-chat],
6939
+ .aparte-split > aparte-chat,
6940
+ .aparte-split > [data-aparte-chat] {
6941
+ box-sizing: border-box;
6942
+ height: 100%;
6943
+ min-height: 0;
6944
+ }
6945
+
6946
+ /* The seam. `touch-action: none` so a drag on a touch screen resizes rather than
6947
+ scrolling the page — without it the browser claims the gesture at the first move
6948
+ and the pointer events stop arriving. */
6949
+ .aparte-split__handle {
6950
+ position: relative;
6951
+ box-sizing: border-box;
6952
+ border: 0;
6953
+ padding: 0;
6954
+ background: var(--aparte-border);
6955
+ cursor: col-resize;
6956
+ touch-action: none;
6957
+ transition: background-color var(--aparte-duration-fast) var(--aparte-ease);
6958
+ }
6959
+
6960
+ .aparte-split__handle:hover,
6961
+ .aparte-split__handle[data-dragging] {
6962
+ background: var(--aparte-primary);
6963
+ }
6964
+
6965
+ /* A real outline, and the ring kept as decoration.
6966
+
6967
+ The seam's whole keyboard story is arrowing it, and its only focus indicator was
6968
+ `--aparte-focus-ring` — a soft wash that measures 1.39:1 against the page in the
6969
+ light palette and 1.83:1 in the dark one, where WCAG asks 3:1 of a focus indicator.
6970
+ `outline: none` with nothing in its place is the shape of that failure: the
6971
+ indicator was not weak, it was absent, on a 4px band with `border: 0` that a
6972
+ keyboard user has no other way to find.
6973
+
6974
+ The outline is what carries the contrast (3.54:1 light, 7.36:1 dark, measured); the
6975
+ shadow stays because it reads as a glow AROUND the seam and the two do not fight.
6976
+ The forced-colors entry in responsive.css keeps overriding the colour only — it is
6977
+ now overriding an outline that exists rather than substituting for one that does
6978
+ not. */
6979
+ .aparte-split__handle:focus-visible {
6980
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
6981
+ outline-offset: 0;
6982
+ box-shadow: var(--aparte-focus-ring);
6983
+ }
6984
+
6985
+ /* The hit slop: a 4px seam is a 4px target, and a pointer misses it. The pseudo-element
6986
+ is wider than the seam and centred on it, so the grab zone reaches into both panes
6987
+ without the seam itself getting bigger. */
6988
+ .aparte-split__handle::after {
6989
+ content: '';
6990
+ position: absolute;
6991
+ inset-block: 0;
6992
+ inset-inline-start: calc(var(--aparte-split-hit-area) / -2 + var(--aparte-split-handle-size) / 2);
6993
+ inline-size: var(--aparte-split-hit-area);
6994
+ }
6995
+
6996
+ /* The drag overlay, a child of the HANDLE for the duration of one gesture. Being the
6997
+ topmost hit target it swallows an iframe pane's hit-testing (a drag over a frame is
6998
+ otherwise lost to the frame's document), stops a text selection from starting, and
6999
+ paints the drag cursor across the whole page. A child of the handle rather than of
7000
+ the body so `click` — and therefore `dblclick` — still dispatches at the handle,
7001
+ their nearest common ancestor.
7002
+
7003
+ It is `position: fixed`, so nothing between it and the viewport may establish a
7004
+ containing block: `.aparte-split` must never get `container-type`, `transform`,
7005
+ `filter` or `backdrop-filter`, or the overlay covers the split alone. */
7006
+ .aparte-split__scrim {
7007
+ position: fixed;
7008
+ inset: 0;
7009
+ z-index: var(--aparte-z-dropdown);
7010
+ cursor: col-resize;
7011
+ }
7012
+
7013
+ /* An optional wrapper for the pane that is not a chat — the pane equivalent of
7014
+ `.aparte-sidebar__body`. The chat pane does not use it: the viewport scrolls itself. */
7015
+ .aparte-split__pane {
7016
+ box-sizing: border-box;
7017
+ min-inline-size: 0;
7018
+ min-block-size: 0;
7019
+ overflow: auto;
7020
+ background: var(--aparte-surface-1);
7021
+ }
7022
+
7023
+ /* `primary="end"` sizes the LAST pane instead of the first: the same three tracks,
7024
+ read the other way round. */
7025
+ aparte-split[primary='end'],
7026
+ .aparte-split--primary-end {
7027
+ grid-template-columns:
7028
+ minmax(0, 1fr)
7029
+ var(--aparte-split-handle-size)
7030
+ clamp(
7031
+ 0px,
7032
+ clamp(var(--aparte-split-min), var(--aparte-split-position), var(--aparte-split-max)),
7033
+ calc(100% - var(--aparte-split-handle-size))
7034
+ );
7035
+ }
7036
+
7037
+ /* The other axis. The attribute and the class are selected in the same rule so the
7038
+ static recipe and the live element can never draw two different things. */
7039
+ aparte-split[orientation='vertical'],
7040
+ .aparte-split--vertical {
7041
+ grid-template-columns: none;
7042
+ grid-template-rows:
7043
+ clamp(
7044
+ 0px,
7045
+ clamp(var(--aparte-split-min), var(--aparte-split-position), var(--aparte-split-max)),
7046
+ calc(100% - var(--aparte-split-handle-size))
7047
+ )
7048
+ var(--aparte-split-handle-size)
7049
+ minmax(0, 1fr);
7050
+ }
7051
+
7052
+ /* The one rule in this sheet that out-specifies the stacked block below — two
7053
+ attributes against its one — so it has to opt out of the stacked state by hand or a
7054
+ vertical `primary="end"` split keeps three rows while showing one pane, and the two
7055
+ empty tracks take the height off the pane that is left. */
7056
+ aparte-split[orientation='vertical'][primary='end']:not([data-stacked]),
7057
+ .aparte-split--vertical.aparte-split--primary-end:not(.aparte-split--only-start):not(.aparte-split--only-end) {
7058
+ grid-template-rows:
7059
+ minmax(0, 1fr)
7060
+ var(--aparte-split-handle-size)
7061
+ clamp(
7062
+ 0px,
7063
+ clamp(var(--aparte-split-min), var(--aparte-split-position), var(--aparte-split-max)),
7064
+ calc(100% - var(--aparte-split-handle-size))
7065
+ );
7066
+ }
7067
+
7068
+ aparte-split[orientation='vertical'] > .aparte-split__handle,
7069
+ .aparte-split--vertical > .aparte-split__handle {
7070
+ cursor: row-resize;
7071
+ }
7072
+
7073
+ aparte-split[orientation='vertical'] > .aparte-split__handle::after,
7074
+ .aparte-split--vertical > .aparte-split__handle::after {
7075
+ inset-block: calc(var(--aparte-split-hit-area) / -2 + var(--aparte-split-handle-size) / 2) auto;
7076
+ inset-inline: 0;
7077
+ inline-size: auto;
7078
+ block-size: var(--aparte-split-hit-area);
7079
+ }
7080
+
7081
+ aparte-split[orientation='vertical'] .aparte-split__scrim,
7082
+ .aparte-split--vertical .aparte-split__scrim {
7083
+ cursor: row-resize;
7084
+ }
7085
+
7086
+ /* One pane at a time. `data-stacked` is written by the element from its breakpoint;
7087
+ the two modifier classes are the byte-identical CSS route for a host that owns its
7088
+ own breakpoint. The shown pane spans all three tracks and the seam goes with the
7089
+ pane it no longer separates — including its tab stop, which the element removes.
7090
+
7091
+ Not "position: 100% and a zero-size handle": that leaves a focusable separator of
7092
+ no width sitting in the tab order. */
7093
+ aparte-split[data-stacked],
7094
+ .aparte-split--only-start,
7095
+ .aparte-split--only-end {
7096
+ grid-template-columns: minmax(0, 1fr);
7097
+ grid-template-rows: minmax(0, 1fr);
7098
+ }
7099
+
7100
+ aparte-split[data-stacked] > .aparte-split__handle,
7101
+ .aparte-split--only-start > .aparte-split__handle,
7102
+ .aparte-split--only-end > .aparte-split__handle {
7103
+ display: none;
7104
+ }
7105
+
7106
+ aparte-split[data-stacked]:not([pane='end']) > :nth-child(3),
7107
+ .aparte-split--only-start > :nth-child(3) {
7108
+ display: none;
7109
+ }
7110
+
7111
+ aparte-split[data-stacked][pane='end'] > :nth-child(1),
7112
+ .aparte-split--only-end > :nth-child(1) {
7113
+ display: none;
7114
+ }
7115
+
7116
+ /* `disabled` is inert, so it must not look grabbable: the element takes the tab stop
7117
+ away and swallows the pointer, and a resize cursor plus a hover highlight on a seam
7118
+ that answers nothing is exactly the half-rendered affordance the house rule forbids.
7119
+ The seam itself stays drawn — it still separates two panes.
7120
+
7121
+ Last in the sheet because it must out-rank the vertical `cursor: row-resize` above,
7122
+ which it ties with on specificity. Attribute only: the static recipe has no
7123
+ behaviour, so there is no live-versus-static pair to keep in step. */
7124
+ aparte-split[disabled] > .aparte-split__handle {
7125
+ cursor: default;
7126
+ }
7127
+
7128
+ aparte-split[disabled] > .aparte-split__handle:hover {
7129
+ background: var(--aparte-border);
7130
+ }
6312
7131
  /*
6313
7132
  * aparté — Reset-agnostic typography for model-authored markdown.
6314
7133
  *
@@ -6489,22 +7308,6 @@ aparte-context[data-level="danger"] .aparte-progress__bar {
6489
7308
  overflow-x: hidden;
6490
7309
  background: var(--aparte-code-bg);
6491
7310
  }
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
7311
  /*
6509
7312
  * aparté — Container-width and pointer-modality adjustments. LAST on purpose.
6510
7313
  *
@@ -6531,14 +7334,17 @@ aparte-context[data-level="danger"] .aparte-progress__bar {
6531
7334
  }
6532
7335
 
6533
7336
  /* Windows high contrast. In forced-colors mode the UA repaints colours and
6534
- drops box-shadow entirely — so the two focus indicators drawn with
7337
+ drops box-shadow entirely — so the focus indicators drawn with
6535
7338
  --aparte-focus-ring, and the error ring on an avatar, do not merely change
6536
7339
  colour, they VANISH. An outline is painted, and `Highlight` is the system's
6537
7340
  own focus colour, so the indicator survives without fighting the user's
6538
7341
  palette. Only the indicators that disappear are restated here: everything
6539
- whose affordance is text or a border already survives on its own. */
7342
+ whose affordance is text or a border already survives on its own — which the
7343
+ split's seam does not, being a 4px band with `border: 0` whose entire keyboard
7344
+ story is arrowing it. */
6540
7345
  @media (forced-colors: active) {
6541
7346
  .aparte-thumb--image[role='button']:focus-visible,
7347
+ .aparte-split__handle:focus-visible,
6542
7348
  .aparte-provider-select:focus,
6543
7349
  .aparte-model-select:focus {
6544
7350
  outline: var(--aparte-focus-outline-width) solid Highlight;
@@ -6626,11 +7432,26 @@ aparte-context[data-level="danger"] .aparte-progress__bar {
6626
7432
  opacity: 1;
6627
7433
  }
6628
7434
 
6629
- /* Hover cannot reveal these on touch keep them visible and tappable. */
6630
- .aparte-conv-item__delete,
6631
- .aparte-conv-item__archive {
7435
+ /* Nor the on a pending attachment, which is the only way to remove one. */
7436
+ .aparte-thumb__remove {
7437
+ opacity: 1;
7438
+ }
7439
+
7440
+ /* Hover cannot reveal the row's ⋯ on touch — keep it visible and tappable. */
7441
+ .aparte-conv-item__more {
6632
7442
  opacity: 1;
6633
7443
  --aparte-conv-action-btn-size: 28px;
6634
7444
  }
7445
+
7446
+ /* A 4px seam is no target for a finger — the same modality rule the buttons above
7447
+ follow. The seam keeps its width; only the invisible grab zone grows. */
7448
+ .aparte-split__handle {
7449
+ --aparte-split-hit-area: var(--aparte-touch-target-size);
7450
+ }
7451
+
7452
+ /* A 2px tick is no target for a finger; the minimaps of the market hide on touch too. */
7453
+ aparte-scroll-rail {
7454
+ display: none;
7455
+ }
6635
7456
  }
6636
7457