@apliteni/apliteni-ui 0.9.1 → 0.11.4

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.
@@ -29,7 +29,7 @@
29
29
  /* ----------------------------------------------------------------------------
30
30
  * Toast — a floating, dismissible notification.
31
31
  * A status (colour) × style (surface) matrix, entirely token-driven:
32
- * status success · danger · warn · info · neutral → sets --toast-accent/-glow/-on/-solid
32
+ * status success · danger · warn · info · neutral → sets --toast-accent/-action-ink/-glow/-on/-solid
33
33
  * style soft (tinted) · solid (filled) · outline (bordered)
34
34
  * -------------------------------------------------------------------------- */
35
35
  .ui-toast {
@@ -51,12 +51,21 @@
51
51
  --toast-accent is the status as a line or a small mark, --toast-glow its wash,
52
52
  --toast-on the glyph ink on the accent circle. --toast-solid is separate on
53
53
  purpose: a full fill is a different job from a 22px circle, and it is chosen
54
- as a PAIR with --signal-solid-ink rather than against a global. See #149. */
55
- .ui-toast--success { --toast-accent: var(--green); --toast-glow: var(--glow-green); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-success); }
56
- .ui-toast--danger { --toast-accent: var(--pink); --toast-glow: var(--glow-pink); --toast-on: var(--danger-contrast); --toast-solid: var(--signal-solid-danger); }
57
- .ui-toast--warn { --toast-accent: var(--amber); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-warn); }
58
- .ui-toast--info { --toast-accent: var(--cyan); --toast-glow: var(--glow-cyan); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-info); }
59
- .ui-toast--neutral { --toast-accent: var(--muted); --toast-glow: var(--surface-2); --toast-on: var(--strong); --toast-solid: var(--signal-solid-neutral); }
54
+ as a PAIR with --signal-solid-ink rather than against a global. See #149.
55
+
56
+ --toast-action-ink is separate from --toast-accent for the same reason the
57
+ fill is: the accent is a graphic colour, sized for a 3px rule and a 22px
58
+ circle, and in the light theme it is not a colour text can be set in. The
59
+ trailing action is the one part of a toast that is BOTH the status colour and
60
+ a piece of text, so it takes the chip inks, which are the text-grade version
61
+ of the same five statuses. In the dark theme the two are the same value by
62
+ construction — --chip-success-ink IS --green there — so this splits the light
63
+ theme apart and leaves the dark one exactly as it was. */
64
+ .ui-toast--success { --toast-accent: var(--green); --toast-action-ink: var(--chip-success-ink); --toast-glow: var(--glow-green); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-success); }
65
+ .ui-toast--danger { --toast-accent: var(--pink); --toast-action-ink: var(--chip-danger-ink); --toast-glow: var(--glow-pink); --toast-on: var(--danger-contrast); --toast-solid: var(--signal-solid-danger); }
66
+ .ui-toast--warn { --toast-accent: var(--amber); --toast-action-ink: var(--chip-warn-ink); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-warn); }
67
+ .ui-toast--info { --toast-accent: var(--cyan); --toast-action-ink: var(--chip-info-ink); --toast-glow: var(--glow-cyan); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-info); }
68
+ .ui-toast--neutral { --toast-accent: var(--muted); --toast-action-ink: var(--muted); --toast-glow: var(--surface-2); --toast-on: var(--strong); --toast-solid: var(--signal-solid-neutral); }
60
69
 
61
70
  /* left status marker (soft + outline; solid is already a full fill) */
62
71
  .ui-toast--soft::before,
@@ -71,9 +80,16 @@
71
80
  .ui-toast__title { font-weight: var(--weight-medium); color: var(--strong); }
72
81
  .ui-toast__text { color: var(--dim); }
73
82
 
74
- /* trailing action ("Undo" / "Retry") */
75
- .ui-toast__action { flex: none; align-self: center; background: none; border: 0; padding: 4px 9px; border-radius: var(--radius-xs); font: inherit; font-weight: var(--weight-medium); color: var(--toast-accent); cursor: pointer; }
76
- .ui-toast__action:hover { background: color-mix(in srgb, var(--toast-accent) 14%, transparent); }
83
+ /* trailing action ("Undo" / "Retry") — status-coloured TEXT, so it takes the
84
+ text-grade ink rather than the accent.
85
+ The hover wash is the page background rather than the accent again: a second
86
+ accent tint on an already accent-tinted ground moved the ground TOWARDS the
87
+ ink and took every soft pair below the floor. --bg is white in the light theme
88
+ and near-black in the dark one, so one expression lightens the ground where
89
+ the ink is dark and darkens it where the ink is bright — the hovered pair is
90
+ at or above the resting pair in both. */
91
+ .ui-toast__action { flex: none; align-self: center; background: none; border: 0; padding: 4px 9px; border-radius: var(--radius-xs); font: inherit; font-weight: var(--weight-medium); color: var(--toast-action-ink); cursor: pointer; }
92
+ .ui-toast__action:hover { background: color-mix(in srgb, var(--bg) 55%, transparent); }
77
93
 
78
94
  .ui-toast__close { flex: none; align-self: flex-start; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px; display: grid; place-items: center; }
79
95
  .ui-toast__close svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 1.8; }
@@ -108,7 +124,10 @@
108
124
  .ui-toast--solid .ui-toast__text { color: color-mix(in srgb, var(--toast-ink) 90%, transparent); }
109
125
  .ui-toast--solid .ui-toast__icon { background: color-mix(in srgb, var(--toast-ink) 22%, transparent); color: var(--toast-ink); }
110
126
  .ui-toast--solid .ui-toast__action { color: var(--toast-ink); }
111
- .ui-toast--solid .ui-toast__action:hover { background: color-mix(in srgb, var(--toast-ink) 18%, transparent); }
127
+ /* Hover darkens the FILL rather than lightening the ink. Washing the ink over
128
+ itself was the one surface #149 did not reach, and it worked against the pair
129
+ it was drawn from: it pulled the fill towards the ink it has to clear. */
130
+ .ui-toast--solid .ui-toast__action:hover { background: color-mix(in srgb, var(--signal-contrast) 14%, transparent); }
112
131
  .ui-toast--solid .ui-toast__close { color: color-mix(in srgb, var(--toast-ink) 68%, transparent); }
113
132
  .ui-toast--solid .ui-toast__close:hover { color: var(--toast-ink); }
114
133
  .ui-toast--solid .ui-toast__timer { background: color-mix(in srgb, var(--toast-ink) 55%, transparent); }
@@ -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; }