@apliteni/apliteni-ui 0.9.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ /* ============================================================================
2
+ * Confirm — the kit's confirmation dialog: a centred panel over a scrim, for
3
+ * the question a destructive action has to ask. Fully token-driven so it
4
+ * re-themes across accents + light/dark. Shares its scrim + focus-trap
5
+ * behaviour with the drawer (see overlay.js).
6
+ *
7
+ * Motion is the drawer's, deliberately: --dur-med/--ease, and `visibility`
8
+ * switched rather than transitioned. A transitioned visibility is still
9
+ * `hidden` in the frame the class lands, so openConfirm()'s focus() would have
10
+ * nothing to focus and the reader would be left on <body>. The root owns the
11
+ * switch; the panel and scrim inherit it.
12
+ *
13
+ * Local tokens (surface/shadow/scrim) so the panel + scrim re-theme cleanly:
14
+ * --confirm-surface panel background --confirm-shadow panel elevation
15
+ * --confirm-scrim backdrop colour (--scrim already re-themes per theme)
16
+ * ========================================================================== */
17
+
18
+ .ui-confirm {
19
+ --confirm-surface: var(--surface-2);
20
+ --confirm-shadow: var(--shadow-lg);
21
+ --confirm-scrim: var(--scrim);
22
+ --confirm-w: 420px;
23
+
24
+ position: fixed;
25
+ inset: 0;
26
+ /* One above the drawer's own overlay level: a confirm asks a question about the
27
+ drawer it was opened over, so it has to be readable above it whatever order
28
+ the two are mounted in. */
29
+ z-index: calc(var(--z-overlay) + 1);
30
+ pointer-events: none; /* container never blocks; scrim/panel opt back in */
31
+
32
+ /* Only the hide is transitioned, so the panel is still on screen while it
33
+ fades out; the show is instant, because focus moves in the same frame.
34
+ `linear` and not var(--ease): visibility is discrete, easing a step buys
35
+ nothing, and a curve that leaves [0, 1] would flip it mid-fade. */
36
+ visibility: hidden;
37
+ transition: visibility var(--dur-med) linear;
38
+ }
39
+ .ui-confirm.is-open { visibility: visible; transition: none; }
40
+
41
+ /* Hit-testable only while open. The root stays visible for --dur-med after
42
+ closeConfirm() has stopped trapping, and an accept button that is still
43
+ clickable in that window runs the caller's destructive work a second time. */
44
+ .ui-confirm.is-open .ui-confirm__scrim,
45
+ .ui-confirm.is-open .ui-confirm__panel { pointer-events: auto; }
46
+
47
+ .ui-confirm__scrim {
48
+ position: absolute;
49
+ inset: 0;
50
+ background: var(--confirm-scrim);
51
+ opacity: 0;
52
+ transition: opacity var(--dur-med) var(--ease);
53
+ }
54
+ .ui-confirm.is-open .ui-confirm__scrim { opacity: 1; }
55
+
56
+ /* Panel — centred with `translate` so the entrance can own `transform` without
57
+ dropping the panel into the corner when it resets to none. */
58
+ .ui-confirm__panel {
59
+ position: absolute;
60
+ top: 50%;
61
+ left: 50%;
62
+ translate: -50% -50%;
63
+ width: min(var(--confirm-w), calc(100vw - var(--space-8)));
64
+ /* A fixed, centred panel cannot be scrolled into view — scrollIntoView does
65
+ nothing to it — so a panel taller than the viewport puts its answers below
66
+ the fold for good. Bound it, and let the consequence scroll instead. The
67
+ dvh line wins where it is understood; the vh line is what holds it up
68
+ everywhere else. */
69
+ max-height: calc(100vh - var(--space-8));
70
+ max-height: calc(100dvh - var(--space-8));
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: var(--space-2);
74
+ padding: var(--space-5);
75
+ background: var(--confirm-surface);
76
+ border: 1px solid var(--border);
77
+ border-radius: var(--radius-lg);
78
+ box-shadow: var(--confirm-shadow);
79
+ opacity: 0;
80
+ transform: translateY(8px);
81
+ transition:
82
+ opacity var(--dur-med) var(--ease),
83
+ transform var(--dur-med) var(--ease);
84
+ }
85
+ .ui-confirm.is-open .ui-confirm__panel { opacity: 1; transform: none; }
86
+ .ui-confirm__panel:focus-visible { outline: none; box-shadow: var(--confirm-shadow), var(--ring); }
87
+
88
+ /* -- Slots ----------------------------------------------------------------- */
89
+ /* The question and the answers keep their size; the consequence between them is
90
+ the one that gives, and scrolls — the same split the drawer's header / body /
91
+ footer makes. */
92
+ .ui-confirm__title {
93
+ margin: 0;
94
+ flex: none;
95
+ font-family: var(--font-sans);
96
+ font-size: var(--text-md);
97
+ font-weight: var(--weight-semibold);
98
+ line-height: 1.35;
99
+ color: var(--strong);
100
+ }
101
+ .ui-confirm__body {
102
+ margin: 0;
103
+ flex: 1 1 auto;
104
+ min-height: 0;
105
+ overflow-y: auto;
106
+ font-family: var(--font-sans);
107
+ font-size: var(--text-sm);
108
+ font-weight: var(--weight-normal);
109
+ line-height: 1.55;
110
+ color: var(--muted);
111
+ }
112
+ .ui-confirm__acts {
113
+ display: flex;
114
+ flex: none;
115
+ flex-wrap: wrap;
116
+ align-items: center;
117
+ justify-content: flex-end;
118
+ gap: var(--space-2);
119
+ margin-top: var(--space-2);
120
+ }
121
+
122
+ /* -- Reduced motion: no rise, just a fade --------------------------------- */
123
+ @media (prefers-reduced-motion: reduce) {
124
+ .ui-confirm { transition: visibility var(--dur-fast) linear; }
125
+ .ui-confirm__panel {
126
+ transform: none;
127
+ transition: opacity var(--dur-fast) linear;
128
+ }
129
+ .ui-confirm__scrim { transition: opacity var(--dur-fast) linear; }
130
+ }
@@ -1,9 +1,9 @@
1
1
  /* ============================================================================
2
2
  * Drawer — edge-anchored overlay panel (Sheet / Slide-over): a panel that
3
3
  * slides in from any screen edge over a scrim. Fully token-driven so it
4
- * re-themes across accents + light/dark. Shares its scrim + focus-trap concept
5
- * with a future Modal (see drawer.js). `prefers-reduced-motion` drops the slide
6
- * for a plain fade.
4
+ * re-themes across accents + light/dark. Shares its scrim + focus trap with
5
+ * confirm() (see overlay.js). `prefers-reduced-motion` drops the slide for a
6
+ * plain fade.
7
7
  *
8
8
  * Local tokens (surface/shadow/scrim) so the panel + scrim re-theme cleanly:
9
9
  * --drawer-surface panel background --drawer-shadow panel elevation
@@ -21,18 +21,35 @@
21
21
  inset: 0;
22
22
  z-index: var(--z-overlay);
23
23
  pointer-events: none; /* container never blocks; scrim/panel opt back in */
24
+
25
+ /* One place owns "is this drawer on screen at all", and the scrim and panel
26
+ inherit it. Only the HIDE is transitioned, so the panel is still on screen
27
+ while it slides out; the show is instant (`.is-open` cancels the
28
+ transition) because openDrawer() focuses a control in the same frame it
29
+ opens, and focus() on a hidden element does nothing. `linear` and not
30
+ var(--ease): visibility is discrete, easing a step buys nothing, and a curve
31
+ that leaves [0, 1] would flip it in the middle of the fade. */
32
+ visibility: hidden;
33
+ transition: visibility var(--dur-med) linear;
24
34
  }
25
- /* Scrim fades in; visibility trails the fade so the panel can animate out. */
35
+ .ui-drawer.is-open { visibility: visible; transition: none; }
36
+
37
+ /* Hit-testable only while open. The root stays visible for --dur-med after
38
+ closeDrawer() has stopped trapping, and any control still clickable in that
39
+ window — the footer's actions, the close button — runs the caller's handler
40
+ again on the way out. */
41
+ .ui-drawer.is-open .ui-drawer__scrim,
42
+ .ui-drawer.is-open .ui-drawer__panel { pointer-events: auto; }
43
+
44
+ /* Scrim — fades in and out; the drawer root decides when it stops existing. */
26
45
  .ui-drawer__scrim {
27
46
  position: absolute;
28
47
  inset: 0;
29
48
  background: var(--drawer-scrim);
30
- pointer-events: auto;
31
49
  opacity: 0;
32
- visibility: hidden;
33
- transition: opacity var(--dur-med) var(--ease), visibility var(--dur-med) var(--ease);
50
+ transition: opacity var(--dur-med) var(--ease);
34
51
  }
35
- .ui-drawer.is-open .ui-drawer__scrim { opacity: 1; visibility: visible; }
52
+ .ui-drawer.is-open .ui-drawer__scrim { opacity: 1; }
36
53
 
37
54
  /* Panel — the sliding surface. Flex column: header / scrollable body / footer. */
38
55
  .ui-drawer__panel {
@@ -41,16 +58,13 @@
41
58
  flex-direction: column;
42
59
  background: var(--drawer-surface);
43
60
  box-shadow: var(--drawer-shadow);
44
- pointer-events: auto;
45
61
  opacity: 1;
46
- visibility: hidden;
47
62
  transition:
48
63
  transform var(--dur-med) var(--ease),
49
- opacity var(--dur-med) var(--ease),
50
- visibility var(--dur-med) var(--ease);
64
+ opacity var(--dur-med) var(--ease);
51
65
  }
52
66
  .ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--drawer-shadow), var(--ring); }
53
- .ui-drawer.is-open .ui-drawer__panel { visibility: visible; transform: none; }
67
+ .ui-drawer.is-open .ui-drawer__panel { transform: none; }
54
68
 
55
69
  /* -- Edge anchoring + slide-in direction ---------------------------------- */
56
70
  .ui-drawer--right .ui-drawer__panel,
@@ -154,11 +168,12 @@
154
168
 
155
169
  /* -- Reduced motion: no slide, just a fade -------------------------------- */
156
170
  @media (prefers-reduced-motion: reduce) {
171
+ .ui-drawer { transition: visibility var(--dur-fast) linear; }
157
172
  .ui-drawer__panel {
158
173
  transform: none !important;
159
174
  opacity: 0;
160
- transition: opacity var(--dur-fast) linear, visibility var(--dur-fast) linear;
175
+ transition: opacity var(--dur-fast) linear;
161
176
  }
162
177
  .ui-drawer.is-open .ui-drawer__panel { opacity: 1; }
163
- .ui-drawer__scrim { transition: opacity var(--dur-fast) linear, visibility var(--dur-fast) linear; }
178
+ .ui-drawer__scrim { transition: opacity var(--dur-fast) linear; }
164
179
  }
@@ -1,54 +1,209 @@
1
1
  /* ============================================================================
2
- * Layout primitives — account sidebar, page shell, auth card, landing hero.
2
+ * Layout primitives — the page shell, the auth card, the landing hero.
3
3
  * These compose the example apps; each stays token-driven and themeable.
4
+ * The hand-written .ui-side rail and .ui-shell grid are gone: appShell() draws
5
+ * the rail with sidebarNav(), so nothing emitted that markup any more.
4
6
  * ========================================================================== */
5
7
 
6
- /* -- Account sidebar (from viz/account.mjs .side) ------------------------ */
7
- .ui-side { position: sticky; top: 80px; align-self: start; }
8
- .ui-side .cap {
9
- font-size: 11px;
10
- letter-spacing: 0.18em;
11
- text-transform: uppercase;
12
- color: var(--muted);
13
- padding: 0 12px 11px;
14
- }
15
- .ui-side a {
8
+ /* -- The one page shell (appShell) --------------------------------------- */
9
+ /* A full-height rail beside one <main>. The rail's own rows are sidebarNav()'s
10
+ (.ui-nav--side), so nothing here restyles a nav item — these rules place the
11
+ rail, its brand head and its signed-in footer, and set the reading column. */
12
+ .ui-app {
13
+ display: grid;
14
+ grid-template-columns: max-content 1fr;
15
+ min-height: calc(100vh - var(--ui-app-top, 0px));
16
+ background: var(--bg);
17
+ color: var(--text);
18
+ font-family: var(--font-sans);
19
+ }
20
+ /* With a topbar above it, the shell starts below one. 52px is .topbar's height
21
+ in topbar.css; the topbar sticks so the rail below it can too. */
22
+ .ui-app-page { --ui-app-top: 52px; }
23
+ .ui-app-page .topbar { position: sticky; top: 0; z-index: var(--z-sticky); }
24
+
25
+ /* --surface-2, not --bg-elevated: elevated resolves to --bg itself in the light
26
+ theme, an exact no-op that left the border as the whole of the rail.
27
+ overflow-y, because the box is pinned at the viewport's height for the whole
28
+ scroll of .ui-app — without it a row past that height paints below the fold
29
+ and no amount of document scrolling brings it back. The rail's own padding is
30
+ what keeps a focus ring out of the clip, so it stays above --ring's spread. */
31
+ .ui-app__rail {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: var(--space-5);
35
+ padding: var(--space-5) var(--space-4);
36
+ border-right: 1px solid var(--border);
37
+ background: var(--surface-2);
38
+ position: sticky;
39
+ top: var(--ui-app-top, 0px);
40
+ height: calc(100vh - var(--ui-app-top, 0px));
41
+ overflow-y: auto;
42
+ }
43
+ /* The rail's hover WAS --surface-2, so moving the rail onto it would have made
44
+ hovering a row do nothing. Scoped, so a sidebarNav() anywhere else is
45
+ untouched — and the destructive row is left out, because this rule ties with
46
+ nav.css's danger wash on specificity and wins on source order alone. Sign out
47
+ was quietly losing the --pink wash the guideline page cites nav.css for. The
48
+ current row is left out for the same reason: this rule outranks nav.css's
49
+ active hover, and painting --surface-3 over a row already resting on it is
50
+ what made the row you are standing on the one row that ignores the pointer. */
51
+ .ui-app__rail .ui-nav__item:not(.is-danger):not(.is-active):hover { background: var(--surface-3); }
52
+ .ui-app__brand {
16
53
  display: flex;
17
54
  align-items: center;
18
- gap: 11px;
19
- padding: 10px 13px;
20
- border-radius: 10px;
21
- color: var(--text);
55
+ gap: var(--space-2);
56
+ padding: 2px 6px;
22
57
  text-decoration: none;
23
- font-size: 14.5px;
24
- }
25
- .ui-side a:hover { background: var(--surface-2); }
26
- .ui-side a.on { background: var(--surface); color: var(--strong); font-weight: 500; }
27
- .ui-side a svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.7; opacity: 0.7; }
28
- .ui-side a.on svg { opacity: 1; stroke: var(--accent); }
29
- .ui-side .ssep { height: 1px; background: var(--border); margin: 8px 12px; }
30
- .ui-side a.out { color: var(--muted); }
31
- .ui-side a.out:hover { color: var(--pink); }
58
+ font-weight: var(--weight-semibold);
59
+ letter-spacing: var(--tracking-tight);
60
+ }
61
+ /* Carries the shell's class as well as its own, for the same reason the rail
62
+ rows do (see nav.css): a host stylesheet's `a:link` is (0,1,1). */
63
+ .ui-app .ui-app__brand { color: var(--strong); }
64
+ .ui-app__brand svg { display: block; flex: none; }
32
65
 
33
- /* -- Account shell grid -------------------------------------------------- */
34
- .ui-shell {
35
- position: relative;
36
- z-index: 1;
37
- max-width: 960px;
38
- margin: 0 auto;
39
- padding: 40px 26px 96px;
40
- display: grid;
41
- grid-template-columns: 200px 1fr;
42
- gap: 44px;
43
- }
44
- .ui-shell__page { min-width: 0; }
45
- .ui-shell__crumbs { font-size: 13px; color: var(--dim); margin-bottom: 8px; }
46
- .ui-shell__crumbs b { color: var(--text); font-weight: 500; }
47
- .ui-shell__page h1 { color: var(--strong); font-weight: 600; font-size: 30px; letter-spacing: -0.01em; margin-bottom: 7px; }
48
- .ui-shell__page .sub { color: var(--dim); font-size: 15.5px; margin-bottom: 30px; }
66
+ /* The nav grows, so the reader block below it pins to the bottom of a full-height
67
+ rail instead of hugging the last nav row, and the nav's own footer — sign out —
68
+ sits directly above it. */
69
+ .ui-app__rail .ui-nav--side { flex: 1 1 auto; }
70
+ .ui-app__rail .ui-nav__foot { margin-top: auto; }
71
+
72
+ /* Which row is current is structural here, not chromatic: the resting glyphs
73
+ step down so the current one is the brightest whatever --accent is doing, and
74
+ the bar beside it carries the hue. A 3x17px bar on a 45x44 tile was the
75
+ smallest of three weak signals doing the most work, so it grows with the row. */
76
+ .ui-app__rail .ui-nav__ic svg { opacity: 0.62; }
77
+ .ui-app__rail .ui-nav__item.is-active .ui-nav__ic svg,
78
+ .ui-app__rail .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: currentColor; }
79
+ .ui-app__rail .ui-nav__item.is-active::before { width: 3px; height: 62%; }
80
+
81
+ /* Sign out is a navigation row and rests like one. The danger intent is the
82
+ step nav.css writes for it, which a resting --muted was pre-empting — it made
83
+ the last row of the rail the quietest thing in it. Both of nav.css's danger
84
+ states are named here, not hover alone: this rule outranks them, so a state
85
+ it forgets is a state it silently cancels. */
86
+ .ui-app__rail .ui-nav__item.is-danger:not(:hover):not(:focus-visible) { color: var(--text); }
87
+
88
+ /* One rule closes the rail, not two twenty pixels apart: the nav's own footer
89
+ keeps its hairline and the reader block is separated by space alone — it has
90
+ an avatar and a weight change already. */
91
+ .ui-app__user {
92
+ display: flex;
93
+ align-items: center;
94
+ gap: var(--space-2);
95
+ padding: var(--space-5) var(--space-1) 0;
96
+ }
97
+ .ui-app__av {
98
+ width: 30px; height: 30px; flex: none;
99
+ border-radius: var(--radius-pill);
100
+ background: var(--surface-3);
101
+ color: var(--strong);
102
+ display: grid; place-items: center;
103
+ font-size: 12px;
104
+ font-weight: var(--weight-semibold);
105
+ }
106
+ .ui-app__who { font-size: 12.5px; line-height: 1.3; min-width: 0; }
107
+ .ui-app__who b { display: block; color: var(--strong); font-weight: var(--weight-semibold); }
108
+ /* nowrap is what makes the ellipsis fire — without it the address wraps, and
109
+ the rail column is max-content, so a long one widens the rail instead. */
110
+ .ui-app__who span { display: block; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
111
+
112
+ /* The reading column centres in the track it is given — without width: 100% it
113
+ was hugging the rail, 519px of dead background on the right at 1728px wide.
114
+ The width is what keeps the auto margins honest: an auto inline margin takes
115
+ a grid item out of stretch, and Chrome then lays it out at its max-content
116
+ width, which is how the payout ledger pushed the page 529px sideways at 375.
117
+ The 860px is the floor for hand-written markup — shell.js validates what it
118
+ writes into --ui-app-main and falls back to this same number. */
119
+ .ui-app__main {
120
+ min-width: 0;
121
+ width: 100%;
122
+ max-width: var(--ui-app-main, 860px);
123
+ margin-inline: auto;
124
+ padding: 40px clamp(24px, 4vw, 56px) 72px;
125
+ }
126
+ /* Crumb, then the title block. At --space-2 the crumb sat 8px off the h1 and
127
+ the h1 8px off its subtitle, so all three ranks read as one. */
128
+ .ui-app__main .ui-nav--crumbs { margin-bottom: var(--space-5); }
129
+ .ui-app__main h1 {
130
+ color: var(--strong);
131
+ font-size: 30px;
132
+ font-weight: var(--weight-bold);
133
+ line-height: 1.1;
134
+ letter-spacing: var(--tracking-tight);
135
+ margin-bottom: 8px;
136
+ }
137
+ .ui-app__sub { color: var(--dim); font-size: 15px; line-height: 1.6; max-width: 60ch; margin-bottom: 32px; }
138
+ .ui-app__body { display: flex; flex-direction: column; gap: var(--space-5); }
139
+ /* A flex child's min-width is its content by default, so one wide block — a
140
+ ledger, a code sample — pushes the whole page sideways instead of scrolling
141
+ inside itself. This is what keeps the shell narrower than the window. */
142
+ .ui-app__body > * { min-width: 0; }
143
+
144
+ /* -- A row of controls above a list --------------------------------------- */
145
+ /* Filter fields, a segmented switch, the button that applies them. The row is
146
+ one line wherever the line holds and breaks where it does not, and the text
147
+ field is what makes that one decision instead of two: an .ui-input is
148
+ width:100%, so as a flex item its hypothetical size is the whole container
149
+ and wrapping alone put every control beside it on a second line at 1280 as
150
+ readily as at 375. Given a basis of its own the field shares the line, takes
151
+ the slack when there is any, and the line breaks only once the field would be
152
+ squeezed under that basis — 6rem, about as narrow as a placeholder plus a word
153
+ of what you typed. min-width:0 keeps an input's automatic minimum (its `size`
154
+ attribute, ~20 characters) from setting a floor well above the basis. */
155
+ .ui-toolbar { display: flex; flex-wrap: wrap; gap: 10px; }
156
+ .ui-toolbar > .ui-input,
157
+ .ui-toolbar > .ui-input-group { flex: 1 1 6rem; min-width: 0; }
158
+
159
+ /* -- The narrow rail ------------------------------------------------------ */
160
+ /* Below this width the rail folds to an icon strip instead of disappearing.
161
+ sidebarNav() names every row at every width, so folding the label out of view
162
+ costs a screen reader nothing. Scoped to the shell's own rail — a sidebarNav()
163
+ somewhere else keeps its labels. The 44px floor is the touch target, and the
164
+ column is that floor plus the rail's own padding and border — measured, not
165
+ guessed. Nothing here resizes the glyph: an icon-sizing rule inside a media
166
+ query is one src/styles/icon-size.test.js cannot measure, and it says so. */
49
167
  @media (max-width: 720px) {
50
- .ui-shell { grid-template-columns: 1fr; gap: 20px; }
51
- .ui-side { position: static; }
168
+ .ui-app { grid-template-columns: 58px 1fr; }
169
+ .ui-app__rail { padding: var(--space-3) 6px; gap: var(--space-3); }
170
+ .ui-app__rail .ui-nav--side { width: auto; }
171
+ .ui-app__rail .ui-nav__cap,
172
+ .ui-app__rail .ui-nav__label,
173
+ .ui-app__rail .ui-nav__badge,
174
+ .ui-app__rail .ui-nav__caret,
175
+ .ui-app__rail .ui-app__who,
176
+ .ui-app__brand span { display: none; }
177
+ .ui-app__rail .ui-nav__item { justify-content: center; gap: 0; padding: 0; min-height: 44px; }
178
+ .ui-app__rail .ui-nav__item.is-active::before { left: 3px; }
179
+ .ui-app__rail .ui-nav__foot { padding-top: var(--space-2); }
180
+ .ui-app__brand { justify-content: center; padding: 2px 0; }
181
+ .ui-app__user { justify-content: center; padding-left: 0; padding-right: 0; }
182
+ /* A group's children are rail rows at this width too. The nested list is NOT
183
+ folded away: wireNav() toggles the `hidden` attribute and nothing else, so
184
+ a display:none here left the toggle announcing aria-expanded over a list it
185
+ could not reach — and took the row carrying aria-current with it. What goes
186
+ instead is the indent and its guide hairline; a 46px column has no room. */
187
+ .ui-app__rail .ui-nav__sub { margin: 0; padding-left: 0; border-left: 0; }
188
+ .ui-app__rail .ui-nav__item--sub.is-active::before { left: 3px; }
189
+ /* sidebarNav() takes `icon` as optional at every level, and the glyph is the
190
+ whole of a row on screen here — so a row without one, at any level, would be
191
+ a blank 44px target. It gets a dot in the row's own ink instead; the
192
+ aria-label names it either way. */
193
+ .ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after {
194
+ content: "";
195
+ width: 5px; height: 5px;
196
+ border-radius: 50%;
197
+ background: currentColor;
198
+ opacity: 0.62;
199
+ }
200
+ .ui-app__main { padding: 28px var(--space-4) 56px; }
201
+ .ui-app__main h1 { font-size: 25px; }
202
+ /* A settings row is label-beside-control down to the width where the control
203
+ no longer fits beside anything; below that it stacks. Scoped to the shell
204
+ rather than written into card.css, because a card row outside a shell is
205
+ somebody else's container to size — see the report. */
206
+ .ui-app__main .ui-card__row { flex-wrap: wrap; gap: var(--space-3); }
52
207
  }
53
208
 
54
209
  /* -- Auth card (sign-in / consent) — centered, glowing --------------------- */
@@ -1,8 +1,8 @@
1
1
  /* ============================================================================
2
2
  * Navigation — sidebar rail, horizontal tabs, breadcrumbs. One token-driven
3
3
  * class family (ui-nav__…) so every variant re-themes across accents +
4
- * light/dark. Mirrors the look the app shells draw inline (.ui-side) but as a
5
- * first-class, standalone component. Active/hover surfaces are pure tokens.
4
+ * light/dark. appShell() draws its rail from sidebarNav(), so this file is the
5
+ * page shell's rail as well as a standalone component. States are pure tokens.
6
6
  * ========================================================================== */
7
7
 
8
8
  .ui-nav { font-family: var(--font-sans); }
@@ -35,7 +35,6 @@
35
35
  width: 100%;
36
36
  padding: 9px 12px;
37
37
  border-radius: 10px;
38
- color: var(--text);
39
38
  text-decoration: none;
40
39
  font-size: 14.5px;
41
40
  font-weight: 400;
@@ -47,6 +46,7 @@
47
46
  font-family: inherit;
48
47
  transition: background 0.16s var(--ease), color 0.16s var(--ease);
49
48
  }
49
+ .ui-nav .ui-nav__item { color: var(--text); } /* (0,2,0): a host sheet's a:link is (0,1,1) — why: docs/adr/0007-one-page-shell-built-from-the-kits-own-nav.md */
50
50
  .ui-nav__item:hover { background: var(--surface-2); color: var(--strong); }
51
51
  .ui-nav__item.is-active {
52
52
  background: var(--surface-3);
@@ -75,12 +75,26 @@
75
75
  .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: var(--accent); }
76
76
  .ui-nav__label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
77
77
 
78
- /* Destructive row (sign out, revoke). Quiet at rest, --pink once the pointer
79
- reaches it — same two-step as .ui-btn--danger and .ui-dropdown__item.is-danger. */
78
+ /* Destructive row (sign out, revoke). Quiet at rest, --pink once a pointer or
79
+ the keyboard reaches it — as .ui-btn--danger and .ui-dropdown__item do. */
80
80
  .ui-nav__item.is-danger { color: var(--muted); }
81
81
  .ui-nav__item.is-danger:hover { background: var(--glow-pink); color: var(--pink); }
82
+ /* Tabbing onto it is the same "about to click this" the pointer means. The ring
83
+ says where you are; only this says what the row does. */
84
+ .ui-nav__item.is-danger:focus-visible { background: var(--glow-pink); color: var(--pink); }
82
85
  .ui-nav__item.is-disabled { color: var(--muted); opacity: 0.55; cursor: default; pointer-events: none; }
83
86
 
87
+ /* The current row under the pointer. It rests on --surface-3, which is also what
88
+ a resting row hovers to, so the one row with no hover response was the row the
89
+ reader is standing on. There is no surface above --surface-3, so the step is
90
+ mixed: --text is near-white in dark and near-black in light, and both themes
91
+ raise a surface by moving it away from the page. Written after the danger
92
+ rules so it cannot out-rank them — sign out is never the current row, but a
93
+ rule that could take its wash would be one to argue with later. */
94
+ .ui-nav__item.is-active:not(.is-danger):hover {
95
+ background: color-mix(in srgb, var(--text) 10%, var(--surface-3));
96
+ }
97
+
84
98
  /* Per-item badge / counter */
85
99
  .ui-nav__badge {
86
100
  flex: none;
@@ -101,6 +115,16 @@
101
115
  .ui-nav__badge.is-live { background: var(--chip-success-fill); color: var(--chip-success-ink); }
102
116
  .ui-nav__badge.is-danger { background: var(--glow-pink); color: var(--pink); }
103
117
  .ui-nav__item.is-active .ui-nav__badge.is-neutral { background: var(--surface-3); color: var(--strong); }
118
+ /* Same shape, one row up: on an active row the accent counter takes the row's
119
+ OWN surface instead of laying its wash over it. An active row is already
120
+ painted --surface-3, so --glow-purple there is two tints stacked. Every
121
+ figure here is measured against the tokens this kit now ships. The accent on
122
+ that pair falls under WCAG AA in four of the eight theme x accent cells —
123
+ 4.08 in dark Nebula, where the flat surface reads 4.92 — and no alpha reaches
124
+ it, the largest that clears being 0.06 in dark Nebula and 0.02 in light
125
+ Emerald. So the accent wash is painted on a base surface, never a raised one.
126
+ See #157 and docs/adr/0006-the-accent-is-measured-against-its-own-wash.md. */
127
+ .ui-nav__item.is-active .ui-nav__badge.is-accent { background: var(--surface-3); color: var(--accent); }
104
128
 
105
129
  /* Collapsible group */
106
130
  .ui-nav__group { display: flex; flex-direction: column; }
@@ -213,11 +237,14 @@
213
237
  gap: 6px;
214
238
  padding: 3px 6px;
215
239
  border-radius: 7px;
216
- color: var(--dim);
217
240
  text-decoration: none;
218
241
  font-size: 13px;
219
242
  transition: color 0.16s var(--ease), background 0.16s var(--ease);
220
243
  }
244
+ /* Carries the block's class as well as its own, for the reason .ui-nav__item
245
+ does above: a linked crumb is an <a>, and a host stylesheet's `a:link` is
246
+ (0,1,1), which outranks a bare (0,1,0) class. */
247
+ .ui-nav .ui-nav__crumb { color: var(--dim); }
221
248
  a.ui-nav__crumb:hover { color: var(--strong); background: var(--surface-2); }
222
249
  a.ui-nav__crumb:focus-visible { outline: none; box-shadow: var(--ring); }
223
250
  .ui-nav__crumb.is-current { color: var(--strong); font-weight: 500; }
@@ -21,7 +21,10 @@
21
21
  --accent: #ff6a3d;
22
22
  --accent-strong: var(--accent);
23
23
  --accent-contrast: #241006;
24
- --glow-purple: rgba(255, 106, 61, 0.18);
24
+ /* Thinned from 0.18 for WCAG AA: the ember accent cleared every flat surface
25
+ but not its own wash over a card, which lifts the ground toward the ink. The
26
+ hue is untouched. See issue #157. */
27
+ --glow-purple: rgba(255, 106, 61, 0.15);
25
28
  --ring: 0 0 0 3px rgba(255, 106, 61, 0.38);
26
29
  --grad-from: #ff7a45;
27
30
  --grad-to: #ffcf6a;
@@ -49,7 +52,9 @@
49
52
  --accent: #3b9dff;
50
53
  --accent-strong: var(--accent);
51
54
  --accent-contrast: #04121f;
52
- --glow-purple: rgba(59, 157, 255, 0.18);
55
+ /* Thinned from 0.18 for WCAG AA, the same wash-over-a-card pair as dark
56
+ Phoenix and from the same alpha. The hue is untouched. See issue #157. */
57
+ --glow-purple: rgba(59, 157, 255, 0.15);
53
58
  --ring: 0 0 0 3px rgba(59, 157, 255, 0.38);
54
59
  --grad-from: #3b9dff;
55
60
  --grad-to: #20dcf5;
@@ -58,9 +63,25 @@
58
63
  --purple: #1156b8;
59
64
  --purple-light: #1568d6;
60
65
  --purple-mid: #1560c8;
61
- --accent: #1568d6;
66
+ /* Deepened for WCAG AA on the raised grey panel and its wash: #1568d6 read
67
+ 4.54:1 on --surface-3 and 3.98:1 on --glow-purple over it; #005bc8 clears
68
+ both at 5.43:1 and 4.76:1. Both washed figures are at the 0.10 alpha below,
69
+ which is what this cell ships — an earlier version of this comment quoted
70
+ them at the 0.05 the wash briefly carried, which flattered both inks.
71
+ Light Ocean is the one light accent #96 never deepened, because it happened
72
+ to clear plain white. Same kind of move, smaller: hue and chroma unchanged
73
+ in OKLCH (H 258, C 0.185) and four points of lightness down, where #96's
74
+ two took about ten points and gave up 15.9% of the chroma on Phoenix and
75
+ 17.6% on Emerald. See issues #96 and #157. */
76
+ --accent: #005bc8;
62
77
  --accent-strong: var(--accent);
63
78
  --accent-contrast: #ffffff;
79
+ /* Back to 0.10, the alpha this cell carried before #157. #157 thinned it to
80
+ 0.05 while --accent was still #1568d6, because no alpha could carry that
81
+ blue over the grey panel. Deepening the ink is what closed the pair, so the
82
+ wash does not have to be the thing that gives. The rgb stays --purple-light
83
+ rather than the accent — in light a glow tints --purple-light, which
84
+ stories/signal-contrast.test.js holds. See issue #157. */
64
85
  --glow-purple: rgba(21, 104, 214, 0.10);
65
86
  --ring: 0 0 0 3px rgba(21, 104, 214, 0.28);
66
87
  --grad-from: #1568d6;
@@ -89,7 +110,9 @@
89
110
  --accent: #087a52;
90
111
  --accent-strong: var(--accent);
91
112
  --accent-contrast: #ffffff;
92
- --glow-purple: rgba(11, 156, 104, 0.10);
113
+ /* Thinned from 0.10 for WCAG AA, the same grey-panel pair as light Ocean and
114
+ from the same alpha. See issue #157. */
115
+ --glow-purple: rgba(11, 156, 104, 0.08);
93
116
  --ring: 0 0 0 3px rgba(11, 156, 104, 0.28);
94
117
  --grad-from: #0b9c68;
95
118
  --grad-to: #12b0a0;