anentrypoint-design 0.0.487 → 0.0.488

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.487",
3
+ "version": "0.0.488",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -46,7 +46,9 @@ export function SearchInput({ value = '', placeholder = 'search…', onInput, on
46
46
  // has no way to morph one element type into another in place - it produced
47
47
  // a corrupted merged DOM node carrying attributes from both shapes.
48
48
  return h('span', { key, class: 'ds-search-input-wrap' },
49
- input,
49
+ h('span', { key: 'fld', class: 'ds-search-field' },
50
+ h('span', { key: 'ic', class: 'ds-search-icon', 'aria-hidden': 'true' }, Icon('search', { size: 15 })),
51
+ input),
50
52
  clearBtn,
51
53
  resultCount != null ? h('span', { key: 'cnt', class: 'sr-only', role: 'status', 'aria-live': 'polite' }, resultCount) : null);
52
54
  }
@@ -17,7 +17,7 @@ export function Panel({ title, count, right, style = '', class: className = '',
17
17
  return h('div', { class: cls, style, ...(id ? { id } : {}) },
18
18
  title != null ? h('div', { class: 'panel-head' },
19
19
  h(headingTag, { class: 'panel-title' }, title),
20
- right != null ? right : (count != null ? h('span', {}, String(count)) : null)
20
+ right != null ? right : (count != null ? h('span', { class: 'ds-badge' }, String(count)) : null)
21
21
  ) : null,
22
22
  h('div', { class: 'panel-body' }, ...(Array.isArray(children) ? children : [children]))
23
23
  );
@@ -48,14 +48,16 @@ export function RootsPicker({ roots = [], selected, onSelect, label = 'roots' }
48
48
  }, r.label || r.id)));
49
49
  }
50
50
 
51
- export function DropZone({ children, dragover, onDrop, onDragOver, onDragLeave, label = 'drop files here', onPick } = {}) {
51
+ export function DropZone({ children, dragover, rejected, onDrop, onDragOver, onDragLeave, label = 'drop files here', onPick } = {}) {
52
52
  // With children the zone is a passive WRAPPER: content renders normally and
53
53
  // the dashed affordance appears only while a drag is over it (real file
54
54
  // managers never burn a permanent band on a maybe-drop). Without children
55
- // it keeps the explicit picker-block look.
55
+ // it keeps the explicit picker-block look. `rejected` lets the host flag a
56
+ // drag whose payload fails a type/size guard with a distinct treatment
57
+ // (.rejected) instead of the normal accept-toned .dragover.
56
58
  const kids = Array.isArray(children) ? children : children ? [children] : [];
57
59
  return h('div', {
58
- class: 'ds-dropzone' + (kids.length ? ' ds-dropzone--wrap' : '') + (dragover ? ' dragover' : ''),
60
+ class: 'ds-dropzone' + (kids.length ? ' ds-dropzone--wrap' : '') + (dragover ? ' dragover' : '') + (rejected ? ' rejected' : ''),
59
61
  ondragover: (e) => { e.preventDefault(); onDragOver && onDragOver(e); },
60
62
  ondragleave: (e) => { if (!e.currentTarget.contains(e.relatedTarget)) { onDragLeave && onDragLeave(e); } },
61
63
  ondrop: (e) => { e.preventDefault(); onDrop && onDrop(e.dataTransfer.files); }
@@ -45,6 +45,7 @@ export function useFloating(anchorEl, contentEl, { placement = 'bottom-start', o
45
45
  contentEl.style.left = x + 'px';
46
46
  contentEl.style.top = y + 'px';
47
47
  finalPlacement = s + '-' + align;
48
+ contentEl.setAttribute('data-placement', finalPlacement);
48
49
  };
49
50
  compute();
50
51
  const cb = () => compute();
@@ -88,12 +88,14 @@ export function WsResizer(col) {
88
88
  const onDown = (e) => {
89
89
  e.preventDefault();
90
90
  const handleEl = e.currentTarget;
91
+ handleEl.classList.add('ws-resizer-active');
91
92
  let lastX = e.clientX;
92
93
  const move = (ev) => { const dx = ev.clientX - lastX; lastX = ev.clientX; wsResize(col, dx, false, handleEl); };
93
94
  const up = () => {
94
95
  document.removeEventListener('pointermove', move);
95
96
  document.removeEventListener('pointerup', up);
96
97
  document.body.style.cursor = '';
98
+ handleEl.classList.remove('ws-resizer-active');
97
99
  wsResize(col, 0, true, handleEl); // commit the settled width once
98
100
  };
99
101
  document.addEventListener('pointermove', move);
@@ -2,6 +2,21 @@
2
2
  html, body { margin: 0; padding: 0; }
3
3
  button, input, select, textarea { font: inherit; }
4
4
 
5
+ /* Global reduced-motion safety net: catches any component transition/animation
6
+ that lacks its own per-file prefers-reduced-motion override (most components
7
+ already opt out locally; this is the backstop for the rest, e.g.
8
+ plugins-config.css, catalog-theme.css, sidebar-misc.css, row-print.css,
9
+ topbar.css, primitives.css, git-status.css, panel-row.css). Scroll-snap
10
+ carousels handle scroll-behavior themselves and are unaffected. */
11
+ @media (prefers-reduced-motion: reduce) {
12
+ *, *::before, *::after {
13
+ animation-duration: 0.001ms !important;
14
+ animation-iteration-count: 1 !important;
15
+ transition-duration: 0.001ms !important;
16
+ scroll-behavior: auto !important;
17
+ }
18
+ }
19
+
5
20
  .skip-link {
6
21
  --skip-link-preflight-translate-clear: -100%;
7
22
  position: absolute;
@@ -193,6 +208,13 @@ a.ds-link-accent, .ds-prose a {
193
208
  text-underline-offset: 4px;
194
209
  text-decoration-thickness: 2px;
195
210
  }
211
+ /* Visited state was entirely absent -- a returning user had no way to tell an
212
+ already-read link from an unread one, the one link state browsers don't
213
+ fake for you. --purple-2 keeps it distinct from both the unvisited accent
214
+ and any hover/danger tone already in the palette. */
215
+ a.ds-link-accent:visited, .ds-prose a:visited {
216
+ color: var(--purple-2);
217
+ }
196
218
  a.ds-link-accent:hover, .ds-prose a:hover {
197
219
  background: var(--accent); color: var(--accent-fg); text-decoration-color: transparent;
198
220
  }
@@ -59,6 +59,7 @@
59
59
  text-transform: lowercase; letter-spacing: 0.02em; user-select: none;
60
60
  transition: transform 0.12s ease, box-shadow 0.12s ease;
61
61
  position: relative;
62
+ box-shadow: inset 0 0 0 1px var(--rule);
62
63
  }
63
64
  .chat-msg:hover .chat-avatar { transform: scale(1.03); box-shadow: 0 1px 4px color-mix(in oklab, var(--fg) 6%, transparent); }
64
65
  .chat-msg.you .chat-avatar {
@@ -100,6 +101,13 @@
100
101
  min-width: 0;
101
102
  transition: transform 0.12s ease, box-shadow 0.12s ease;
102
103
  }
104
+ /* Tail corner: the bubble edge nearest its own avatar (bottom-left for
105
+ 'them', bottom-right for 'you') squares off slightly, matching bubble
106
+ position to the sender's avatar the way a speech-bubble tail would — a
107
+ consistent radius on all four corners reads as a generic card, not a
108
+ turn in a conversation. */
109
+ .chat-msg:not(.you) .chat-bubble { border-bottom-left-radius: var(--r-0); }
110
+ .chat-msg.you .chat-bubble { border-bottom-right-radius: var(--r-0); }
103
111
  .chat-msg:hover .chat-bubble { transform: translateY(-1px); box-shadow: 0 2px 8px color-mix(in oklab, var(--fg) 6%, transparent); transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease); }
104
112
  /* Own bubble — calm tinted lead, not a full electric fill (chat stays readable). */
105
113
  .chat-msg.you .chat-bubble { background: var(--accent-tint); color: var(--fg); }
@@ -538,3 +546,160 @@
538
546
  fell back to the paper tone at 3.13:1 on ink. */
539
547
  .aicat-meta .name { color: var(--mascot-deep, var(--mascot)); font-weight: 600; }
540
548
 
549
+ /* ============================================================
550
+ Composer attachment-detected badge, context line, hint, toolbar,
551
+ note strip, elapsed counter, dragover state — all referenced from
552
+ composer.js / composer-affordances.js but previously had no CSS at
553
+ all (unstyled DOM nodes / plain text falling through to block
554
+ defaults).
555
+ ============================================================ */
556
+ .chat-composer-context {
557
+ display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-1);
558
+ width: 100%; order: -1;
559
+ padding: var(--space-1) var(--space-1) 0;
560
+ font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-3);
561
+ letter-spacing: 0.01em;
562
+ }
563
+ button.chat-composer-context {
564
+ background: transparent; border: 0; cursor: pointer; text-align: left;
565
+ border-radius: var(--r-0);
566
+ }
567
+ button.chat-composer-context:hover { color: var(--fg-2); }
568
+ button.chat-composer-context:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
569
+ .chat-composer-context-sep { color: var(--fg-3); opacity: 0.6; }
570
+ .chat-composer-context-bit {
571
+ background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer;
572
+ font: inherit; color: var(--fg-3); border-radius: var(--r-0);
573
+ transition: color var(--dur-snap, .15s) var(--ease, ease);
574
+ }
575
+ .chat-composer-context-bit:hover { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 2px; }
576
+ .chat-composer-context-bit:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
577
+ .chat-composer-context-text { color: var(--fg-3); }
578
+
579
+ .chat-composer-detected-badge {
580
+ display: flex; align-items: center; gap: var(--space-2);
581
+ width: 100%; order: -1;
582
+ padding: var(--space-1) var(--space-2-5);
583
+ margin: 0 0 var(--space-1);
584
+ background: color-mix(in oklab, var(--accent) 12%, var(--bg-2));
585
+ border: 1px solid color-mix(in oklab, var(--accent) 28%, transparent);
586
+ border-radius: var(--r-1);
587
+ font-size: var(--fs-xs); color: var(--fg);
588
+ }
589
+ .chat-composer-detected-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
590
+ .chat-composer-detected-dismiss {
591
+ flex-shrink: 0; width: 20px; height: 20px;
592
+ display: inline-flex; align-items: center; justify-content: center;
593
+ background: transparent; border: 0; cursor: pointer; border-radius: 50%;
594
+ color: var(--fg-3); font-family: var(--ff-mono); font-size: var(--fs-tiny); line-height: 1;
595
+ transition: background var(--dur-snap, .15s) var(--ease, ease), color var(--dur-snap, .15s) var(--ease, ease);
596
+ }
597
+ .chat-composer-detected-dismiss:hover { background: color-mix(in oklab, var(--fg) 10%, transparent); color: var(--fg); }
598
+ .chat-composer-detected-dismiss:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
599
+
600
+ .chat-composer-hint {
601
+ width: 100%; order: 2;
602
+ font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-3);
603
+ padding: 0 var(--space-1);
604
+ }
605
+
606
+ .chat-composer-toolbar {
607
+ display: flex; align-items: center; gap: var(--space-1);
608
+ flex-shrink: 0;
609
+ }
610
+ .chat-composer-toolbar .composer-btn {
611
+ width: 32px; height: 32px; border-radius: var(--r-1);
612
+ }
613
+ @media (pointer: coarse) {
614
+ .chat-composer-toolbar .composer-btn { width: 44px; height: 44px; }
615
+ }
616
+
617
+ .chat-composer-note {
618
+ width: 100%; order: 3;
619
+ font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-3);
620
+ padding: var(--space-1) var(--space-1) 0;
621
+ }
622
+
623
+ .chat-composer-elapsed {
624
+ font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-3);
625
+ flex-shrink: 0; padding: 0 var(--space-1);
626
+ }
627
+
628
+ .chat-composer.dragover {
629
+ border-color: var(--accent);
630
+ box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 18%, transparent);
631
+ background: color-mix(in oklab, var(--accent) 4%, var(--bg));
632
+ }
633
+
634
+ /* ============================================================
635
+ EmojiPicker (ov-emoji-*) — an anchored popover with search input,
636
+ category tabs, and an emoji grid; previously entirely unstyled.
637
+ Search input mirrors .ds-search-input's tonal-fill treatment so it
638
+ reads consistently with the rest of the system's search fields.
639
+ ============================================================ */
640
+ .ov-emoji-root {
641
+ position: fixed;
642
+ display: flex; flex-direction: column; gap: var(--space-2);
643
+ width: 280px; max-height: 320px;
644
+ padding: var(--space-2-5);
645
+ background: var(--bg);
646
+ border: 1px solid color-mix(in oklab, var(--fg) 12%, transparent);
647
+ border-radius: var(--r-2);
648
+ box-shadow: var(--shadow-overlay, var(--shadow-3));
649
+ z-index: var(--z-dropdown, var(--z-raised));
650
+ }
651
+ .ov-emoji-search {
652
+ width: 100%; box-sizing: border-box;
653
+ font: inherit; font-size: var(--fs-sm); color: var(--fg);
654
+ background: var(--bg-2); border: 0;
655
+ box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule);
656
+ border-radius: var(--r-1);
657
+ height: var(--ctl-sm, 32px);
658
+ padding: 0 var(--space-2-5);
659
+ transition: box-shadow var(--dur-snap, .15s) var(--ease, ease);
660
+ }
661
+ .ov-emoji-search::placeholder { color: var(--fg-3); }
662
+ .ov-emoji-search:focus-visible,
663
+ .ov-emoji-search:focus {
664
+ outline: none;
665
+ box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--accent), 0 0 0 2px color-mix(in oklab, var(--accent) 18%, transparent);
666
+ }
667
+ .ov-emoji-tabs {
668
+ display: flex; gap: var(--space-1);
669
+ border-bottom: 1px solid color-mix(in oklab, var(--fg) 10%, transparent);
670
+ padding-bottom: var(--space-1);
671
+ }
672
+ .ov-emoji-tab {
673
+ flex: 1;
674
+ display: inline-flex; align-items: center; justify-content: center;
675
+ height: 30px; font-size: var(--fs-sm);
676
+ background: transparent; border: 0; border-radius: var(--r-1);
677
+ cursor: pointer;
678
+ transition: background var(--dur-snap, .15s) var(--ease, ease);
679
+ }
680
+ .ov-emoji-tab:hover { background: color-mix(in oklab, var(--fg) 8%, transparent); }
681
+ .ov-emoji-tab[aria-selected="true"] { background: color-mix(in oklab, var(--accent) 16%, transparent); }
682
+ .ov-emoji-tab:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
683
+ .ov-emoji-grid { overflow-y: auto; flex: 1; min-height: 0; scrollbar-width: thin; scrollbar-color: var(--bg-3) transparent; }
684
+ .ov-emoji-grid-inner {
685
+ display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--space-1);
686
+ }
687
+ .ov-emoji-cell {
688
+ display: inline-flex; align-items: center; justify-content: center;
689
+ aspect-ratio: 1; font-size: var(--fs-lg); line-height: 1;
690
+ background: transparent; border: 0; border-radius: var(--r-1);
691
+ cursor: pointer;
692
+ transition: background var(--dur-snap, .15s) var(--ease, ease), transform 0.12s ease;
693
+ }
694
+ .ov-emoji-cell:hover { background: color-mix(in oklab, var(--fg) 10%, transparent); transform: scale(1.12); }
695
+ .ov-emoji-cell:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; background: color-mix(in oklab, var(--fg) 10%, transparent); }
696
+ .ov-emoji-empty {
697
+ grid-column: 1 / -1;
698
+ text-align: center; color: var(--fg-3); font-size: var(--fs-xs);
699
+ padding: var(--space-3) 0;
700
+ }
701
+ @media (prefers-reduced-motion: reduce) {
702
+ .ov-emoji-cell { transition: background var(--dur-snap, .15s) var(--ease, ease); }
703
+ .ov-emoji-cell:hover { transform: none; }
704
+ }
705
+
@@ -449,6 +449,13 @@
449
449
  border-radius: var(--r-3); background: var(--bg);
450
450
  transition: border-color var(--dur-base) var(--ease), background var(--dur-base) var(--ease);
451
451
  }
452
+ /* Idle hover: a quiet pre-drag affordance (border firms up, faint tint) so a
453
+ pointer resting over the zone reads as "droppable" before a drag even
454
+ starts — distinct from and weaker than the .dragover treatment below. */
455
+ .ds-dropzone:hover {
456
+ border-color: var(--fg-3);
457
+ background: var(--bg-2);
458
+ }
452
459
  .ds-dropzone-inner {
453
460
  display: flex; align-items: center; justify-content: center;
454
461
  gap: var(--space-2); flex-wrap: wrap;
@@ -457,6 +464,9 @@
457
464
  /* Icon size, not type — see .ds-file-cell-media .ds-file-icon above. */
458
465
  .ds-dropzone-glyph { font-size: var(--icon-lg, 24px); color: var(--fg-3); }
459
466
  .ds-dropzone-label { font-size: var(--fs-sm); color: var(--fg-3); }
467
+ .ds-dropzone:not(.dragover):not(.rejected):hover {
468
+ border-color: var(--fg-3);
469
+ }
460
470
  .ds-dropzone.dragover {
461
471
  border-color: var(--accent);
462
472
  background: var(--accent-tint);
@@ -470,6 +480,26 @@
470
480
  0%, 100% { border-color: var(--accent); }
471
481
  50% { border-color: color-mix(in oklab, var(--accent) 45%, transparent); }
472
482
  }
483
+ /* Rejected — an in-flight drag carrying a disallowed payload (wrong type,
484
+ over the size cap). Distinct from dragover: warn tone, not accent, so the
485
+ two "something is happening" states never read as the same affordance. */
486
+ .ds-dropzone.rejected {
487
+ border-color: var(--warn);
488
+ background: color-mix(in oklab, var(--warn) 10%, var(--bg));
489
+ }
490
+ @media (prefers-reduced-motion: no-preference) {
491
+ .ds-dropzone.rejected { animation: ds-dropzone-shake 0.4s var(--ease); }
492
+ }
493
+ .ds-dropzone.rejected .ds-dropzone-glyph,
494
+ .ds-dropzone.rejected .ds-dropzone-label { color: var(--warn); }
495
+ @keyframes ds-dropzone-shake {
496
+ 0%, 100% { transform: translateX(0); }
497
+ 25% { transform: translateX(-3px); }
498
+ 75% { transform: translateX(3px); }
499
+ }
500
+ .ds-dropzone--wrap.rejected > .ds-dropzone-inner {
501
+ outline-color: var(--warn);
502
+ }
473
503
  /* Wrapper form (has children): no permanent chrome - the content renders as if
474
504
  the zone were not there, and the dashed affordance overlays it ONLY while a
475
505
  drag is in flight. */
@@ -577,6 +607,8 @@
577
607
  max(var(--space-3), env(safe-area-inset-bottom))
578
608
  max(var(--space-3), env(safe-area-inset-left));
579
609
  background: color-mix(in oklab, var(--ink) 55%, transparent);
610
+ backdrop-filter: blur(2px);
611
+ -webkit-backdrop-filter: blur(2px);
580
612
  }
581
613
  @media (prefers-reduced-motion: no-preference) {
582
614
  .ds-modal-backdrop { animation: ds-modal-fade var(--dur-base) var(--ease); }
@@ -651,10 +683,17 @@
651
683
  floor. --ink on the same fill measures 6.27:1. Same principle as the
652
684
  --accent/--accent-ink split the token layer documents: a light saturated fill
653
685
  takes the dark text tone. Measured in a live browser. */
686
+ /* Severity is never color-alone (gd-14, color-blindness/grayscale-print safe):
687
+ the bold weight + widened letter-spacing is a shape cue that survives a
688
+ colorless render, matching how .btn-primary.danger differs from every
689
+ other .btn-primary at a glance even with color stripped out. */
654
690
  .btn-primary.danger {
655
691
  background: var(--warn);
656
692
  border-color: var(--warn);
657
693
  color: var(--ink);
694
+ box-shadow: inset 0 0 0 var(--bw-hair, 1px) color-mix(in oklab, var(--ink) 25%, transparent);
695
+ font-weight: 700;
696
+ letter-spacing: 0.01em;
658
697
  }
659
698
  .btn-primary.danger:hover { filter: brightness(0.92); border-color: var(--warn); }
660
699
 
@@ -61,6 +61,12 @@
61
61
  padding-bottom: var(--space-2); border-bottom: var(--bw-hair) solid var(--bg-3);
62
62
  }
63
63
  .ds-hero-stat:last-child { border-bottom: 0; padding-bottom: 0; }
64
+ /* HeroFromPageData's richer badge shape ({label, desc}) renders a strong
65
+ figure plus a muted descriptor inside the same .ds-hero-stat row — give
66
+ the pair a baseline-aligned inline layout instead of falling back to
67
+ unstyled default inline flow. */
68
+ .ds-hero-stat-n { font-weight: 700; color: var(--fg); margin-right: var(--space-2); }
69
+ .ds-hero-stat-l { color: var(--fg-3); font-size: var(--fs-xs); }
64
70
  .ds-hero-title {
65
71
  font-family: var(--ff-display); font-weight: 700;
66
72
  /* Bespoke display clamp, deliberately off the --fs-* ladder: --fs-hero is
@@ -173,6 +179,8 @@
173
179
  padding: var(--space-2) 0;
174
180
  font-family: var(--ff-display); font-weight: 600;
175
181
  text-transform: uppercase; letter-spacing: var(--tr-caps);
182
+ mask-image: linear-gradient(90deg, transparent 0, var(--fg) 28px, var(--fg) calc(100% - 28px), transparent 100%);
183
+ -webkit-mask-image: linear-gradient(90deg, transparent 0, var(--fg) 28px, var(--fg) calc(100% - 28px), transparent 100%);
176
184
  }
177
185
  .ds-marquee-track {
178
186
  display: inline-flex; gap: var(--space-5);
@@ -205,7 +213,7 @@
205
213
  .cli {
206
214
  display: flex; align-items: center; gap: 14px;
207
215
  overflow-x: auto;
208
- padding: 20px 24px;
216
+ padding: var(--space-3-5) var(--space-4);
209
217
  background: var(--ink); color: var(--paper);
210
218
  border-radius: var(--r-3);
211
219
  font-family: var(--ff-mono); font-size: var(--fs-lg);
@@ -213,12 +221,13 @@
213
221
  .cli .prompt { color: var(--green-2); }
214
222
  .cli .cmd { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; color: var(--paper); }
215
223
  .cli .copy {
216
- padding: 8px 16px; background: transparent; color: var(--paper);
224
+ padding: var(--space-2) var(--space-3); background: transparent; color: var(--paper);
217
225
  font-size: var(--fs-tiny); text-transform: uppercase; letter-spacing: var(--tr-caps);
218
226
  cursor: pointer; box-shadow: inset 0 0 0 1px var(--rule-strong);
219
227
  border-radius: var(--r-pill);
220
228
  }
221
229
  .cli .copy:hover { background: var(--fg-3); }
230
+ .cli .copy:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
222
231
 
223
232
  /* Multi-line CLI block: when .cli holds .ds-cli-row / .ds-cli-comment children
224
233
  (quickstart scripts, multi-command snippets — see CliBlock in
@@ -254,7 +263,7 @@
254
263
  .ds-quickstart-cli { flex-direction: column; align-items: stretch; gap: var(--space-2); }
255
264
  .ds-quickstart-copy {
256
265
  align-self: flex-end;
257
- padding: 8px 16px; background: transparent; color: var(--paper);
266
+ padding: var(--space-2) var(--space-3); background: transparent; color: var(--paper);
258
267
  font-family: var(--ff-mono); font-size: var(--fs-tiny); text-transform: uppercase; letter-spacing: var(--tr-caps);
259
268
  cursor: pointer; box-shadow: inset 0 0 0 1px var(--rule-strong);
260
269
  border-radius: var(--r-pill); border: none;
@@ -450,7 +459,7 @@ table tr.clickable:focus-visible td { background: var(--bg-2); }
450
459
  display: inline-flex; align-items: center; gap: var(--space-hair);
451
460
  font-size: var(--fs-xs); font-weight: 600;
452
461
  }
453
- .kpi-delta-up { color: var(--green); }
462
+ .kpi-delta-up { color: var(--success); }
454
463
  .kpi-delta-down { color: var(--danger); }
455
464
  .ds-sparkline path { stroke: currentColor; }
456
465
  .ds-sparkline-up { color: var(--success); }
@@ -157,7 +157,17 @@
157
157
  aspect-ratio: auto;
158
158
  }
159
159
  }
160
- .ds-deck-slide { flex: 1; display: flex; min-width: 0; }
160
+ .ds-deck-slide {
161
+ flex: 1; display: flex; min-width: 0;
162
+ animation: ds-deck-slide-in var(--dur-base) var(--ease);
163
+ }
164
+ @media (prefers-reduced-motion: reduce) {
165
+ .ds-deck-slide { animation: none; }
166
+ }
167
+ @keyframes ds-deck-slide-in {
168
+ from { opacity: 0; transform: translateY(var(--space-1-75)); }
169
+ to { opacity: 1; transform: none; }
170
+ }
161
171
  .ds-deck-controls {
162
172
  /* 14px sits between --space-2-75 (12) and --space-3 (16) with no tier; it is
163
173
  the deck control spacing tuned against the mono glyph run. */
@@ -168,6 +178,19 @@
168
178
  @media (pointer: coarse) {
169
179
  .ds-deck-controls .btn { min-height: 44px; min-width: 44px; }
170
180
  }
181
+ /* Dot progress indicator: one dot per slide, current slide filled with
182
+ --accent (same fill token .ds-dot-on uses elsewhere in the system), rest
183
+ dim at --fg-3 so the row reads as a track, not a second counter. */
184
+ .ds-deck-progress {
185
+ display: flex; align-items: center; justify-content: center;
186
+ gap: var(--space-1-5); padding-bottom: var(--space-2);
187
+ }
188
+ .ds-deck-dot {
189
+ width: var(--space-1-5); height: var(--space-1-5); border-radius: 50%;
190
+ background: var(--fg-3); flex: 0 0 auto;
191
+ transition: background var(--dur-base) var(--ease), transform var(--dur-base) var(--ease);
192
+ }
193
+ .ds-deck-dot--active { background: var(--accent); transform: scale(1.3); }
171
194
  /* Kit small-label voice — one local composition of the house `.t-label` recipe
172
195
  (base.css) for every eyebrow/label in the appended kit blocks below. These
173
196
  previously each restated the four declarations with drifted tracking
@@ -278,7 +301,12 @@
278
301
  }
279
302
  .ds-gallery-tile {
280
303
  cursor: pointer; display: flex; flex-direction: column; gap: var(--space-1-75);
281
- padding: var(--space-2-75); border-radius: var(--r-1); min-height: clamp(120px, 18vw, 160px);
304
+ padding: var(--space-2-75); border-radius: var(--r-1);
305
+ /* Fixed aspect-ratio (not just a min-height floor) so every tile in the grid
306
+ occupies the same shape regardless of caption length — the ascii specimen
307
+ is centered inside via .ds-tile-cap's flex alignment, the equivalent of
308
+ object-fit: cover for a text tile with no bitmap to crop. */
309
+ aspect-ratio: 4 / 3;
282
310
  border: 0; text-align: left; font: inherit; color: inherit;
283
311
  background: var(--tile-tone, var(--panel-1));
284
312
  transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
@@ -288,7 +316,7 @@
288
316
  translateY + shadow idiom .chat-msg:hover .chat-bubble already uses. */
289
317
  .ds-gallery-tile:hover { transform: translateY(-2px); box-shadow: 0 2px 8px color-mix(in oklab, var(--fg) 10%, transparent); }
290
318
  .ds-gallery-tile:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
291
- .ds-gallery-tile--tight { min-height: clamp(96px, 14vw, 120px); }
319
+ .ds-gallery-tile--tight { aspect-ratio: 3 / 2; }
292
320
  /* The lightbox scrim/card chrome itself is now Dialog's (.ds-ep-dialog /
293
321
  .ds-ep-dialog-backdrop in editor-primitives.css) — Dialog already ships
294
322
  role="dialog", aria-modal, Tab-trap and focus-restore-on-close, which the
@@ -312,13 +340,21 @@
312
340
  @media (max-width: 480px) {
313
341
  .ds-topbar-search { flex-basis: 100%; }
314
342
  }
315
- .ds-empty-state { padding: clamp(var(--space-3), 5vw, var(--space-4)); text-align: center; color: var(--panel-text-3); }
343
+ /* Namespaced .ds-kit-empty-state (not .ds-empty-state) -- .ds-empty-state is
344
+ primitives.css's shared base (padding var(--space-6), color var(--fg-3));
345
+ this sheet loads after it in app-shell.css's import order, so reusing the
346
+ same class name silently overrode that base's padding/color for every
347
+ consumer, defeating the "shared base, one place" contract primitives.css
348
+ documents. This block is unused by any component today, but kept
349
+ namespaced so a future consumer gets this sheet's fluid clamp() treatment
350
+ deliberately rather than by cascade accident. */
351
+ .ds-kit-empty-state { padding: clamp(var(--space-3), 5vw, var(--space-4)); text-align: center; color: var(--panel-text-3); }
316
352
  /* Fluid ICON size (empty-state mark), not a type tier: it scales with the
317
353
  viewport between 24 and 32px. Every fluid --fs-* tier is a heading clamp
318
354
  tuned to cqi + heading weight, so none of them substitutes here. */
319
- .ds-empty-state-glyph { font-size: clamp(24px, 6vw, 32px); }
320
- .ds-empty-state-msg { margin: var(--space-1-75) 0; }
321
- .ds-empty-state-hint { margin: 0; font-size: var(--fs-sm); }
355
+ .ds-kit-empty-state-glyph { font-size: clamp(24px, 6vw, 32px); }
356
+ .ds-kit-empty-state-msg { margin: var(--space-1-75) 0; }
357
+ .ds-kit-empty-state-hint { margin: 0; font-size: var(--fs-sm); }
322
358
 
323
359
  /* --- system primer ------------------------------------------ */
324
360
  /* The three primer panels below share one fluid inset,
@@ -707,6 +743,7 @@
707
743
  border-radius: 50%; background: var(--bg-3); color: var(--fg);
708
744
  font-family: var(--ff-mono); font-weight: 600; text-transform: lowercase;
709
745
  letter-spacing: 0.02em; user-select: none; flex-shrink: 0; object-fit: cover;
746
+ box-shadow: inset 0 0 0 1px var(--rule);
710
747
  }
711
748
  .ds-avatar-sm { width: 24px; height: 24px; font-size: var(--fs-pico); }
712
749
  .ds-avatar-md { width: 36px; height: 36px; font-size: var(--fs-micro); }
@@ -834,6 +871,7 @@
834
871
  font-weight: 600;
835
872
  line-height: 1;
836
873
  user-select: none;
874
+ box-shadow: inset 0 0 0 1px var(--rule);
837
875
  }
838
876
 
839
877
  /* Body stack — name + time on one baseline, text beneath. .chat-stack's
@@ -55,9 +55,11 @@
55
55
  .ds-skeleton {
56
56
  background: linear-gradient(90deg, var(--bg-2) 25%, var(--bg-3) 50%, var(--bg-2) 75%);
57
57
  background-size: 200% 100%;
58
- animation: ds-shimmer 1.5s infinite;
59
58
  border-radius: var(--r-1);
60
59
  }
60
+ @media (prefers-reduced-motion: no-preference) {
61
+ .ds-skeleton { animation: ds-shimmer 1.5s infinite; }
62
+ }
61
63
 
62
64
  @keyframes ds-shimmer {
63
65
  0% { background-position: 200% 0; }
@@ -59,9 +59,19 @@
59
59
  font-size: var(--fs-tiny); font-weight: 600;
60
60
  color: var(--fg-3);
61
61
  letter-spacing: var(--tr-caps); text-transform: uppercase;
62
+ /* A hairline divider is the header's only structural separator from the
63
+ body — without it, an uppercase caps label at fs-tiny/600 reads as just
64
+ another dim meta line once the eye drops past the top of the panel. */
65
+ border-bottom: var(--bw-hair) solid var(--rule);
66
+ margin-bottom: var(--space-1);
62
67
  }
63
68
  .panel.panel-wide .panel-head { padding-left: 0; padding-right: 0; }
64
- .panel-head > :last-child {
69
+ /* A borderless .panel-wide (no card edge to lean on) would otherwise show a
70
+ divider floating with nothing to visually anchor it — drop it there and
71
+ rely on spacing alone, matching panel-wide's already-transparent chrome. */
72
+ .panel.panel-wide .panel-head { border-bottom: none; margin-bottom: 0; }
73
+ .panel.panel--dense .panel-head { border-bottom: var(--bw-hair) solid var(--rule); margin-bottom: 0; }
74
+ .panel-head > :last-child:not(.ds-badge) {
65
75
  font-weight: 500; color: var(--fg-3);
66
76
  font-family: var(--ff-mono); letter-spacing: 0;
67
77
  }
@@ -65,6 +65,10 @@
65
65
  .ds-icon-btn-base { width: 32px; height: 32px; }
66
66
  .ds-icon-btn-lg { width: 40px; height: 40px; }
67
67
  .ds-icon-btn-xl { width: 48px; height: 48px; }
68
+ @media (pointer: coarse) {
69
+ .ds-icon-btn-xs { width: 32px; height: 32px; }
70
+ .ds-icon-btn-sm { width: 32px; height: 32px; }
71
+ }
68
72
  .ds-icon-btn-ghost { background: transparent; }
69
73
  .ds-icon-btn-ghost:hover { background: var(--fg); color: var(--bg); }
70
74
  .ds-icon-btn-primary { background: var(--accent); color: var(--accent-fg); }
@@ -86,7 +90,11 @@
86
90
  border-radius: var(--r-pill);
87
91
  background: var(--bg-3); color: var(--fg-2);
88
92
  }
89
- .ds-badge.ds-badge--sm { min-width: var(--icon-xs); height: var(--icon-xs); padding: 0 var(--space-1); font-size: var(--fs-pico); }
93
+ /* --fs-nano (11px), not --fs-pico (10px): --fs-pico sits below the 11px
94
+ legibility floor other small-scale labels in this system respect (see
95
+ .ds-stat-lbl, .ds-session-row-id), and this size is used live for the
96
+ workspace rail item-count badge (atoms.js Badge size="sm"). */
97
+ .ds-badge.ds-badge--sm { min-width: var(--icon-xs); height: var(--icon-xs); padding: 0 var(--space-1); font-size: var(--fs-nano); }
90
98
  .ds-badge.ds-badge--lg { min-width: 22px; height: 22px; padding: 0 var(--space-2); font-size: var(--fs-tiny); }
91
99
  .ds-badge.tone-green, .ds-badge.tone-live, .ds-badge.tone-success {
92
100
  background: var(--green-tint); color: var(--green-deep);
@@ -143,7 +151,6 @@
143
151
  .chip.tone-disabled { background: var(--bg-3); color: var(--fg-3); }
144
152
  .chip.tone-blue { background: var(--accent-tint); color: var(--accent-ink); }
145
153
  .chip.tone-orange { background: color-mix(in oklab, var(--flame) 30%, var(--sun) 70%); color: var(--ink); }
146
- .chip.tone-yellow { background: var(--sun); color: var(--ink); }
147
154
 
148
155
  .chip.ds-chip-removable { padding-right: var(--space-1); }
149
156
  .ds-chip-remove-btn {
@@ -183,6 +190,7 @@
183
190
  }
184
191
  .glyph { --glyph-size-sm: var(--fs-nano); --glyph-size-base: var(--fs-tiny); --glyph-size-lg: var(--fs-body); }
185
192
  .glyph-sm { width: var(--icon-xs); height: var(--icon-xs); }
193
+ .glyph-base { width: 18px; height: 18px; }
186
194
  .glyph-lg { width: 22px; height: 22px; }
187
195
 
188
196
  /* Shared empty-state base -- hero-content.css's .empty and community.css's
@@ -201,3 +209,41 @@
201
209
  .ds-empty-state--panel { background: var(--bg-2); border-radius: var(--r-1); }
202
210
  .ds-empty-state--compact { padding: var(--space-5) var(--space-3); }
203
211
 
212
+ /* .ds-tooltip — shared floating bubble (overlay-primitives/tooltip.js appends
213
+ one instance to <body> and repositions it via useFloating). Fade timing
214
+ matches the component's own 350ms hover-intent delay. */
215
+ .ds-tooltip {
216
+ position: fixed;
217
+ z-index: var(--z-top);
218
+ max-width: 240px;
219
+ padding: var(--space-1) var(--space-2);
220
+ border-radius: var(--r-1);
221
+ background: var(--fg);
222
+ color: var(--bg);
223
+ font-size: var(--fs-xs);
224
+ line-height: 1.35;
225
+ box-shadow: var(--shadow-2);
226
+ pointer-events: none;
227
+ opacity: 0;
228
+ transform: scale(0.96);
229
+ transition: opacity var(--dur-snap) var(--ease), transform var(--dur-snap) var(--ease);
230
+ }
231
+ .ds-tooltip:not([hidden]) { opacity: 1; transform: scale(1); }
232
+ .ds-tooltip::after {
233
+ content: '';
234
+ position: absolute;
235
+ width: 6px;
236
+ height: 6px;
237
+ background: var(--fg);
238
+ transform: rotate(45deg);
239
+ }
240
+ .ds-tooltip[data-placement^="top"]::after { bottom: -3px; left: 50%; margin-left: -3px; }
241
+ .ds-tooltip[data-placement^="bottom"]::after { top: -3px; left: 50%; margin-left: -3px; }
242
+ .ds-tooltip[data-placement^="left"]::after { right: -3px; top: 50%; margin-top: -3px; }
243
+ .ds-tooltip[data-placement^="right"]::after { left: -3px; top: 50%; margin-top: -3px; }
244
+ .ds-tooltip.kind-danger { background: var(--warn); color: var(--ink); }
245
+ .ds-tooltip.kind-danger::after { background: var(--warn); }
246
+ @media (prefers-reduced-motion: reduce) {
247
+ .ds-tooltip { transition: none; }
248
+ }
249
+