@aparte/core 0.13.1 → 0.14.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 (95) hide show
  1. package/README.md +2 -2
  2. package/dist/client/aparte-client.d.ts +36 -35
  3. package/dist/client/aparte-client.d.ts.map +1 -1
  4. package/dist/client/stream-adapter.d.ts +37 -145
  5. package/dist/client/stream-adapter.d.ts.map +1 -1
  6. package/dist/components/bubble/aparte-chat-bubble.d.ts +10 -2
  7. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  8. package/dist/components/composer/aparte-composer-add-attachment.d.ts +6 -6
  9. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  10. package/dist/components/composer/aparte-composer-input.d.ts +9 -1
  11. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  12. package/dist/components/composer/aparte-composer.d.ts +12 -3
  13. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  14. package/dist/components/context/aparte-context.d.ts +81 -0
  15. package/dist/components/context/aparte-context.d.ts.map +1 -0
  16. package/dist/components/context/index.d.ts +3 -0
  17. package/dist/components/context/index.d.ts.map +1 -0
  18. package/dist/components/index.d.ts +4 -0
  19. package/dist/components/index.d.ts.map +1 -1
  20. package/dist/components/suggestions/aparte-suggestions.d.ts +95 -0
  21. package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -0
  22. package/dist/components/suggestions/index.d.ts +3 -0
  23. package/dist/components/suggestions/index.d.ts.map +1 -0
  24. package/dist/components/viewport/aparte-chat-viewport.d.ts +35 -7
  25. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  26. package/dist/config/aparte-config.d.ts +1 -21
  27. package/dist/config/aparte-config.d.ts.map +1 -1
  28. package/dist/config/index.d.ts +0 -2
  29. package/dist/config/index.d.ts.map +1 -1
  30. package/dist/config/locale.d.ts +30 -1
  31. package/dist/config/locale.d.ts.map +1 -1
  32. package/dist/conversations/index.d.ts +1 -1
  33. package/dist/conversations/index.d.ts.map +1 -1
  34. package/dist/conversations/types.d.ts +9 -57
  35. package/dist/conversations/types.d.ts.map +1 -1
  36. package/dist/custom-elements.json +7818 -7479
  37. package/dist/generated/element-attributes.d.ts +34 -6
  38. package/dist/generated/element-attributes.d.ts.map +1 -1
  39. package/dist/{glyphs-QXfbL3n_.js → glyphs-B3BZ2TIb.js} +21 -21
  40. package/dist/glyphs-B3BZ2TIb.js.map +1 -0
  41. package/dist/host/aparte-chat-host.d.ts +1 -1
  42. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  43. package/dist/icons/extended.d.ts +87 -85
  44. package/dist/icons/extended.d.ts.map +1 -1
  45. package/dist/icons/glyphs.d.ts +53 -50
  46. package/dist/icons/glyphs.d.ts.map +1 -1
  47. package/dist/icons.js +41 -41
  48. package/dist/icons.js.map +1 -1
  49. package/dist/index.css +512 -170
  50. package/dist/index.d.ts +10 -4
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +593 -183
  53. package/dist/index.js.map +1 -1
  54. package/dist/index.node.d.ts +6 -4
  55. package/dist/index.node.d.ts.map +1 -1
  56. package/dist/index.node.js +56 -56
  57. package/dist/index.node.js.map +1 -1
  58. package/dist/{is-awaiting-reply-CWp1ffHh.js → is-awaiting-reply-BZ_iJ3s3.js} +175 -994
  59. package/dist/is-awaiting-reply-BZ_iJ3s3.js.map +1 -0
  60. package/dist/parsers/aparte-stream-parser.d.ts +6 -17
  61. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  62. package/dist/primitives/select/aparte-select.d.ts +2 -2
  63. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  64. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  65. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  66. package/dist/runtime/message-repository.d.ts +7 -0
  67. package/dist/runtime/message-repository.d.ts.map +1 -1
  68. package/dist/transport/types.d.ts +8 -0
  69. package/dist/transport/types.d.ts.map +1 -1
  70. package/dist/types/chat.d.ts +32 -38
  71. package/dist/types/chat.d.ts.map +1 -1
  72. package/dist/types/errors.d.ts +16 -1
  73. package/dist/types/errors.d.ts.map +1 -1
  74. package/dist/types/event-map.d.ts +4 -0
  75. package/dist/types/event-map.d.ts.map +1 -1
  76. package/dist/types/events.d.ts +15 -0
  77. package/dist/types/events.d.ts.map +1 -1
  78. package/dist/types/index.d.ts +1 -1
  79. package/dist/types/index.d.ts.map +1 -1
  80. package/dist/types/models.d.ts +1 -8
  81. package/dist/types/models.d.ts.map +1 -1
  82. package/dist/types/segments.d.ts +1 -13
  83. package/dist/types/segments.d.ts.map +1 -1
  84. package/dist/types/tools.d.ts +1 -1
  85. package/dist/utils/chat-boundary.d.ts +8 -4
  86. package/dist/utils/chat-boundary.d.ts.map +1 -1
  87. package/package.json +5 -2
  88. package/dist/client/xml-artifact-feed.d.ts +0 -76
  89. package/dist/client/xml-artifact-feed.d.ts.map +0 -1
  90. package/dist/config/skeleton-provider.d.ts +0 -26
  91. package/dist/config/skeleton-provider.d.ts.map +0 -1
  92. package/dist/glyphs-QXfbL3n_.js.map +0 -1
  93. package/dist/is-awaiting-reply-CWp1ffHh.js.map +0 -1
  94. package/dist/renderers/segments/pipeline-waiting.d.ts +0 -3
  95. package/dist/renderers/segments/pipeline-waiting.d.ts.map +0 -1
package/dist/index.css CHANGED
@@ -167,6 +167,10 @@
167
167
  --aparte-focus-outline-width: 2px;
168
168
  /* Scrollbars rendered by the library. */
169
169
  --aparte-scrollbar-width: 6px;
170
+ /* The transcript's scrollbar track; the thumb is derived from the neutral, in the
171
+ derived layer. A host page with a scrollbar of its own (the docs site has one)
172
+ sets both so the chat's does not read as a second, foreign scrollbar. */
173
+ --aparte-scrollbar-track: transparent;
170
174
  /* Comfortable hit-area on coarse (touch) pointers. */
171
175
  --aparte-touch-target-size: 44px;
172
176
 
@@ -202,6 +206,11 @@
202
206
 
203
207
  /* Action Bar */
204
208
  --aparte-action-bar-btn-size: 28px;
209
+ /* The mark (display/mark.css): how much of its intent a marked row's ground takes,
210
+ and how wide the bar on its start edge is. One pair for the select's chosen option,
211
+ the checked field choice, the active conversation and an answered question. */
212
+ --aparte-mark-tint: 18%;
213
+ --aparte-mark-bar: 2px;
205
214
 
206
215
  /* Branch Picker */
207
216
  --aparte-branch-picker-btn-size: 20px;
@@ -387,12 +396,12 @@
387
396
  a shadow tuned for a light ground is no shadow at all on a dark one, which is how
388
397
  the select's dropdown lost its lift in dark mode until it was caught. */
389
398
  --aparte-menu-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
390
- --aparte-popover-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
399
+ --aparte-popover-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 10px 28px rgba(0, 0, 0, 0.16);
391
400
  --aparte-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
392
401
  --aparte-switch-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
393
402
  --aparte-range-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
394
403
 
395
- --aparte-select-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
404
+ --aparte-select-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 10px 28px rgba(0, 0, 0, 0.16);
396
405
  --aparte-select-min-width: 200px;
397
406
  --aparte-select-dropdown-max-height: 300px;
398
407
  --aparte-select-status-dot-size: 8px;
@@ -440,8 +449,6 @@
440
449
  --aparte-art-file-icon-size: 40px;
441
450
  --aparte-art-file-preview-padding: 20px;
442
451
  --aparte-art-file-error-padding-inline: 18px;
443
- --aparte-pw-min-height: 28px;
444
- --aparte-pw-dot-size: 8px;
445
452
  }
446
453
 
447
454
  /* =========================================
@@ -477,9 +484,9 @@
477
484
 
478
485
  /* Scroll-to-bottom button */
479
486
  --aparte-scroll-btn-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
480
- --aparte-select-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
487
+ --aparte-select-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 10px 28px rgba(0, 0, 0, 0.55);
481
488
  --aparte-menu-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
482
- --aparte-popover-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
489
+ --aparte-popover-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 10px 28px rgba(0, 0, 0, 0.55);
483
490
  --aparte-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
484
491
  --aparte-switch-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
485
492
  --aparte-range-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
@@ -602,6 +609,7 @@ aparte-chat {
602
609
  --aparte-elic-step-padding: var(--aparte-space-2) var(--aparte-space-1);
603
610
  --aparte-elic-step-gap: var(--aparte-space-7);
604
611
  --aparte-viewport-padding: var(--aparte-space-8);
612
+ --aparte-scrollbar-thumb: var(--aparte-neutral);
605
613
  --aparte-input-padding-x: var(--aparte-space-6);
606
614
  --aparte-input-padding-y: var(--aparte-space-5);
607
615
  --aparte-conv-list-gap: var(--aparte-space-1);
@@ -980,8 +988,8 @@ aparte-conversation-list {
980
988
  curve, not a differently-named copy of this one. */
981
989
 
982
990
  /* One pulse for every dot the library shows. There were three keyframes doing the
983
- same thing at slightly different rhythms; the pipeline's travelling wave comes from
984
- the animation-delay on its second and third dot, not from a skewed curve. */
991
+ same thing at slightly different rhythms; a travelling wave, where a row wants one,
992
+ comes from an animation-delay on its later dots, not from a skewed curve. */
985
993
  @keyframes aparte-pulse {
986
994
  0%, 100% { opacity: 0.3; transform: scale(0.85); }
987
995
  50% { opacity: 1; transform: scale(1.1); }
@@ -2076,7 +2084,8 @@ aparte-conversation-list {
2076
2084
  background var(--aparte-duration-fast) var(--aparte-ease),
2077
2085
  border-color var(--aparte-duration-fast) var(--aparte-ease);
2078
2086
  }
2079
- .aparte-field-choice:hover { background: var(--aparte-surface-2); }
2087
+ /* Not on a chosen row: its mark (below) is what the pointer must not erase. */
2088
+ .aparte-field-choice:not(:has(:checked)):not(.aparte-field-choice--selected):hover { background: var(--aparte-surface-2); }
2080
2089
 
2081
2090
  /* Boxed — each row carries its own edge, for a list that must read as a set of
2082
2091
  separate targets rather than a menu. */
@@ -2084,9 +2093,31 @@ aparte-conversation-list {
2084
2093
 
2085
2094
  /* Selected. `:has()` covers the native case with no class to toggle; the explicit
2086
2095
  modifier covers a row whose state lives elsewhere — a button acting as an option,
2087
- where there is no checked input to look at. */
2096
+ where there is no checked input to look at. It wears the mark (display/mark.css):
2097
+ the tint and the start bar the select's chosen option wears, so a picked choice in
2098
+ an elicitation panel and a picked option in a dropdown say it the same way. The
2099
+ border stays its own — it was the only signal before, and a boxed row keeps its
2100
+ edge coloured. Written as the mark's declarations rather than its class because a
2101
+ `:has()` state cannot add a class to itself; the modifier is doubled so both halves
2102
+ of this rule sit at (0,2,0), above the hover. The bar is the mark's pseudo-element,
2103
+ on the logical start edge — see display/mark.css for why not a shadow. */
2088
2104
  .aparte-field-choice:has(:checked),
2089
- .aparte-field-choice--selected { border-color: var(--aparte-primary); }
2105
+ .aparte-field-choice.aparte-field-choice--selected {
2106
+ position: relative;
2107
+ border-color: var(--aparte-primary);
2108
+ background: color-mix(in srgb, var(--aparte-primary) var(--aparte-mark-tint), transparent);
2109
+ }
2110
+
2111
+ .aparte-field-choice:has(:checked)::before,
2112
+ .aparte-field-choice.aparte-field-choice--selected::before {
2113
+ content: "";
2114
+ position: absolute;
2115
+ inset-block: 0;
2116
+ inset-inline-start: 0;
2117
+ width: var(--aparte-mark-bar);
2118
+ background: var(--aparte-primary-ink);
2119
+ pointer-events: none;
2120
+ }
2090
2121
 
2091
2122
  .aparte-field-choice:has(:disabled),
2092
2123
  .aparte-field-choice[aria-disabled='true'] {
@@ -2512,6 +2543,98 @@ aparte-icon {
2512
2543
  @media (forced-colors: active) {
2513
2544
  .aparte-thumbnail { border-color: currentColor; }
2514
2545
  }
2546
+
2547
+ /* ── The attachment strip and its tiles ─────────────────────────────────────────
2548
+ Shared by the composer preview and the strip under a sent message: the bubble
2549
+ emits the very same classes. These rules lived in composer.css, a COMPONENT sheet,
2550
+ so the bubble — which imports no composer — rendered its file chips bare. A recipe
2551
+ lives in the display layer; the composer keeps only what is its own (the ✗ button). */
2552
+ .aparte-attachments {
2553
+ display: flex;
2554
+ flex-wrap: wrap;
2555
+ /* The strip sits directly above the message bubble and belongs to it, so it
2556
+ must share the bubble's edge. The user bubble hugs its text on the LEADING
2557
+ edge (AI-chat convention, see .aparte-message-content below), so anchoring
2558
+ the strip to the trailing edge would split one message across both sides. */
2559
+ justify-content: flex-start;
2560
+ gap: var(--aparte-space-3);
2561
+ margin-bottom: var(--aparte-space-4);
2562
+ /* Really small tiles inside the conversation. */
2563
+ --aparte-attachment-image-size: 40px;
2564
+ /* Many attachments → cap the height and scroll instead of growing forever. */
2565
+ max-height: var(--aparte-attachments-max-height);
2566
+ overflow-y: auto;
2567
+ }
2568
+
2569
+ /* The class `display: flex` above would otherwise override the `hidden`
2570
+ attribute — restore it so an empty attachment strip takes no space. */
2571
+ .aparte-attachments[hidden] {
2572
+ display: none;
2573
+ }
2574
+
2575
+ /* ── Attachment thumbnail tile ──────────────────────────────────────────────
2576
+ Shared by the composer attachment preview and the sent-message attachment
2577
+ strip. A square that shows the image itself (or a file glyph); the filename
2578
+ and — in the composer — a remove button surface on hover. */
2579
+ /* The tile is `.aparte-thumbnail`; the composer only sets its own measurements and
2580
+ the quieter ground an attachment strip uses. */
2581
+ .aparte-thumb {
2582
+ --aparte-thumbnail-size: var(--aparte-attachment-image-size);
2583
+ --aparte-thumbnail-radius: var(--aparte-thumb-radius);
2584
+ --aparte-thumbnail-bg: var(--aparte-attachment-chip-bg);
2585
+ --aparte-thumbnail-border: var(--aparte-attachment-chip-border);
2586
+ }
2587
+
2588
+ /* Only an image tile the app declared it can preview is a button — see
2589
+ aparteGlobalConfig.setHostHandlers(). Undeclared, it is a picture, not a control. */
2590
+ .aparte-thumb--image[role='button'] {
2591
+ cursor: pointer;
2592
+ }
2593
+
2594
+ .aparte-thumb--image[role='button']:focus-visible {
2595
+ box-shadow: var(--aparte-focus-ring);
2596
+ }
2597
+
2598
+ .aparte-thumb__img {
2599
+ width: 100%;
2600
+ height: 100%;
2601
+ object-fit: cover;
2602
+ display: block;
2603
+ }
2604
+
2605
+ .aparte-thumb--file {
2606
+ display: flex;
2607
+ align-items: center;
2608
+ justify-content: center;
2609
+ }
2610
+
2611
+ .aparte-thumb__ext {
2612
+ font-size: var(--aparte-font-size-xs);
2613
+ font-weight: var(--aparte-font-weight-bold);
2614
+ letter-spacing: 0.03em;
2615
+ color: var(--aparte-text-muted);
2616
+ }
2617
+
2618
+ .aparte-thumb__name {
2619
+ position: absolute;
2620
+ left: 0;
2621
+ right: 0;
2622
+ bottom: 0;
2623
+ padding: var(--aparte-thumb-name-padding);
2624
+ font-size: var(--aparte-font-size-2xs);
2625
+ line-height: var(--aparte-line-height-tight);
2626
+ color: var(--aparte-thumb-name-color);
2627
+ background: var(--aparte-thumb-name-scrim);
2628
+ white-space: nowrap;
2629
+ overflow: hidden;
2630
+ text-overflow: ellipsis;
2631
+ opacity: 0;
2632
+ transition: opacity var(--aparte-duration-base) var(--aparte-ease);
2633
+ pointer-events: none;
2634
+ }
2635
+ .aparte-thumb:hover .aparte-thumb__name {
2636
+ opacity: 1;
2637
+ }
2515
2638
  /* =========================================
2516
2639
  aparte-spinner — a rotating indicator
2517
2640
  =========================================
@@ -2653,29 +2776,6 @@ aparte-icon {
2653
2776
  0% { background-position: 200% 0; }
2654
2777
  100% { background-position: -200% 0; }
2655
2778
  }
2656
-
2657
- /* ── The zero-dependency fallbacks ────────────────────────────────────────────
2658
- What `getSkeleton()` returns when no skeleton provider is registered. They used
2659
- to carry their look in a `style=""` attribute — six inline declarations of
2660
- Tailwind-slate hex (`#9ca3af`, `#1e293b`, `#64748b`), the exact palette this
2661
- theme replaced. Three consequences, and the first is the one that matters: an
2662
- inline style cannot be overridden by a consumer's stylesheet, so a dark-theme
2663
- host got a light-grey label it had no way to reach. The second: hex in a `.ts`
2664
- is invisible to `check:derived-vars`, which reads only `styles/`. The third:
2665
- they had drifted from the recipe above, which already reads tokens.
2666
-
2667
- Structure only, still — no shimmer, no animation. That is what makes them the
2668
- fallback rather than a second skeleton implementation. */
2669
- .aparte-skeleton-fallback {
2670
- padding: var(--aparte-space-4);
2671
- color: var(--aparte-text-muted);
2672
- }
2673
- .aparte-skeleton-fallback--snug { padding: var(--aparte-space-3); }
2674
- .aparte-skeleton-fallback--tight { padding: var(--aparte-space-2); }
2675
- .aparte-skeleton-fallback--code {
2676
- background: var(--aparte-surface-2);
2677
- border-radius: var(--aparte-radius-md);
2678
- }
2679
2779
  /* =========================================
2680
2780
  aparte-divider — horizontal, vertical, optionally labelled
2681
2781
  =========================================
@@ -2914,6 +3014,87 @@ aparte-icon {
2914
3014
  line-height: var(--aparte-line-height-none);
2915
3015
  }
2916
3016
 
3017
+ /*
3018
+ * aparté — The mark: a row a list has settled on.
3019
+ *
3020
+ * One of the sheets split out of aparte.css. Order matters: src/index.ts imports
3021
+ * them in the order listed there, and check:derived-vars reads them concatenated in
3022
+ * that same order, the way a browser does.
3023
+ *
3024
+ * WHAT IT IS. A chosen option, an accepted answer, the conversation you are in: the
3025
+ * one item among its peers that a decision landed on. Before this sheet the select
3026
+ * drew it one way (an accent tint plus an inset bar on the start edge), the field
3027
+ * choice another (a primary border), the conversation list a third (its own
3028
+ * background and weight), and the ask-user receipt none at all — four vocabularies
3029
+ * for one meaning, measured in the UI audit of 2026-08-28. The market agrees on two
3030
+ * signals: a tint on the ground (Radix, shadcn, Material's state layer) and a bar on
3031
+ * the row's start edge (GitHub Primer's ActionList). This recipe is both, on one
3032
+ * intent axis, and every row that means "this one" wears it.
3033
+ *
3034
+ * THE AXIS. `--aparte-mark-intent` is the fill, primary by default: chosen. The
3035
+ * modifiers set it to the theme's other masters — success for an answer that was
3036
+ * given or a call that was approved, danger for a destructive outcome, neutral for
3037
+ * the item you are merely in. `--quiet` is the outcome that did NOT happen: a declined
3038
+ * request, a version superseded — no tint, no bar, the muted voice. The market never
3039
+ * strikes through; neither does this.
3040
+ *
3041
+ * THE BAR IS DRAWN IN INK. The tint is the fill at 18%, which carries almost no
3042
+ * contrast by itself (1.2:1 in the light theme); the bar is the whole non-text signal,
3043
+ * so it reads `--aparte-mark-intent-ink` — the master's ink, the one the button and
3044
+ * the badge already derive for text on a ground. The raw success fill measured 2.27:1
3045
+ * on the light surface, under the 3:1 WCAG asks of a graphic; its ink is 7.1:1.
3046
+ *
3047
+ * THE BAR IS A PSEUDO-ELEMENT, NOT A SHADOW. An inset shadow knows only left and
3048
+ * right, so a right-to-left row got its bar on the wrong edge unless a document-level
3049
+ * `[dir="rtl"]` rule caught it — which a `dir` on the row itself, or `dir="auto"`,
3050
+ * escapes. `inset-inline-start` is the start edge whatever the direction, at any level.
3051
+ * Nothing shifts: the pseudo-element is out of the flow.
3052
+ *
3053
+ * THE MEASURES. `--aparte-mark-tint` and `--aparte-mark-bar` live in theme.css so a
3054
+ * theme moves every mark at once.
3055
+ */
3056
+ .aparte-mark {
3057
+ --aparte-mark-intent: var(--aparte-primary);
3058
+ --aparte-mark-intent-ink: var(--aparte-primary-ink);
3059
+ position: relative;
3060
+ background: color-mix(in srgb, var(--aparte-mark-intent) var(--aparte-mark-tint), transparent);
3061
+ }
3062
+
3063
+ .aparte-mark::before {
3064
+ content: "";
3065
+ position: absolute;
3066
+ inset-block: 0;
3067
+ inset-inline-start: 0;
3068
+ width: var(--aparte-mark-bar);
3069
+ background: var(--aparte-mark-intent-ink);
3070
+ pointer-events: none;
3071
+ }
3072
+
3073
+ .aparte-mark--success {
3074
+ --aparte-mark-intent: var(--aparte-success);
3075
+ --aparte-mark-intent-ink: var(--aparte-success-ink);
3076
+ }
3077
+
3078
+ .aparte-mark--danger {
3079
+ --aparte-mark-intent: var(--aparte-error);
3080
+ --aparte-mark-intent-ink: var(--aparte-error-ink);
3081
+ }
3082
+
3083
+ /* The neutral master has no derived ink; the fill is its own ink (5:1 light, 6.9:1 dark). */
3084
+ .aparte-mark--neutral {
3085
+ --aparte-mark-intent: var(--aparte-neutral);
3086
+ --aparte-mark-intent-ink: var(--aparte-neutral);
3087
+ }
3088
+
3089
+ /* The outcome that did not happen. */
3090
+ .aparte-mark--quiet {
3091
+ background: none;
3092
+ color: var(--aparte-text-muted);
3093
+ }
3094
+
3095
+ .aparte-mark--quiet::before {
3096
+ display: none;
3097
+ }
2917
3098
  /*
2918
3099
  * aparté — layered surfaces. Ready-made classes for the things that float ABOVE the
2919
3100
  * transcript: tabs, an accordion, a dropdown menu, a popover, a tooltip.
@@ -3566,14 +3747,47 @@ aparte-select[disabled] .aparte-select-trigger {
3566
3747
  cursor: not-allowed;
3567
3748
  }
3568
3749
 
3750
+ /* The label is a grid with ONE cell: the visible text and, under it, a hidden stack of
3751
+ every option's label. The stack sets the cell's width — the widest option's — so the
3752
+ control keeps one width whatever is selected, like a native <select>; the text layer
3753
+ ellipsises when a host constrains the control below that (`min-width: 0` is what lets
3754
+ a flex item shrink at all). */
3569
3755
  .aparte-select-label {
3570
3756
  flex: 1;
3757
+ min-width: 0;
3758
+ display: grid;
3571
3759
  overflow: hidden;
3572
- text-overflow: ellipsis;
3573
- white-space: nowrap;
3574
3760
  color: var(--aparte-select-text, var(--aparte-text));
3575
3761
  }
3576
3762
 
3763
+ .aparte-select-label > * {
3764
+ grid-area: 1 / 1;
3765
+ white-space: nowrap;
3766
+ }
3767
+
3768
+ .aparte-select-label-text {
3769
+ overflow: hidden;
3770
+ text-overflow: ellipsis;
3771
+ }
3772
+
3773
+ /* `min-width: 0; overflow: hidden` on the stack is what keeps the ellipsis alive: a
3774
+ grid item's automatic minimum size is its min-content — the widest nowrap label —
3775
+ and that floored the label's track, so a host that constrained the control got a
3776
+ hard clip mid-glyph instead of "…" (measured in Chromium: clientWidth 244 for a
3777
+ 140px control). Its max-content contribution, the one that sets the width, is
3778
+ untouched. */
3779
+ .aparte-select-label-sizer {
3780
+ display: grid;
3781
+ min-width: 0;
3782
+ overflow: hidden;
3783
+ visibility: hidden;
3784
+ pointer-events: none;
3785
+ }
3786
+
3787
+ .aparte-select-label-sizer > span {
3788
+ grid-area: 1 / 1;
3789
+ }
3790
+
3577
3791
  .aparte-select-chevron {
3578
3792
  --aparte-icon-size: var(--aparte-icon-size-sm);
3579
3793
  flex-shrink: 0;
@@ -3647,15 +3861,30 @@ aparte-option[data-active] {
3647
3861
  box-shadow: inset 0 0 0 2px var(--aparte-primary);
3648
3862
  }
3649
3863
 
3650
- /* Selected option: an accent TINT plus an accent bar, keeping the theme's own
3651
- text colour. A solid `--aparte-primary` fill with white text failed WCAG AA in
3652
- both themes (the brass accent is mid-luminance, so white sits around 3.4:1),
3653
- and an unreadable selected row is the one row that must be readable. Both
3654
- tokens stay overridable for a design that wants the solid fill back. */
3864
+ /* Selected option: the mark (display/mark.css) — an accent TINT plus an accent bar
3865
+ on the start edge, keeping the theme's own text colour. A solid `--aparte-primary`
3866
+ fill with white text failed WCAG AA in both themes (the brass accent is
3867
+ mid-luminance, so white sits around 3.4:1), and an unreadable selected row is the
3868
+ one row that must be readable. The measures are the mark's tokens, so this option,
3869
+ a checked field choice and the active conversation move together; the option's own
3870
+ two tokens stay overridable for a design that wants the solid fill back. Not the
3871
+ class: an option's selected state is an attribute the select writes, and a second
3872
+ name for it on the same element would be one more thing to keep in step. */
3655
3873
  aparte-option[selected] {
3656
- background: var(--aparte-select-option-selected, color-mix(in srgb, var(--aparte-primary) 18%, transparent));
3874
+ position: relative;
3875
+ background: var(--aparte-select-option-selected, color-mix(in srgb, var(--aparte-primary) var(--aparte-mark-tint), transparent));
3657
3876
  color: var(--aparte-select-option-selected-text, var(--aparte-select-text, var(--aparte-text)));
3658
- box-shadow: inset 2px 0 0 var(--aparte-primary);
3877
+ }
3878
+
3879
+ /* The bar: the mark's pseudo-element on the logical start edge (see display/mark.css). */
3880
+ aparte-option[selected]::before {
3881
+ content: "";
3882
+ position: absolute;
3883
+ inset-block: 0;
3884
+ inset-inline-start: 0;
3885
+ width: var(--aparte-mark-bar);
3886
+ background: var(--aparte-primary-ink);
3887
+ pointer-events: none;
3659
3888
  }
3660
3889
 
3661
3890
  aparte-option[disabled] {
@@ -3720,16 +3949,12 @@ aparte-optgroup[collapsed] .aparte-optgroup-chevron {
3720
3949
  transform: rotate(-90deg);
3721
3950
  }
3722
3951
 
3723
- /* Dark mode */
3724
- [data-aparte-theme="dark"] aparte-select .aparte-select-trigger,
3725
- [data-aparte-theme="dark"] aparte-select .aparte-select-dropdown {
3726
- background: var(--aparte-select-bg, var(--aparte-surface-1));
3727
- border-color: var(--aparte-select-border, var(--aparte-border));
3728
- }
3729
-
3730
- [data-aparte-theme="dark"] aparte-option:hover {
3731
- background: var(--aparte-select-option-hover, var(--aparte-surface-2));
3732
- }
3952
+ /* No dark block: every colour above reads a token the derived layer re-anchors on
3953
+ `[data-aparte-theme]`, so `--aparte-surface-1` IS the dark surface there. The block
3954
+ that used to sit here restated the trigger's and the panel's backgrounds from
3955
+ `--aparte-select-bg` — the TRIGGER's token — so a consumer making the trigger
3956
+ transparent (a pill on a coloured page) got a see-through panel in the dark, with
3957
+ the page's text showing through the options. */
3733
3958
 
3734
3959
  .aparte-optgroup-loader {
3735
3960
  padding: var(--aparte-space-4) var(--aparte-space-6);
@@ -3896,7 +4121,7 @@ aparte-chat-viewport {
3896
4121
  overflow-x: hidden;
3897
4122
  overscroll-behavior: contain;
3898
4123
  scrollbar-width: thin;
3899
- scrollbar-color: var(--aparte-neutral) transparent;
4124
+ scrollbar-color: var(--aparte-scrollbar-thumb) var(--aparte-scrollbar-track);
3900
4125
  }
3901
4126
 
3902
4127
  .aparte-viewport-container::-webkit-scrollbar {
@@ -3904,7 +4129,14 @@ aparte-chat-viewport {
3904
4129
  }
3905
4130
 
3906
4131
  .aparte-viewport-container::-webkit-scrollbar-track {
3907
- background: transparent;
4132
+ background: var(--aparte-scrollbar-track);
4133
+ }
4134
+
4135
+ /* The thumb too: the token pair was half-wired on the legacy WebKit path — the
4136
+ standard `scrollbar-color` read both, this block read only the track. */
4137
+ .aparte-viewport-container::-webkit-scrollbar-thumb,
4138
+ aparte-chat-viewport.aparte-viewport--framework::-webkit-scrollbar-thumb {
4139
+ background: var(--aparte-scrollbar-thumb);
3908
4140
  }
3909
4141
 
3910
4142
  /* ─── Scroll-to-bottom button ─────────────────────────────────────────────── */
@@ -3963,7 +4195,7 @@ aparte-chat-viewport[framework-managed] {
3963
4195
  padding: var(--aparte-viewport-padding);
3964
4196
  padding-bottom: calc(var(--aparte-viewport-padding) + var(--aparte-fw-spacer, 0px));
3965
4197
  scrollbar-width: thin;
3966
- scrollbar-color: var(--aparte-neutral) transparent;
4198
+ scrollbar-color: var(--aparte-scrollbar-thumb) var(--aparte-scrollbar-track);
3967
4199
  }
3968
4200
 
3969
4201
  /* The framework wrapper renders bubbles as DIRECT flex children of the viewport
@@ -3981,7 +4213,7 @@ aparte-chat-viewport.aparte-viewport--framework::-webkit-scrollbar {
3981
4213
  }
3982
4214
 
3983
4215
  aparte-chat-viewport.aparte-viewport--framework::-webkit-scrollbar-track {
3984
- background: transparent;
4216
+ background: var(--aparte-scrollbar-track);
3985
4217
  }
3986
4218
 
3987
4219
  /* The scroll button lives INSIDE the scrolling host here (last child), so it
@@ -4127,6 +4359,8 @@ aparte-chat-bubble {
4127
4359
  max-width: var(--aparte-message-max-width);
4128
4360
  margin: 0 auto;
4129
4361
  min-width: 0;
4362
+ /* The containing block of a floating footer — see the action bar below. */
4363
+ position: relative;
4130
4364
  }
4131
4365
 
4132
4366
  /* The avatar's shape, size and type live in `display.css` — a message avatar IS an
@@ -4222,6 +4456,58 @@ aparte-chat-bubble:last-of-type .aparte-message[data-role="assistant"]:not(.apar
4222
4456
  display: none;
4223
4457
  }
4224
4458
 
4459
+ /* An older message reserves no row for its actions.
4460
+ *
4461
+ * Measured (UI audit 2026-08-28, §8.3 #7): 103px between the text of one turn and the
4462
+ * text of the next, and 34 of them were this footer — 28px of buttons nobody could
4463
+ * see plus its margin, under EVERY message, so that the bar could appear on hover
4464
+ * without moving anything. The market floats it instead. It cannot float below the
4465
+ * message here: the bubble is a paint-containment boundary (`content-visibility`),
4466
+ * so anything outside its box is clipped. It floats INSIDE the box, over the header
4467
+ * row's empty end — the one place it overlaps no text — as the small bordered
4468
+ * toolbar a Slack message shows on hover.
4469
+ *
4470
+ * Three footers stay in the flow. The last assistant message: its bar is always on
4471
+ * show, under the reply, where every chat puts it. A message whose branch picker is
4472
+ * visible (`data-branches`, written by the bubble beside the picker's own state): the
4473
+ * picker must stay in view. And every message under a pointer that cannot hover, or
4474
+ * that is coarse — `(hover: hover) and (pointer: fine)`, because the two features are
4475
+ * independent and a TV remote reports both hover and coarse; responsive.css gives a
4476
+ * coarse pointer a 44px bar in the flow, and floated over the header row a 44px bar
4477
+ * would sit on the first line of the reply.
4478
+ *
4479
+ * The bar's place depends on the header row: 8px from the top, it fills the empty end
4480
+ * of the "Assistant · 12:34" line and clears the text by the header's height. A host
4481
+ * that hides `.aparte-header` moves that line away and should move the bar too
4482
+ * (`.aparte-footer { top: … }`), or keep it in the flow.
4483
+ *
4484
+ * Invisible but NOT inert, on purpose. `opacity: 0` keeps the bar hit-testable, so a
4485
+ * drag-select started on the empty end of the header row begins on a button nobody
4486
+ * sees — a review flagged it, and `pointer-events: none` until `:hover` was tried. It
4487
+ * made the bar unreachable to anything that clicks without hovering first: Playwright
4488
+ * (which hit-tests the point before it moves the mouse — the header "intercepted" the
4489
+ * edit button on four projects), and with it voice control and switch access, which
4490
+ * synthesise a click the same way. A control only a hover can enable is a control
4491
+ * some users cannot reach; a phantom button on an empty header end is not. */
4492
+ @media (hover: hover) and (pointer: fine) {
4493
+ aparte-chat-bubble:not(:last-of-type) .aparte-message:not([data-branches]) .aparte-footer,
4494
+ aparte-chat-bubble:last-of-type .aparte-message[data-role="user"]:not([data-branches]) .aparte-footer {
4495
+ position: absolute;
4496
+ top: var(--aparte-space-4);
4497
+ inset-inline-end: var(--aparte-space-6);
4498
+ margin: 0;
4499
+ z-index: 1;
4500
+ }
4501
+
4502
+ aparte-chat-bubble:not(:last-of-type) .aparte-message:not([data-branches]) .aparte-action-bar,
4503
+ aparte-chat-bubble:last-of-type .aparte-message[data-role="user"]:not([data-branches]) .aparte-action-bar {
4504
+ background: var(--aparte-surface-1);
4505
+ border: var(--aparte-border-width) solid var(--aparte-border);
4506
+ border-radius: var(--aparte-radius-md);
4507
+ box-shadow: var(--aparte-popover-shadow);
4508
+ }
4509
+ }
4510
+
4225
4511
  .aparte-action-btn {
4226
4512
  color: var(--aparte-action-bar-btn-color);
4227
4513
  --aparte-btn-radius: var(--aparte-radius-action-btn);
@@ -4336,6 +4622,15 @@ aparte-chat-bubble:last-of-type .aparte-message[data-role="assistant"]:not(.apar
4336
4622
  .aparte-message[data-error] .aparte-avatar {
4337
4623
  box-shadow: var(--aparte-avatar-error-ring);
4338
4624
  }
4625
+
4626
+ /* An empty, settled turn renders no chrome. The bubble sets `data-empty` when it has
4627
+ no content, no segments, no attachments and is not streaming — a tool-only turn, or
4628
+ one stopped before its first token — and the whole row goes, name and timestamp
4629
+ included, instead of an orphan header floating over nothing. The element stays in
4630
+ the DOM (streaming and the action bar address it by id), it just does not paint. */
4631
+ .aparte-message[data-empty] {
4632
+ display: none;
4633
+ }
4339
4634
  /*
4340
4635
  * aparté — The composer and everything it holds: editor, send, attachments, model picker.
4341
4636
  *
@@ -4404,92 +4699,6 @@ aparte-chat-viewport.aparte-viewport--framework::-webkit-scrollbar-thumb {
4404
4699
  Attachments
4405
4700
  ========================================= */
4406
4701
 
4407
- .aparte-attachments {
4408
- display: flex;
4409
- flex-wrap: wrap;
4410
- /* The strip sits directly above the message bubble and belongs to it, so it
4411
- must share the bubble's edge. The user bubble hugs its text on the LEADING
4412
- edge (AI-chat convention, see .aparte-message-content below), so anchoring
4413
- the strip to the trailing edge would split one message across both sides. */
4414
- justify-content: flex-start;
4415
- gap: var(--aparte-space-3);
4416
- margin-bottom: var(--aparte-space-4);
4417
- /* Really small tiles inside the conversation. */
4418
- --aparte-attachment-image-size: 40px;
4419
- /* Many attachments → cap the height and scroll instead of growing forever. */
4420
- max-height: var(--aparte-attachments-max-height);
4421
- overflow-y: auto;
4422
- }
4423
-
4424
- /* The class `display: flex` above would otherwise override the `hidden`
4425
- attribute — restore it so an empty attachment strip takes no space. */
4426
- .aparte-attachments[hidden] {
4427
- display: none;
4428
- }
4429
-
4430
- /* ── Attachment thumbnail tile ──────────────────────────────────────────────
4431
- Shared by the composer attachment preview and the sent-message attachment
4432
- strip. A square that shows the image itself (or a file glyph); the filename
4433
- and — in the composer — a remove button surface on hover. */
4434
- /* The tile is `.aparte-thumbnail`; the composer only sets its own measurements and
4435
- the quieter ground an attachment strip uses. */
4436
- .aparte-thumb {
4437
- --aparte-thumbnail-size: var(--aparte-attachment-image-size);
4438
- --aparte-thumbnail-radius: var(--aparte-thumb-radius);
4439
- --aparte-thumbnail-bg: var(--aparte-attachment-chip-bg);
4440
- --aparte-thumbnail-border: var(--aparte-attachment-chip-border);
4441
- }
4442
-
4443
- /* Only an image tile the app declared it can preview is a button — see
4444
- aparteGlobalConfig.setHostHandlers(). Undeclared, it is a picture, not a control. */
4445
- .aparte-thumb--image[role='button'] {
4446
- cursor: pointer;
4447
- }
4448
-
4449
- .aparte-thumb--image[role='button']:focus-visible {
4450
- box-shadow: var(--aparte-focus-ring);
4451
- }
4452
-
4453
- .aparte-thumb__img {
4454
- width: 100%;
4455
- height: 100%;
4456
- object-fit: cover;
4457
- display: block;
4458
- }
4459
-
4460
- .aparte-thumb--file {
4461
- display: flex;
4462
- align-items: center;
4463
- justify-content: center;
4464
- }
4465
-
4466
- .aparte-thumb__ext {
4467
- font-size: var(--aparte-font-size-xs);
4468
- font-weight: var(--aparte-font-weight-bold);
4469
- letter-spacing: 0.03em;
4470
- color: var(--aparte-text-muted);
4471
- }
4472
-
4473
- .aparte-thumb__name {
4474
- position: absolute;
4475
- left: 0;
4476
- right: 0;
4477
- bottom: 0;
4478
- padding: var(--aparte-thumb-name-padding);
4479
- font-size: var(--aparte-font-size-2xs);
4480
- line-height: var(--aparte-line-height-tight);
4481
- color: var(--aparte-thumb-name-color);
4482
- background: var(--aparte-thumb-name-scrim);
4483
- white-space: nowrap;
4484
- overflow: hidden;
4485
- text-overflow: ellipsis;
4486
- opacity: 0;
4487
- transition: opacity var(--aparte-duration-base) var(--aparte-ease);
4488
- pointer-events: none;
4489
- }
4490
- .aparte-thumb:hover .aparte-thumb__name {
4491
- opacity: 1;
4492
- }
4493
4702
 
4494
4703
  .aparte-thumb__remove {
4495
4704
  /* Icon size comes from `--sm` — see `--aparte-btn-icon-size` in button.css. */
@@ -4962,7 +5171,7 @@ aparte-composer-cancel .aparte-cc-button:hover {
4962
5171
  flex: 0 0 auto;
4963
5172
  }
4964
5173
  /*
4965
- * aparté — Segment renderers: thinking, code, tool call, error, pipeline.
5174
+ * aparté — Segment renderers: thinking, code, tool call, error.
4966
5175
  *
4967
5176
  * One of the sheets split out of aparte.css. Order matters: src/index.ts imports
4968
5177
  * them in the order listed there, and check:derived-vars reads them concatenated in
@@ -5176,8 +5385,13 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
5176
5385
  0,2,0 beats the base rule's 0,1,0 and source order puts rejected/aborted last. Core
5177
5386
  itself documents data-status as a free-form host attribute on <aparte-option>. */
5178
5387
  .aparte-segment-tool-call[data-status="resolved"] .aparte-tool-state { color: var(--aparte-success-ink); }
5179
- .aparte-segment-tool-call[data-status="rejected"] .aparte-tool-state,
5180
- .aparte-segment-tool-call[data-status="aborted"] .aparte-tool-state { color: var(--aparte-error-ink); }
5388
+ /* Rejected and aborted have NO rule: they keep the base's muted ink. They used to
5389
+ share one red, the error ink — which said "something went wrong" about a call the
5390
+ user declined or a run they stopped, and made the two indistinguishable. The market
5391
+ (UI audit 2026-08-28, coherence pass) keeps red for what is destructive and shows an
5392
+ outcome that did not happen in the muted voice with a glyph: the renderer draws a
5393
+ cross for rejected and a stop square for aborted, so the word and the glyph tell them
5394
+ apart, not a colour that meant a third thing. */
5181
5395
  /* The Approve / Reject button rules were here. They moved with the buttons, to
5182
5396
  .aparte-approval-option in components/elicitation.css - and what stayed behind was a
5183
5397
  leftover: a rule tinting .aparte-tool-label's border while a decision was pending,
@@ -5235,7 +5449,7 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
5235
5449
  }
5236
5450
 
5237
5451
  /* ─────────────────────────────────────────────────────────────────────────────
5238
- Segment renderers — tool call · artifact card · pipeline waiting
5452
+ Segment renderers — tool call · artifact card
5239
5453
 
5240
5454
  These three shipped their CSS from `getStyles()` until now: a template literal
5241
5455
  in the renderer's own .ts file, injected into <head> at runtime. That seam is
@@ -5265,27 +5479,6 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
5265
5479
  ───────────────────────────────────────────────────────────────────────────── */
5266
5480
 
5267
5481
  /* Tool call */
5268
- .aparte-segment-pipeline-waiting {
5269
- display: flex;
5270
- align-items: center;
5271
- gap: var(--aparte-space-3);
5272
- padding: var(--aparte-space-3) var(--aparte-space-1);
5273
- min-height: var(--aparte-pw-min-height);
5274
- }
5275
- /* The geometry is `.aparte-dot`. What stays is this row's own: its size, its resting
5276
- colour, and a STAGGERED pulse — three dots on delays read as a wave travelling, which
5277
- a symmetric breath does not. */
5278
- .aparte-pw-dot {
5279
- --aparte-status-dot-size: var(--aparte-pw-dot-size);
5280
- --aparte-status-color: var(--aparte-text-muted);
5281
- opacity: 0.3;
5282
- }
5283
- .aparte-pw-dot:nth-child(2) { animation-delay: var(--aparte-duration-slow); }
5284
- .aparte-pw-dot:nth-child(3) { animation-delay: 0.4s; }
5285
-
5286
- /* =========================================
5287
- Provider-HTML overflow guards
5288
- ========================================= */
5289
5482
  /* Markdown providers inject arbitrary HTML into .aparte-segment-text, and the bubble
5290
5483
  clips overflow (`aparte-chat-bubble { overflow: hidden }`) — without these
5291
5484
  guards a wide image or table is silently cut at the bubble edge. */
@@ -5661,7 +5854,12 @@ details[open] > .aparte-tool-summary .aparte-tool-toggle { transform: rotate(180
5661
5854
  --aparte-field-choice-radius: var(--aparte-elic-option-radius);
5662
5855
  }
5663
5856
 
5664
- .aparte-elic-option:not(.aparte-elic-option--command):hover {
5857
+ /* Blind to the chosen row: this rule is (0,3,0) in a sheet imported after field.css,
5858
+ so it used to out-specify the mark a checked choice wears there — hover the option
5859
+ you picked and its tint and primary border went back to the plain hover ground
5860
+ (measured in Chromium by the coherence pass's reviewer). The recipe's own hover is
5861
+ scoped the same way; a chosen row keeps saying it is chosen under the pointer. */
5862
+ .aparte-elic-option:not(.aparte-elic-option--command):not(:has(:checked)):not(.aparte-field-choice--selected):hover {
5665
5863
  background: var(--aparte-surface-2);
5666
5864
  border-color: var(--aparte-border);
5667
5865
  }
@@ -5724,6 +5922,14 @@ textarea.aparte-elic-text {
5724
5922
  border-color: var(--aparte-primary);
5725
5923
  background: color-mix(in srgb, var(--aparte-primary) 6%, transparent);
5726
5924
  }
5925
+ /* One ring at a time. The recommended option is the first to receive focus, and its
5926
+ tinted border under the recipe's focus ring read as a double outline (UI audit
5927
+ 2026-08-28, §8.3 #9). While it holds focus the ring is the indicator; the tint
5928
+ comes back when focus leaves. */
5929
+ .aparte-elic-option--recommended:focus-within {
5930
+ background: transparent;
5931
+ border-color: var(--aparte-border-focus);
5932
+ }
5727
5933
 
5728
5934
  /* The whole-request escape, in the corner and out of the way of anything that
5729
5935
  answers a question. It used to be a row at the bottom beside the button that
@@ -5935,12 +6141,27 @@ textarea.aparte-elic-text {
5935
6141
  outline-offset: var(--aparte-focus-outline-offset);
5936
6142
  }
5937
6143
 
6144
+ /* The conversation you are in wears the mark's bar (display/mark.css) on its start
6145
+ edge — Primer's ActionList pattern, and the same bar the select's chosen option and
6146
+ a checked field choice carry. Its ground keeps the list's own tokens: a navigation
6147
+ list is read for a long time, and the mark's tint on every visit was too loud. */
5938
6148
  .aparte-conv-item--active {
6149
+ position: relative;
5939
6150
  background: var(--aparte-conv-item-bg-active);
5940
6151
  color: var(--aparte-conv-item-color-active);
5941
6152
  font-weight: var(--aparte-conv-item-font-weight-active);
5942
6153
  }
5943
6154
 
6155
+ .aparte-conv-item--active::before {
6156
+ content: "";
6157
+ position: absolute;
6158
+ inset-block: 0;
6159
+ inset-inline-start: 0;
6160
+ width: var(--aparte-mark-bar);
6161
+ background: var(--aparte-primary-ink);
6162
+ pointer-events: none;
6163
+ }
6164
+
5944
6165
  .aparte-conv-item--active .aparte-conv-item__delete {
5945
6166
  opacity: 0;
5946
6167
  }
@@ -5997,6 +6218,77 @@ textarea.aparte-elic-text {
5997
6218
  font-style: italic;
5998
6219
  opacity: 0.85;
5999
6220
  }
6221
+ /*
6222
+ * aparté — <aparte-suggestions>: the row of prompt starters.
6223
+ *
6224
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6225
+ * them concatenated in that same order, the way a browser does.
6226
+ *
6227
+ * The chips are `aparte-btn aparte-btn--surface aparte-btn--pill` — the recipe draws
6228
+ * them, this sheet only lays them out. No token of its own: the gap is the global
6229
+ * spacing scale, so a theme reaches it without a knob to re-set.
6230
+ */
6231
+ aparte-suggestions {
6232
+ display: block;
6233
+ }
6234
+ aparte-suggestions[hidden],
6235
+ aparte-suggestions[data-empty] {
6236
+ display: none;
6237
+ }
6238
+
6239
+ .aparte-suggestions {
6240
+ display: flex;
6241
+ flex-wrap: wrap;
6242
+ gap: var(--aparte-space-2);
6243
+ padding-block: var(--aparte-space-2);
6244
+ }
6245
+
6246
+ /* A starter's label is a short phrase; it must not become a two-line chip. */
6247
+ .aparte-suggestion {
6248
+ white-space: nowrap;
6249
+ }
6250
+ /*
6251
+ * aparté — <aparte-context>: how full the model's context window is.
6252
+ *
6253
+ * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
6254
+ * them concatenated in that same order, the way a browser does.
6255
+ *
6256
+ * The bar is the `aparte-progress` recipe; this sheet lays it out beside its reading
6257
+ * and recolours the fill at the two levels — from the global alert tokens, no token
6258
+ * of its own.
6259
+ */
6260
+ aparte-context {
6261
+ display: block;
6262
+ }
6263
+ aparte-context[hidden],
6264
+ aparte-context[data-empty] {
6265
+ display: none;
6266
+ }
6267
+
6268
+ .aparte-context {
6269
+ display: flex;
6270
+ align-items: center;
6271
+ gap: var(--aparte-space-2);
6272
+ color: var(--aparte-text-muted);
6273
+ font-size: var(--aparte-font-size-sm);
6274
+ }
6275
+
6276
+ .aparte-context .aparte-progress {
6277
+ flex: 1;
6278
+ min-width: 4rem;
6279
+ }
6280
+
6281
+ .aparte-context__text {
6282
+ white-space: nowrap;
6283
+ font-variant-numeric: tabular-nums;
6284
+ }
6285
+
6286
+ aparte-context[data-level="warn"] .aparte-progress__bar {
6287
+ background: var(--aparte-warning);
6288
+ }
6289
+ aparte-context[data-level="danger"] .aparte-progress__bar {
6290
+ background: var(--aparte-error);
6291
+ }
6000
6292
  /*
6001
6293
  * aparté — Reset-agnostic typography for model-authored markdown.
6002
6294
  *
@@ -6118,6 +6410,42 @@ textarea.aparte-elic-text {
6118
6410
  margin: var(--aparte-prose-hr-margin);
6119
6411
  }
6120
6412
 
6413
+ /* Tables. The sanitizer allowlisted table/thead/tbody/tr/th/td from the start and no
6414
+ sheet ever drew them, so a GFM table — the second most common structure in an
6415
+ assistant reply, and a suggestion chip in our own example — rendered as words with
6416
+ no borders, no padding and columns that touched. `display: block` + `overflow-x` is
6417
+ the one honest way to keep a wide table inside a narrow bubble without a wrapper
6418
+ the markdown provider cannot emit. */
6419
+ .aparte-content table,
6420
+ .aparte-segment-content table,
6421
+ .aparte-thinking-content table {
6422
+ display: block;
6423
+ max-width: 100%;
6424
+ overflow-x: auto;
6425
+ border-collapse: collapse;
6426
+ margin: var(--aparte-space-4) 0;
6427
+ font-size: var(--aparte-font-size-md);
6428
+ }
6429
+
6430
+ .aparte-content th,
6431
+ .aparte-segment-content th,
6432
+ .aparte-thinking-content th,
6433
+ .aparte-content td,
6434
+ .aparte-segment-content td,
6435
+ .aparte-thinking-content td {
6436
+ border: var(--aparte-border-width) solid var(--aparte-border);
6437
+ padding: var(--aparte-space-2) var(--aparte-space-4);
6438
+ text-align: start;
6439
+ vertical-align: top;
6440
+ }
6441
+
6442
+ .aparte-content th,
6443
+ .aparte-segment-content th,
6444
+ .aparte-thinking-content th {
6445
+ font-weight: var(--aparte-font-weight-semibold);
6446
+ background: var(--aparte-surface-2);
6447
+ }
6448
+
6121
6449
  .aparte-thinking-content {
6122
6450
  padding: var(--aparte-space-2) 0 var(--aparte-space-1) var(--aparte-space-5);
6123
6451
  font-size: var(--aparte-font-size-md);
@@ -6204,7 +6532,7 @@ textarea.aparte-elic-text {
6204
6532
 
6205
6533
  /* Respect prefers-reduced-motion. Scoped to Aparte's own custom elements (never
6206
6534
  the consumer's page) — this covers every animated element the library renders
6207
- inside them: typing dots, spinners, pipeline/artifact pulses, segment
6535
+ inside them: typing dots, spinners, artifact pulses, segment
6208
6536
  animations. Activity stays conveyed via aria-live / aria-busy. */
6209
6537
  @media (prefers-reduced-motion: reduce) {
6210
6538
  aparte-chat-viewport *,
@@ -6265,6 +6593,20 @@ textarea.aparte-elic-text {
6265
6593
  --aparte-input-action-btn-size: var(--aparte-touch-target-size);
6266
6594
  }
6267
6595
 
6596
+ /* The bubble's action bar too: 28px buttons under a thumb missed as often as they
6597
+ hit (UI audit 2026-08-28, §8.3 #10). The bar's height follows the same token.
6598
+ And it is SHOWN: the bar is revealed by hover, and a finger cannot hover, so on a
6599
+ touch device every message's actions were sitting at opacity 0 with no way to
6600
+ reach them — a larger target for something invisible. The footer stays in the
6601
+ flow here (the floating rule in bubble.css is under `(hover: hover) and
6602
+ (pointer: fine)`, so a device that hovers with a coarse pointer — a TV remote —
6603
+ keeps it in the flow too), so what a touch device gets is the bar every mobile
6604
+ chat shows: under each message, always. */
6605
+ .aparte-action-bar {
6606
+ --aparte-action-bar-btn-size: var(--aparte-touch-target-size);
6607
+ opacity: 1;
6608
+ }
6609
+
6268
6610
  /* Hover cannot reveal these on touch — keep them visible and tappable. */
6269
6611
  .aparte-conv-item__delete,
6270
6612
  .aparte-conv-item__archive {