current_scope 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +377 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/current_scope/application.js +229 -0
- data/app/assets/stylesheets/current_scope/application.css +917 -0
- data/app/controllers/current_scope/application_controller.rb +96 -0
- data/app/controllers/current_scope/events_controller.rb +14 -0
- data/app/controllers/current_scope/role_assignments_controller.rb +190 -0
- data/app/controllers/current_scope/roles_controller.rb +256 -0
- data/app/controllers/current_scope/scoped_role_assignments_controller.rb +165 -0
- data/app/controllers/current_scope/subjects_controller.rb +75 -0
- data/app/helpers/current_scope/application_helper.rb +261 -0
- data/app/models/concerns/current_scope/storable_keys.rb +101 -0
- data/app/models/current_scope/application_record.rb +5 -0
- data/app/models/current_scope/current.rb +74 -0
- data/app/models/current_scope/event.rb +136 -0
- data/app/models/current_scope/role.rb +106 -0
- data/app/models/current_scope/role_assignment.rb +42 -0
- data/app/models/current_scope/role_permission.rb +9 -0
- data/app/models/current_scope/scoped_role_assignment.rb +98 -0
- data/app/views/current_scope/events/index.html.erb +41 -0
- data/app/views/current_scope/roles/edit.html.erb +229 -0
- data/app/views/current_scope/roles/index.html.erb +55 -0
- data/app/views/current_scope/roles/members.html.erb +114 -0
- data/app/views/current_scope/roles/new.html.erb +19 -0
- data/app/views/current_scope/scoped_role_assignments/new.html.erb +144 -0
- data/app/views/current_scope/shared/access_denied.html.erb +30 -0
- data/app/views/current_scope/subjects/index.html.erb +124 -0
- data/app/views/layouts/current_scope/application.html.erb +56 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20260710000001_create_current_scope_tables.rb +31 -0
- data/db/migrate/20260710000002_create_current_scope_events.rb +32 -0
- data/db/migrate/20260714000001_add_description_to_current_scope_roles.rb +5 -0
- data/db/migrate/20260805000001_widen_current_scope_polymorphic_ids.rb +165 -0
- data/lib/current_scope/configuration.rb +613 -0
- data/lib/current_scope/context.rb +41 -0
- data/lib/current_scope/engine.rb +202 -0
- data/lib/current_scope/gating_reflection.rb +113 -0
- data/lib/current_scope/gating_tripwire.rb +84 -0
- data/lib/current_scope/grant_diagnosis.rb +216 -0
- data/lib/current_scope/guard.rb +750 -0
- data/lib/current_scope/mutation_guard.rb +90 -0
- data/lib/current_scope/parent_chain.rb +397 -0
- data/lib/current_scope/permission_catalog.rb +146 -0
- data/lib/current_scope/permission_grid.rb +132 -0
- data/lib/current_scope/permissions.rb +94 -0
- data/lib/current_scope/resolver.rb +669 -0
- data/lib/current_scope/schema_guard.rb +223 -0
- data/lib/current_scope/scopeable.rb +38 -0
- data/lib/current_scope/sod_preflight.rb +380 -0
- data/lib/current_scope/test_helpers.rb +53 -0
- data/lib/current_scope/version.rb +3 -0
- data/lib/current_scope.rb +432 -0
- data/lib/generators/current_scope/install/install_generator.rb +114 -0
- data/lib/generators/current_scope/install/templates/initializer.rb +175 -0
- data/lib/tasks/current_scope_tasks.rake +454 -0
- metadata +123 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
/* CurrentScope — the mounted management UI: an authorization console.
|
|
2
|
+
*
|
|
3
|
+
* An opinionated, self-contained admin dashboard: a sidebar shell, a
|
|
4
|
+
* precision-monochrome surface with one cobalt signature accent (the product's
|
|
5
|
+
* "ledger ink"), monospace for the technical layer (permission keys, action
|
|
6
|
+
* columns, event types), and first-class light + dark themes. System stack
|
|
7
|
+
* only — no web fonts, no build step, CSP-safe.
|
|
8
|
+
*
|
|
9
|
+
* Theming: tokens live on :root. Light is the default; system dark applies via
|
|
10
|
+
* prefers-color-scheme; an explicit choice is set as html[data-cs-theme] (from
|
|
11
|
+
* the current_scope_theme cookie, so there's no flash). Retheme by redefining
|
|
12
|
+
* the --cs-* custom properties; restructure by overriding the .cs-* classes.
|
|
13
|
+
*
|
|
14
|
+
* The token defaults sit in a @layer so a host's own (unlayered) :root override
|
|
15
|
+
* always wins — otherwise the data-cs-theme attribute selectors below would
|
|
16
|
+
* outweigh a plain :root { --cs-accent: … } in the host stylesheet.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
@layer current_scope.tokens {
|
|
20
|
+
:root {
|
|
21
|
+
color-scheme: light;
|
|
22
|
+
|
|
23
|
+
/* Surfaces — cool near-neutral, ink on paper. */
|
|
24
|
+
--cs-bg: oklch(0.992 0.002 264);
|
|
25
|
+
--cs-surface: oklch(0.975 0.003 264);
|
|
26
|
+
--cs-surface-2: oklch(0.955 0.004 264);
|
|
27
|
+
--cs-line: oklch(0.905 0.005 264);
|
|
28
|
+
--cs-line-strong: oklch(0.80 0.008 264);
|
|
29
|
+
|
|
30
|
+
/* Ink. */
|
|
31
|
+
--cs-ink: oklch(0.26 0.014 264);
|
|
32
|
+
--cs-ink-2: oklch(0.18 0.02 264); /* headings */
|
|
33
|
+
--cs-muted: oklch(0.475 0.015 264); /* ~5.4:1 on --cs-bg */
|
|
34
|
+
|
|
35
|
+
/* Cobalt signature accent — used with discipline (active nav, primary
|
|
36
|
+
action, focus, granted state). Rare on purpose. */
|
|
37
|
+
--cs-accent: oklch(0.515 0.163 262);
|
|
38
|
+
--cs-accent-strong: oklch(0.44 0.17 262);
|
|
39
|
+
--cs-accent-wash: oklch(0.955 0.03 262);
|
|
40
|
+
--cs-on-accent: oklch(0.99 0.01 262);
|
|
41
|
+
|
|
42
|
+
/* Semantics. */
|
|
43
|
+
--cs-danger: oklch(0.53 0.19 25);
|
|
44
|
+
--cs-danger-line: oklch(0.82 0.09 25);
|
|
45
|
+
--cs-danger-wash: oklch(0.965 0.018 25);
|
|
46
|
+
--cs-ok: oklch(0.58 0.12 150);
|
|
47
|
+
--cs-ok-line: oklch(0.82 0.07 150);
|
|
48
|
+
--cs-ok-wash: oklch(0.962 0.02 150);
|
|
49
|
+
|
|
50
|
+
--cs-radius: 6px;
|
|
51
|
+
--cs-radius-sm: 4px;
|
|
52
|
+
--cs-shadow: 0 1px 2px oklch(0.2 0.03 264 / 0.06), 0 4px 12px oklch(0.2 0.03 264 / 0.05);
|
|
53
|
+
--cs-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
54
|
+
--cs-mono: ui-monospace, "SF Mono", "SFMono-Regular", Menlo, Consolas, monospace;
|
|
55
|
+
--cs-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
56
|
+
|
|
57
|
+
--cs-sidebar-w: 15.5rem;
|
|
58
|
+
--cs-topbar-h: 3.25rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Dark tokens, shared by system-dark (no explicit choice) and an explicit
|
|
62
|
+
data-cs-theme="dark". */
|
|
63
|
+
@media (prefers-color-scheme: dark) {
|
|
64
|
+
:root:not([data-cs-theme]) {
|
|
65
|
+
color-scheme: dark;
|
|
66
|
+
--cs-bg: oklch(0.172 0.008 264);
|
|
67
|
+
--cs-surface: oklch(0.208 0.009 264);
|
|
68
|
+
--cs-surface-2: oklch(0.238 0.010 264);
|
|
69
|
+
--cs-line: oklch(0.305 0.010 264);
|
|
70
|
+
--cs-line-strong: oklch(0.41 0.013 264);
|
|
71
|
+
--cs-ink: oklch(0.905 0.008 264);
|
|
72
|
+
--cs-ink-2: oklch(0.975 0.006 264);
|
|
73
|
+
--cs-muted: oklch(0.68 0.013 264);
|
|
74
|
+
--cs-accent: oklch(0.70 0.15 262);
|
|
75
|
+
--cs-accent-strong: oklch(0.78 0.14 262);
|
|
76
|
+
--cs-accent-wash: oklch(0.30 0.07 262);
|
|
77
|
+
--cs-on-accent: oklch(0.16 0.03 262);
|
|
78
|
+
--cs-danger: oklch(0.68 0.17 25);
|
|
79
|
+
--cs-danger-line: oklch(0.42 0.11 25);
|
|
80
|
+
--cs-danger-wash: oklch(0.27 0.06 25);
|
|
81
|
+
--cs-ok: oklch(0.72 0.13 150);
|
|
82
|
+
--cs-ok-line: oklch(0.40 0.08 150);
|
|
83
|
+
--cs-ok-wash: oklch(0.26 0.05 150);
|
|
84
|
+
--cs-shadow: 0 1px 2px oklch(0 0 0 / 0.4), 0 6px 16px oklch(0 0 0 / 0.35);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* NB: these dark tokens intentionally duplicate the @media block above — CSS
|
|
89
|
+
can't share a declaration block between a media query and an attribute
|
|
90
|
+
selector without a preprocessor. Keep the two in sync on any token change. */
|
|
91
|
+
:root[data-cs-theme="dark"] {
|
|
92
|
+
color-scheme: dark;
|
|
93
|
+
--cs-bg: oklch(0.172 0.008 264);
|
|
94
|
+
--cs-surface: oklch(0.208 0.009 264);
|
|
95
|
+
--cs-surface-2: oklch(0.238 0.010 264);
|
|
96
|
+
--cs-line: oklch(0.305 0.010 264);
|
|
97
|
+
--cs-line-strong: oklch(0.41 0.013 264);
|
|
98
|
+
--cs-ink: oklch(0.905 0.008 264);
|
|
99
|
+
--cs-ink-2: oklch(0.975 0.006 264);
|
|
100
|
+
--cs-muted: oklch(0.68 0.013 264);
|
|
101
|
+
--cs-accent: oklch(0.70 0.15 262);
|
|
102
|
+
--cs-accent-strong: oklch(0.78 0.14 262);
|
|
103
|
+
--cs-accent-wash: oklch(0.30 0.07 262);
|
|
104
|
+
--cs-on-accent: oklch(0.16 0.03 262);
|
|
105
|
+
--cs-danger: oklch(0.68 0.17 25);
|
|
106
|
+
--cs-danger-line: oklch(0.42 0.11 25);
|
|
107
|
+
--cs-danger-wash: oklch(0.27 0.06 25);
|
|
108
|
+
--cs-ok: oklch(0.72 0.13 150);
|
|
109
|
+
--cs-ok-line: oklch(0.40 0.08 150);
|
|
110
|
+
--cs-ok-wash: oklch(0.26 0.05 150);
|
|
111
|
+
--cs-shadow: 0 1px 2px oklch(0 0 0 / 0.4), 0 6px 16px oklch(0 0 0 / 0.35);
|
|
112
|
+
}
|
|
113
|
+
} /* @layer current_scope.tokens */
|
|
114
|
+
|
|
115
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
116
|
+
|
|
117
|
+
body {
|
|
118
|
+
margin: 0;
|
|
119
|
+
background: var(--cs-bg);
|
|
120
|
+
color: var(--cs-ink);
|
|
121
|
+
font-family: var(--cs-font);
|
|
122
|
+
font-size: 0.9375rem;
|
|
123
|
+
line-height: 1.55;
|
|
124
|
+
-webkit-font-smoothing: antialiased;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
h1 { font-size: 1.4rem; font-weight: 660; letter-spacing: -0.02em; color: var(--cs-ink-2); margin: 0 0 0.375rem; text-wrap: balance; }
|
|
128
|
+
h2 { font-size: 1.0625rem; font-weight: 600; color: var(--cs-ink-2); margin: 2rem 0 0.75rem; text-wrap: balance; }
|
|
129
|
+
|
|
130
|
+
a { color: var(--cs-accent); text-decoration-thickness: 1px; text-underline-offset: 3px; }
|
|
131
|
+
a:hover { color: var(--cs-accent-strong); }
|
|
132
|
+
|
|
133
|
+
:focus-visible { outline: 2px solid var(--cs-accent); outline-offset: 2px; border-radius: 3px; }
|
|
134
|
+
|
|
135
|
+
/* Bypass block (WCAG 2.4.1): off-canvas until keyboard focus lands on it,
|
|
136
|
+
* then a small accent pill in the top-left, above the sticky sidebar. */
|
|
137
|
+
.cs-skip-link {
|
|
138
|
+
position: absolute;
|
|
139
|
+
top: 0.5rem;
|
|
140
|
+
left: 0.5rem;
|
|
141
|
+
z-index: 10;
|
|
142
|
+
padding: 0.5rem 0.875rem;
|
|
143
|
+
background: var(--cs-accent);
|
|
144
|
+
color: var(--cs-on-accent);
|
|
145
|
+
border-radius: 6px;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
text-decoration: none;
|
|
148
|
+
transform: translateY(calc(-100% - 1rem));
|
|
149
|
+
}
|
|
150
|
+
.cs-skip-link:focus { transform: none; }
|
|
151
|
+
|
|
152
|
+
/* The skip-link target: focusable for the jump, but never shows a ring of
|
|
153
|
+
* its own — the content it reveals is the feedback. */
|
|
154
|
+
#cs-main-content:focus-visible { outline: none; }
|
|
155
|
+
|
|
156
|
+
/* ---- Shell ------------------------------------------------------------ *
|
|
157
|
+
* A left sidebar (wordmark + section nav) and a scrollable main column with
|
|
158
|
+
* a sticky topbar. The dense grid lives inside main.
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
.cs-shell {
|
|
162
|
+
display: grid;
|
|
163
|
+
grid-template-columns: var(--cs-sidebar-w) minmax(0, 1fr);
|
|
164
|
+
min-height: 100vh;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.cs-sidebar {
|
|
168
|
+
position: sticky;
|
|
169
|
+
top: 0;
|
|
170
|
+
align-self: start;
|
|
171
|
+
height: 100vh;
|
|
172
|
+
display: flex;
|
|
173
|
+
flex-direction: column;
|
|
174
|
+
gap: 0.25rem;
|
|
175
|
+
padding: 1.125rem 0.75rem;
|
|
176
|
+
background: var(--cs-surface);
|
|
177
|
+
border-right: 1px solid var(--cs-line);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.cs-brand {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
gap: 0.5rem;
|
|
184
|
+
padding: 0.25rem 0.5rem 1rem;
|
|
185
|
+
font-weight: 660;
|
|
186
|
+
font-size: 0.95rem;
|
|
187
|
+
letter-spacing: -0.01em;
|
|
188
|
+
color: var(--cs-ink-2);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* A tiny 2x2 "grid" mark — the permission matrix, distilled. */
|
|
192
|
+
.cs-brand-mark {
|
|
193
|
+
width: 1.125rem;
|
|
194
|
+
height: 1.125rem;
|
|
195
|
+
border-radius: 4px;
|
|
196
|
+
flex: none;
|
|
197
|
+
background:
|
|
198
|
+
linear-gradient(var(--cs-accent) 0 0) 0 0 / 45% 45%,
|
|
199
|
+
linear-gradient(var(--cs-accent) 0 0) 100% 0 / 45% 45%,
|
|
200
|
+
linear-gradient(var(--cs-line-strong) 0 0) 0 100% / 45% 45%,
|
|
201
|
+
linear-gradient(var(--cs-accent) 0 0) 100% 100% / 45% 45%;
|
|
202
|
+
background-repeat: no-repeat;
|
|
203
|
+
box-shadow: inset 0 0 0 1px var(--cs-line);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.cs-nav { display: flex; flex-direction: column; gap: 0.125rem; }
|
|
207
|
+
|
|
208
|
+
.cs-nav a {
|
|
209
|
+
position: relative;
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
gap: 0.5rem;
|
|
213
|
+
padding: 0.4375rem 0.625rem;
|
|
214
|
+
border-radius: var(--cs-radius-sm);
|
|
215
|
+
color: var(--cs-muted);
|
|
216
|
+
text-decoration: none;
|
|
217
|
+
font-weight: 500;
|
|
218
|
+
font-size: 0.875rem;
|
|
219
|
+
transition: background 120ms var(--cs-ease), color 120ms var(--cs-ease);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.cs-nav a:hover { color: var(--cs-ink); background: var(--cs-surface-2); }
|
|
223
|
+
|
|
224
|
+
.cs-nav a[aria-current="page"] {
|
|
225
|
+
color: var(--cs-ink-2);
|
|
226
|
+
background: var(--cs-accent-wash);
|
|
227
|
+
font-weight: 560;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* the signature accent rail on the active item */
|
|
231
|
+
.cs-nav a[aria-current="page"]::before {
|
|
232
|
+
content: "";
|
|
233
|
+
position: absolute;
|
|
234
|
+
left: -0.75rem;
|
|
235
|
+
top: 0.375rem;
|
|
236
|
+
bottom: 0.375rem;
|
|
237
|
+
width: 3px;
|
|
238
|
+
border-radius: 0 3px 3px 0;
|
|
239
|
+
background: var(--cs-accent);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.cs-sidebar-foot { margin-top: auto; padding: 0.5rem; color: var(--cs-muted); font-size: 0.75rem; }
|
|
243
|
+
|
|
244
|
+
/* ---- Topbar ----------------------------------------------------------- */
|
|
245
|
+
|
|
246
|
+
.cs-topbar {
|
|
247
|
+
position: sticky;
|
|
248
|
+
top: 0;
|
|
249
|
+
z-index: 5;
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
min-height: var(--cs-topbar-h);
|
|
253
|
+
gap: 1rem;
|
|
254
|
+
padding: 0.75rem 1.75rem;
|
|
255
|
+
background: color-mix(in oklch, var(--cs-bg) 82%, transparent);
|
|
256
|
+
backdrop-filter: saturate(1.4) blur(8px);
|
|
257
|
+
border-bottom: 1px solid var(--cs-line);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.cs-topbar-title { font-weight: 600; font-size: 0.9375rem; color: var(--cs-ink-2); }
|
|
261
|
+
.cs-topbar-spacer { flex: 1; }
|
|
262
|
+
|
|
263
|
+
.cs-badge {
|
|
264
|
+
display: inline-flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
gap: 0.375rem;
|
|
267
|
+
padding: 0.1875rem 0.5rem;
|
|
268
|
+
border: 1px solid var(--cs-line-strong);
|
|
269
|
+
border-radius: 999px;
|
|
270
|
+
font-size: 0.75rem;
|
|
271
|
+
color: var(--cs-muted);
|
|
272
|
+
font-family: var(--cs-mono);
|
|
273
|
+
}
|
|
274
|
+
.cs-badge::before { content: ""; width: 6px; height: 6px; border-radius: 999px; background: var(--cs-ok); }
|
|
275
|
+
|
|
276
|
+
/* Theme toggle — sun/moon glyphs swapped by theme via CSS. */
|
|
277
|
+
.cs-theme-toggle {
|
|
278
|
+
display: inline-grid;
|
|
279
|
+
place-items: center;
|
|
280
|
+
width: 2rem;
|
|
281
|
+
height: 2rem;
|
|
282
|
+
padding: 0;
|
|
283
|
+
border: 1px solid var(--cs-line-strong);
|
|
284
|
+
border-radius: var(--cs-radius-sm);
|
|
285
|
+
background: var(--cs-surface);
|
|
286
|
+
color: var(--cs-ink);
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
font-size: 0.95rem;
|
|
289
|
+
line-height: 1;
|
|
290
|
+
}
|
|
291
|
+
.cs-theme-toggle:hover { border-color: var(--cs-ink); background: var(--cs-surface-2); }
|
|
292
|
+
.cs-theme-toggle .cs-icon-dark { display: none; }
|
|
293
|
+
.cs-theme-toggle .cs-icon-light { display: block; }
|
|
294
|
+
:root[data-cs-theme="dark"] .cs-theme-toggle .cs-icon-dark { display: block; }
|
|
295
|
+
:root[data-cs-theme="dark"] .cs-theme-toggle .cs-icon-light { display: none; }
|
|
296
|
+
@media (prefers-color-scheme: dark) {
|
|
297
|
+
:root:not([data-cs-theme]) .cs-theme-toggle .cs-icon-dark { display: block; }
|
|
298
|
+
:root:not([data-cs-theme]) .cs-theme-toggle .cs-icon-light { display: none; }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* ---- Main + flashes --------------------------------------------------- */
|
|
302
|
+
|
|
303
|
+
.cs-main { min-width: 0; }
|
|
304
|
+
.cs-content { padding: 1.75rem; max-width: 74rem; }
|
|
305
|
+
.cs-lede { color: var(--cs-muted); font-size: 0.875rem; margin: 0 0 1.5rem; max-width: 64ch; }
|
|
306
|
+
|
|
307
|
+
.cs-flash {
|
|
308
|
+
display: flex;
|
|
309
|
+
gap: 0.5rem;
|
|
310
|
+
margin: 1rem 1.75rem -0.5rem;
|
|
311
|
+
padding: 0.5rem 0.875rem;
|
|
312
|
+
border: 1px solid var(--cs-line);
|
|
313
|
+
border-radius: var(--cs-radius);
|
|
314
|
+
font-size: 0.875rem;
|
|
315
|
+
}
|
|
316
|
+
.cs-flash--notice { border-color: var(--cs-ok-line); background: var(--cs-ok-wash); }
|
|
317
|
+
.cs-flash--alert { border-color: var(--cs-danger-line); background: var(--cs-danger-wash); }
|
|
318
|
+
|
|
319
|
+
/* In-view flash aliases (form errors use these). */
|
|
320
|
+
.cs-notice, .cs-alert {
|
|
321
|
+
display: block;
|
|
322
|
+
padding: 0.5rem 0.875rem;
|
|
323
|
+
border: 1px solid var(--cs-line);
|
|
324
|
+
border-radius: var(--cs-radius);
|
|
325
|
+
font-size: 0.875rem;
|
|
326
|
+
margin: 0 0 1rem;
|
|
327
|
+
max-width: 30rem;
|
|
328
|
+
}
|
|
329
|
+
.cs-notice { border-color: var(--cs-ok-line); background: var(--cs-ok-wash); }
|
|
330
|
+
.cs-alert { border-color: var(--cs-danger-line); background: var(--cs-danger-wash); }
|
|
331
|
+
|
|
332
|
+
.cs-hint { color: var(--cs-muted); font-size: 0.8125rem; max-width: 68ch; }
|
|
333
|
+
.cs-subtle { color: var(--cs-muted); font-size: 0.8125rem; margin-top: 0.125rem; }
|
|
334
|
+
|
|
335
|
+
/* ---- Toolbar (page actions) ------------------------------------------- */
|
|
336
|
+
|
|
337
|
+
.cs-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin: 0 0 1.25rem; }
|
|
338
|
+
.cs-toolbar .cs-spacer { flex: 1; }
|
|
339
|
+
|
|
340
|
+
/* ---- Filter + bulk (subjects) ----------------------------------------- */
|
|
341
|
+
|
|
342
|
+
.cs-filter { min-width: min(22rem, 100%); }
|
|
343
|
+
|
|
344
|
+
/* The subjects search form: input + Search + Clear on one line, the input
|
|
345
|
+
taking the free space; wraps on a narrow toolbar. */
|
|
346
|
+
.cs-search { display: flex; align-items: center; gap: 0.5rem; flex: 1 1 22rem; min-width: 0; }
|
|
347
|
+
.cs-search .cs-filter { flex: 1; min-width: 0; }
|
|
348
|
+
|
|
349
|
+
.cs-bulk {
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
gap: 0.75rem;
|
|
353
|
+
margin: 0 0 1rem;
|
|
354
|
+
padding: 0.5rem 0.875rem;
|
|
355
|
+
border: 1px solid var(--cs-accent);
|
|
356
|
+
border-radius: var(--cs-radius);
|
|
357
|
+
background: var(--cs-accent-wash);
|
|
358
|
+
font-size: 0.875rem;
|
|
359
|
+
}
|
|
360
|
+
/* The bar sets display:flex, which otherwise defeats the [hidden] attribute the
|
|
361
|
+
JS toggles — keep it hidden until a subject is selected. */
|
|
362
|
+
.cs-bulk[hidden] { display: none; }
|
|
363
|
+
.cs-bulk .cs-spacer { flex: 1; }
|
|
364
|
+
.cs-bulk-org { display: inline-flex; align-items: center; gap: 0.375rem; margin: 0; }
|
|
365
|
+
.cs-bulk-org select { padding: 0.3125rem 0.5rem; }
|
|
366
|
+
|
|
367
|
+
/* Select column — narrow, centered checkboxes. */
|
|
368
|
+
.cs-select-cell { width: 1%; text-align: center; padding-inline: 0.75rem; }
|
|
369
|
+
|
|
370
|
+
.cs-filter-empty { color: var(--cs-muted); font-size: 0.875rem; margin: 0.75rem 0; }
|
|
371
|
+
|
|
372
|
+
/* ---- Cards ------------------------------------------------------------ */
|
|
373
|
+
|
|
374
|
+
.cs-card {
|
|
375
|
+
background: var(--cs-surface);
|
|
376
|
+
border: 1px solid var(--cs-line);
|
|
377
|
+
border-radius: var(--cs-radius);
|
|
378
|
+
box-shadow: var(--cs-shadow);
|
|
379
|
+
}
|
|
380
|
+
.cs-card:not(.cs-card--flush) > * { padding-inline: 1.125rem; }
|
|
381
|
+
|
|
382
|
+
/* The engine's front-door 403 — rendered standalone, without the console shell
|
|
383
|
+
(a sidebar of links this subject can't open would read as "you're in" and
|
|
384
|
+
then refuse every click). Only the centring is new; the surface, type and
|
|
385
|
+
theme tokens come from .cs-card and the base rules above. */
|
|
386
|
+
.cs-denied {
|
|
387
|
+
max-width: 46rem;
|
|
388
|
+
margin: 12vh auto 0;
|
|
389
|
+
padding-block: 1.5rem;
|
|
390
|
+
}
|
|
391
|
+
.cs-denied p { margin: 0 0 0.75rem; max-width: 68ch; }
|
|
392
|
+
/* Flush cards hold data tables. Let them scroll horizontally inside the card so
|
|
393
|
+
a wide table never overflows the page. On a wide screen the table fills the
|
|
394
|
+
card (base width:100%) and scoped-role chips wrap in their own cell — all
|
|
395
|
+
visible, no scroll. On a narrow screen the table switches to its natural width
|
|
396
|
+
(see the max-width:820px block) so columns stay usable and the row height
|
|
397
|
+
stays normal, scrolling horizontally instead of crushing into tall rows. */
|
|
398
|
+
.cs-card--flush { padding: 0; overflow-x: auto; }
|
|
399
|
+
.cs-card--flush .cs-table th:first-child,
|
|
400
|
+
.cs-card--flush .cs-table td:first-child { padding-left: 1.125rem; }
|
|
401
|
+
.cs-card--flush .cs-table th:last-child,
|
|
402
|
+
.cs-card--flush .cs-table td:last-child { padding-right: 1.125rem; }
|
|
403
|
+
|
|
404
|
+
/* ---- Tables ----------------------------------------------------------- */
|
|
405
|
+
|
|
406
|
+
.cs-table { border-collapse: collapse; width: 100%; }
|
|
407
|
+
|
|
408
|
+
.cs-table th {
|
|
409
|
+
text-align: left;
|
|
410
|
+
font-size: 0.7rem;
|
|
411
|
+
font-weight: 600;
|
|
412
|
+
letter-spacing: 0.04em;
|
|
413
|
+
text-transform: uppercase;
|
|
414
|
+
color: var(--cs-muted);
|
|
415
|
+
background: var(--cs-surface-2);
|
|
416
|
+
border-bottom: 1px solid var(--cs-line);
|
|
417
|
+
padding: 0.5rem 0.75rem;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.cs-table td {
|
|
421
|
+
border-bottom: 1px solid var(--cs-line);
|
|
422
|
+
padding: 0.625rem 0.75rem;
|
|
423
|
+
vertical-align: middle;
|
|
424
|
+
}
|
|
425
|
+
.cs-table tbody tr:last-child td { border-bottom: none; }
|
|
426
|
+
.cs-table tbody tr:hover td { background: var(--cs-surface-2); }
|
|
427
|
+
.cs-mono, .cs-table .cs-mono { font-family: var(--cs-mono); font-size: 0.8125rem; }
|
|
428
|
+
|
|
429
|
+
/* ---- Permission grid — the hero surface -------------------------------
|
|
430
|
+
* A true matrix: fixed action columns across the top, one row per controller,
|
|
431
|
+
* a sticky mono controller column down the side. Columns stay ABSOLUTE — a
|
|
432
|
+
* controller that doesn't route a column's actions shows a blank cell, never a
|
|
433
|
+
* shifted one. The signature move is the GRANTED state: a checked cell lights
|
|
434
|
+
* up in the cobalt wash, so access reads at a glance across the whole grid.
|
|
435
|
+
*/
|
|
436
|
+
|
|
437
|
+
.cs-grid-wrap {
|
|
438
|
+
overflow-x: auto;
|
|
439
|
+
border: 1px solid var(--cs-line);
|
|
440
|
+
border-radius: var(--cs-radius);
|
|
441
|
+
box-shadow: var(--cs-shadow);
|
|
442
|
+
background: var(--cs-surface);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.cs-grid { border-collapse: separate; border-spacing: 0; width: 100%; }
|
|
446
|
+
|
|
447
|
+
/* Column headers — the fixed action/group names. Sticky to the TOP OF THE GRID
|
|
448
|
+
WRAPPER (its sticky containing block, because .cs-grid-wrap sets overflow-x) —
|
|
449
|
+
so top:0, NOT the topbar height: any positive offset here pushes the header
|
|
450
|
+
down over the first data row instead of pinning it under the page topbar. */
|
|
451
|
+
.cs-grid thead th {
|
|
452
|
+
position: sticky;
|
|
453
|
+
top: 0;
|
|
454
|
+
z-index: 2;
|
|
455
|
+
background: var(--cs-surface-2);
|
|
456
|
+
color: var(--cs-muted);
|
|
457
|
+
font-family: var(--cs-mono);
|
|
458
|
+
font-size: 0.7rem;
|
|
459
|
+
font-weight: 600;
|
|
460
|
+
letter-spacing: 0.03em;
|
|
461
|
+
text-transform: uppercase;
|
|
462
|
+
text-align: center;
|
|
463
|
+
white-space: nowrap;
|
|
464
|
+
padding: 0.5rem 0.625rem;
|
|
465
|
+
border-bottom: 1px solid var(--cs-line);
|
|
466
|
+
border-right: 1px solid var(--cs-line);
|
|
467
|
+
}
|
|
468
|
+
.cs-grid thead th:last-child { border-right: none; }
|
|
469
|
+
.cs-grid thead th.cs-grid-corner {
|
|
470
|
+
left: 0;
|
|
471
|
+
z-index: 3;
|
|
472
|
+
text-align: left;
|
|
473
|
+
background: var(--cs-surface-2);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/* Sticky controller column. */
|
|
477
|
+
.cs-grid th[scope="row"] {
|
|
478
|
+
position: sticky;
|
|
479
|
+
left: 0;
|
|
480
|
+
z-index: 1;
|
|
481
|
+
background: var(--cs-surface);
|
|
482
|
+
color: var(--cs-ink-2);
|
|
483
|
+
text-align: left;
|
|
484
|
+
white-space: nowrap;
|
|
485
|
+
border-right: 1px solid var(--cs-line);
|
|
486
|
+
border-bottom: 1px solid var(--cs-line);
|
|
487
|
+
padding: 0.375rem 0.75rem;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.cs-grid td {
|
|
491
|
+
border-bottom: 1px solid var(--cs-line);
|
|
492
|
+
border-right: 1px solid var(--cs-line);
|
|
493
|
+
padding: 0.375rem;
|
|
494
|
+
text-align: center;
|
|
495
|
+
transition: background 120ms var(--cs-ease);
|
|
496
|
+
}
|
|
497
|
+
.cs-grid td:last-child { border-right: none; }
|
|
498
|
+
.cs-grid tr:last-child th[scope="row"],
|
|
499
|
+
.cs-grid tr:last-child td { border-bottom: none; }
|
|
500
|
+
.cs-grid tbody tr:hover th[scope="row"] { background: var(--cs-surface-2); }
|
|
501
|
+
.cs-grid tbody tr:hover td:not(.cs-cell-blank):not(:has(input:checked)) { background: var(--cs-surface-2); }
|
|
502
|
+
|
|
503
|
+
.cs-grid label { display: inline-flex; align-items: center; justify-content: center; margin: 0; cursor: pointer; }
|
|
504
|
+
|
|
505
|
+
/* A blank cell — the controller doesn't route this column's actions. A faint
|
|
506
|
+
marker so the gap reads as "not applicable", not "unchecked". */
|
|
507
|
+
.cs-grid td.cs-cell-blank::after { content: "·"; color: var(--cs-line-strong); font-size: 1rem; }
|
|
508
|
+
|
|
509
|
+
/* Per-row master toggle ("enable all in this controller") — sits in the sticky
|
|
510
|
+
row header, reading as a select-all next to the mono controller name. */
|
|
511
|
+
.cs-grid .cs-row-all {
|
|
512
|
+
display: inline-flex;
|
|
513
|
+
align-items: center;
|
|
514
|
+
gap: 0.5rem;
|
|
515
|
+
cursor: pointer;
|
|
516
|
+
font-family: var(--cs-mono);
|
|
517
|
+
font-size: 0.8125rem;
|
|
518
|
+
font-weight: 600;
|
|
519
|
+
color: var(--cs-ink-2);
|
|
520
|
+
}
|
|
521
|
+
.cs-grid .cs-row-all input { flex: none; margin: 0; }
|
|
522
|
+
.cs-grid th[scope="row"]:has([data-cs-row-all]:checked) { background: var(--cs-accent-wash); }
|
|
523
|
+
|
|
524
|
+
/* GRANTED: the cell lights up in cobalt. */
|
|
525
|
+
.cs-grid td:has(input:checked) { background: var(--cs-accent-wash); }
|
|
526
|
+
|
|
527
|
+
/* PARTIAL: some-but-not-all of the group is granted. JS also sets the checkbox
|
|
528
|
+
indeterminate, but this keys off the data attribute so the "partial, not
|
|
529
|
+
ungranted" state is still visible with JS disabled. */
|
|
530
|
+
.cs-grid td:has(input[data-cs-partial="true"]) { background: var(--cs-accent-wash); }
|
|
531
|
+
.cs-grid input[data-cs-partial="true"] { outline: 2px dashed var(--cs-accent); outline-offset: 1px; }
|
|
532
|
+
|
|
533
|
+
/* UNGATED (provable): the gate never runs on this controller, so its granted
|
|
534
|
+
routed actions are inert. The badge is advisory — marking is not disabling
|
|
535
|
+
(KTD-8/R5): nothing grayed, nothing disabled. It sits under the controller
|
|
536
|
+
name in the sticky header, wrapping normally inside the nowrap column. */
|
|
537
|
+
/* #76 — declared skip with a reason (intentional; not the unexplained warning). */
|
|
538
|
+
.cs-declared-skip-badge {
|
|
539
|
+
display: block;
|
|
540
|
+
max-width: 19rem;
|
|
541
|
+
margin-top: 0.25rem;
|
|
542
|
+
padding: 0.25rem 0.4375rem;
|
|
543
|
+
border: 1px solid var(--cs-line-strong);
|
|
544
|
+
border-radius: var(--cs-radius-sm);
|
|
545
|
+
background: var(--cs-surface-2);
|
|
546
|
+
color: var(--cs-ink);
|
|
547
|
+
font-family: var(--cs-font);
|
|
548
|
+
font-size: 0.6875rem;
|
|
549
|
+
font-weight: 400; /* th is bold; reset so only <strong> is emphasized */
|
|
550
|
+
line-height: 1.45;
|
|
551
|
+
white-space: normal;
|
|
552
|
+
}
|
|
553
|
+
.cs-declared-skip-badge strong {
|
|
554
|
+
color: var(--cs-muted);
|
|
555
|
+
font-weight: 650;
|
|
556
|
+
letter-spacing: 0.03em;
|
|
557
|
+
text-transform: uppercase;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.cs-ungated-badge {
|
|
561
|
+
display: block;
|
|
562
|
+
max-width: 19rem;
|
|
563
|
+
margin-top: 0.25rem;
|
|
564
|
+
padding: 0.25rem 0.4375rem;
|
|
565
|
+
border: 1px solid var(--cs-danger-line);
|
|
566
|
+
border-radius: var(--cs-radius-sm);
|
|
567
|
+
background: var(--cs-danger-wash);
|
|
568
|
+
color: var(--cs-ink);
|
|
569
|
+
font-family: var(--cs-font);
|
|
570
|
+
font-size: 0.6875rem;
|
|
571
|
+
font-weight: 400;
|
|
572
|
+
line-height: 1.45;
|
|
573
|
+
white-space: normal;
|
|
574
|
+
}
|
|
575
|
+
.cs-ungated-badge strong {
|
|
576
|
+
color: var(--cs-danger);
|
|
577
|
+
font-weight: 650;
|
|
578
|
+
letter-spacing: 0.03em;
|
|
579
|
+
text-transform: uppercase;
|
|
580
|
+
}
|
|
581
|
+
.cs-ungated-badge code { font-family: var(--cs-mono); }
|
|
582
|
+
|
|
583
|
+
/* A checked (or partial) cell in a marked row must not read as a live grant:
|
|
584
|
+
swap the cobalt wash for a muted hatch. CSS-only, keyed on the badge — the
|
|
585
|
+
cell markup is untouched. The break-glass cell (.cs-cell-bypass) is excluded
|
|
586
|
+
on purpose (KTD-9): that grant IS live on a marked row, so it keeps the real
|
|
587
|
+
granted wash from the rule above. */
|
|
588
|
+
.cs-grid tbody tr:has(.cs-ungated-badge) td:not(.cs-cell-bypass):has(input:checked),
|
|
589
|
+
.cs-grid tbody tr:has(.cs-ungated-badge) td:not(.cs-cell-bypass):has(input[data-cs-partial="true"]),
|
|
590
|
+
.cs-grid tbody tr:has(.cs-missing-controller-badge) td:not(.cs-cell-bypass):has(input:checked),
|
|
591
|
+
.cs-grid tbody tr:has(.cs-missing-controller-badge) td:not(.cs-cell-bypass):has(input[data-cs-partial="true"]),
|
|
592
|
+
.cs-grid tbody tr:has(.cs-declared-skip-badge) td:not(.cs-cell-bypass):has(input:checked),
|
|
593
|
+
.cs-grid tbody tr:has(.cs-declared-skip-badge) td:not(.cs-cell-bypass):has(input[data-cs-partial="true"]) {
|
|
594
|
+
background: repeating-linear-gradient(-45deg,
|
|
595
|
+
var(--cs-surface-2) 0 4px, var(--cs-bg) 4px 8px);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/* #43 — route with no controller class (stale/typo). Advisory only: keys stay
|
|
599
|
+
grantable (catalog = routes) but the request will MissingController 500. */
|
|
600
|
+
.cs-missing-controller-badge {
|
|
601
|
+
display: block;
|
|
602
|
+
max-width: 19rem;
|
|
603
|
+
margin-top: 0.25rem;
|
|
604
|
+
padding: 0.25rem 0.4375rem;
|
|
605
|
+
border: 1px solid var(--cs-warn, #b45309);
|
|
606
|
+
border-radius: var(--cs-radius-sm);
|
|
607
|
+
background: color-mix(in srgb, var(--cs-warn, #b45309) 12%, var(--cs-surface));
|
|
608
|
+
color: var(--cs-ink);
|
|
609
|
+
font-family: var(--cs-font);
|
|
610
|
+
font-size: 0.6875rem;
|
|
611
|
+
line-height: 1.45;
|
|
612
|
+
white-space: normal;
|
|
613
|
+
}
|
|
614
|
+
.cs-missing-controller-badge strong { color: var(--cs-warn, #b45309); }
|
|
615
|
+
.cs-missing-controller-badge code { font-family: var(--cs-mono); }
|
|
616
|
+
|
|
617
|
+
/* UNINSPECTABLE: the controller raised while loading, so the gate state is
|
|
618
|
+
unknown — a distinct third voice, deliberately not the danger badge (no
|
|
619
|
+
claim about gating is being made) and not silence (unmarked reads as
|
|
620
|
+
"checked, and gated" under the hint's rules). */
|
|
621
|
+
.cs-uninspectable-note {
|
|
622
|
+
display: block;
|
|
623
|
+
max-width: 19rem;
|
|
624
|
+
margin-top: 0.25rem;
|
|
625
|
+
padding: 0.25rem 0.4375rem;
|
|
626
|
+
border: 1px dashed var(--cs-line-strong);
|
|
627
|
+
border-radius: var(--cs-radius-sm);
|
|
628
|
+
background: var(--cs-surface-2);
|
|
629
|
+
color: var(--cs-muted);
|
|
630
|
+
font-family: var(--cs-font);
|
|
631
|
+
font-size: 0.6875rem;
|
|
632
|
+
line-height: 1.45;
|
|
633
|
+
white-space: normal;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/* The per-cell exemption note on a marked row's break-glass cell. */
|
|
637
|
+
.cs-bypass-exempt {
|
|
638
|
+
display: block;
|
|
639
|
+
margin-top: 0.125rem;
|
|
640
|
+
font-size: 0.625rem;
|
|
641
|
+
line-height: 1.3;
|
|
642
|
+
color: var(--cs-muted);
|
|
643
|
+
white-space: nowrap;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/* Checkboxes / radios use the accent. */
|
|
647
|
+
input[type="checkbox"], input[type="radio"] {
|
|
648
|
+
accent-color: var(--cs-accent);
|
|
649
|
+
width: 1rem;
|
|
650
|
+
height: 1rem;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/* ---- Chips (scoped-role assignments) ---------------------------------- */
|
|
654
|
+
|
|
655
|
+
.cs-chip {
|
|
656
|
+
display: inline-flex;
|
|
657
|
+
align-items: stretch;
|
|
658
|
+
border: 1px solid var(--cs-line-strong);
|
|
659
|
+
background: var(--cs-surface);
|
|
660
|
+
border-radius: 999px;
|
|
661
|
+
margin: 0 0.25rem 0.25rem 0;
|
|
662
|
+
font-size: 0.8125rem;
|
|
663
|
+
overflow: hidden;
|
|
664
|
+
}
|
|
665
|
+
.cs-chip-label { padding: 0.125rem 0.25rem 0.125rem 0.6875rem; align-self: center; }
|
|
666
|
+
.cs-chip form { display: flex; margin: 0; }
|
|
667
|
+
|
|
668
|
+
/* #90 — orphaned scoped grants (resource deleted) still list as chips/rows but
|
|
669
|
+
open nothing on collection reads since #65. Mark them so operators do not
|
|
670
|
+
trust the grant. Use .cs-inert-badge — NOT .cs-badge (topbar "full access"). */
|
|
671
|
+
.cs-chip--inert {
|
|
672
|
+
border-style: dashed;
|
|
673
|
+
opacity: 0.85;
|
|
674
|
+
background: var(--cs-surface-2);
|
|
675
|
+
}
|
|
676
|
+
.cs-row--inert td { color: var(--cs-muted); }
|
|
677
|
+
/* #134: two states #90's "inert" does not cover. .cs-dead-badge uses a solid
|
|
678
|
+
danger fill where .cs-inert-badge uses a wash of the same token, so the two
|
|
679
|
+
are separable at a glance. Real theme tokens only (--cs-danger, --cs-warn). */
|
|
680
|
+
.cs-dead-badge,
|
|
681
|
+
.cs-check-badge {
|
|
682
|
+
display: inline-block;
|
|
683
|
+
font-size: 0.6875rem;
|
|
684
|
+
font-weight: 600;
|
|
685
|
+
letter-spacing: 0.02em;
|
|
686
|
+
text-transform: uppercase;
|
|
687
|
+
padding: 0.05rem 0.35rem;
|
|
688
|
+
margin-left: 0.35rem;
|
|
689
|
+
border-radius: 0.25rem;
|
|
690
|
+
max-width: 100%;
|
|
691
|
+
}
|
|
692
|
+
.cs-dead-badge {
|
|
693
|
+
color: var(--cs-surface);
|
|
694
|
+
background: var(--cs-danger);
|
|
695
|
+
}
|
|
696
|
+
.cs-check-badge {
|
|
697
|
+
color: var(--cs-warn, #b45309);
|
|
698
|
+
border: 1px solid var(--cs-warn, #b45309);
|
|
699
|
+
background: color-mix(in srgb, var(--cs-warn, #b45309) 12%, var(--cs-surface));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/* The caveat must reach keyboard and screen-reader users; a title attribute
|
|
703
|
+
reaches neither reliably. */
|
|
704
|
+
.cs-badge-note {
|
|
705
|
+
position: absolute;
|
|
706
|
+
width: 1px;
|
|
707
|
+
height: 1px;
|
|
708
|
+
padding: 0;
|
|
709
|
+
margin: -1px;
|
|
710
|
+
overflow: hidden;
|
|
711
|
+
clip-path: inset(50%);
|
|
712
|
+
white-space: nowrap;
|
|
713
|
+
border: 0;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.cs-inert-badge {
|
|
717
|
+
display: inline-block;
|
|
718
|
+
font-size: 0.6875rem;
|
|
719
|
+
font-weight: 600;
|
|
720
|
+
letter-spacing: 0.02em;
|
|
721
|
+
text-transform: uppercase;
|
|
722
|
+
padding: 0.05rem 0.35rem;
|
|
723
|
+
border-radius: 0.25rem;
|
|
724
|
+
margin-left: 0.35rem;
|
|
725
|
+
vertical-align: middle;
|
|
726
|
+
color: var(--cs-danger, oklch(0.45 0.14 25));
|
|
727
|
+
background: color-mix(in oklch, var(--cs-danger, oklch(0.55 0.18 25)) 12%, transparent);
|
|
728
|
+
border: 1px solid color-mix(in oklch, var(--cs-danger, oklch(0.55 0.18 25)) 35%, transparent);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/* Revoke control — a clear, labeled × with a divider so it reads as a button. */
|
|
732
|
+
.cs-chip-remove {
|
|
733
|
+
border: none;
|
|
734
|
+
border-left: 1px solid var(--cs-line);
|
|
735
|
+
border-radius: 0;
|
|
736
|
+
background: none;
|
|
737
|
+
cursor: pointer;
|
|
738
|
+
padding: 0 0.5rem;
|
|
739
|
+
color: var(--cs-muted);
|
|
740
|
+
font-size: 0.95rem;
|
|
741
|
+
line-height: 1;
|
|
742
|
+
transition: background 120ms var(--cs-ease), color 120ms var(--cs-ease);
|
|
743
|
+
}
|
|
744
|
+
.cs-chip-remove:hover, .cs-chip-remove:focus-visible {
|
|
745
|
+
color: var(--cs-on-accent);
|
|
746
|
+
background: var(--cs-danger);
|
|
747
|
+
border-color: var(--cs-danger);
|
|
748
|
+
outline: none;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/* "+ scoped role" reads as an add affordance. */
|
|
752
|
+
.cs-add-scoped {
|
|
753
|
+
display: inline-flex;
|
|
754
|
+
align-items: center;
|
|
755
|
+
gap: 0.25rem;
|
|
756
|
+
font-size: 0.8125rem;
|
|
757
|
+
text-decoration: none;
|
|
758
|
+
color: var(--cs-accent);
|
|
759
|
+
padding: 0.125rem 0.4375rem;
|
|
760
|
+
border: 1px dashed var(--cs-line-strong);
|
|
761
|
+
border-radius: 999px;
|
|
762
|
+
}
|
|
763
|
+
.cs-add-scoped:hover { border-color: var(--cs-accent); background: var(--cs-accent-wash); }
|
|
764
|
+
|
|
765
|
+
/* ---- Forms ------------------------------------------------------------ */
|
|
766
|
+
|
|
767
|
+
/* Scoped to the content region: these layout rules must not reach a form the
|
|
768
|
+
host might place in the sidebar or topbar. */
|
|
769
|
+
.cs-main form p { margin: 0 0 1rem; max-width: 30rem; }
|
|
770
|
+
|
|
771
|
+
/* Non-control captions (bulk subjects line) share form-label type. */
|
|
772
|
+
.cs-main form p > .cs-label,
|
|
773
|
+
.cs-main form p > label {
|
|
774
|
+
display: block;
|
|
775
|
+
font-size: 0.7rem;
|
|
776
|
+
font-weight: 600;
|
|
777
|
+
letter-spacing: 0.03em;
|
|
778
|
+
text-transform: uppercase;
|
|
779
|
+
color: var(--cs-muted);
|
|
780
|
+
margin-bottom: 0.375rem;
|
|
781
|
+
}
|
|
782
|
+
.cs-main form p > label:has(input) {
|
|
783
|
+
display: flex;
|
|
784
|
+
align-items: center;
|
|
785
|
+
gap: 0.4375rem;
|
|
786
|
+
font-size: 0.875rem;
|
|
787
|
+
font-weight: 500;
|
|
788
|
+
letter-spacing: 0;
|
|
789
|
+
text-transform: none;
|
|
790
|
+
color: var(--cs-ink);
|
|
791
|
+
cursor: pointer;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
input[type="text"], input[type="email"], input[type="password"],
|
|
795
|
+
input[type="search"], select, textarea {
|
|
796
|
+
font: inherit;
|
|
797
|
+
color: var(--cs-ink);
|
|
798
|
+
background: var(--cs-bg);
|
|
799
|
+
border: 1px solid var(--cs-line-strong);
|
|
800
|
+
border-radius: var(--cs-radius-sm);
|
|
801
|
+
padding: 0.4375rem 0.625rem;
|
|
802
|
+
max-width: 100%;
|
|
803
|
+
}
|
|
804
|
+
.cs-main form p > input[type="text"],
|
|
805
|
+
.cs-main form p > input[type="email"],
|
|
806
|
+
.cs-main form p > input[type="password"],
|
|
807
|
+
.cs-main form p > select,
|
|
808
|
+
.cs-main form p > textarea { width: 100%; }
|
|
809
|
+
textarea { resize: vertical; min-height: 2.5rem; }
|
|
810
|
+
|
|
811
|
+
input::placeholder { color: var(--cs-muted); opacity: 1; }
|
|
812
|
+
input:focus-visible, select:focus-visible, textarea:focus-visible {
|
|
813
|
+
outline: 2px solid var(--cs-accent);
|
|
814
|
+
outline-offset: 0;
|
|
815
|
+
border-color: var(--cs-accent);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/* ---- Buttons ---------------------------------------------------------- */
|
|
819
|
+
|
|
820
|
+
button, input[type="submit"], .cs-btn {
|
|
821
|
+
appearance: none;
|
|
822
|
+
display: inline-flex;
|
|
823
|
+
align-items: center;
|
|
824
|
+
gap: 0.375rem;
|
|
825
|
+
font: 500 0.875rem var(--cs-font);
|
|
826
|
+
color: var(--cs-ink);
|
|
827
|
+
background: var(--cs-surface);
|
|
828
|
+
border: 1px solid var(--cs-line-strong);
|
|
829
|
+
border-radius: var(--cs-radius-sm);
|
|
830
|
+
padding: 0.4375rem 0.875rem;
|
|
831
|
+
cursor: pointer;
|
|
832
|
+
text-decoration: none;
|
|
833
|
+
transition: background 130ms var(--cs-ease), border-color 130ms var(--cs-ease), translate 90ms var(--cs-ease);
|
|
834
|
+
}
|
|
835
|
+
.cs-btn:hover { color: var(--cs-ink); }
|
|
836
|
+
button:hover, input[type="submit"]:hover, .cs-btn:hover { background: var(--cs-surface-2); border-color: var(--cs-ink); }
|
|
837
|
+
button:active, input[type="submit"]:active, .cs-btn:active { translate: 0 1px; }
|
|
838
|
+
button:disabled, input[type="submit"]:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
839
|
+
|
|
840
|
+
/* Primary — the one committed action per form. */
|
|
841
|
+
.cs-btn-primary,
|
|
842
|
+
form > p:last-child input[type="submit"] {
|
|
843
|
+
color: var(--cs-on-accent);
|
|
844
|
+
background: var(--cs-accent);
|
|
845
|
+
border-color: var(--cs-accent);
|
|
846
|
+
}
|
|
847
|
+
.cs-btn-primary:hover,
|
|
848
|
+
form > p:last-child input[type="submit"]:hover {
|
|
849
|
+
background: var(--cs-accent-strong);
|
|
850
|
+
border-color: var(--cs-accent-strong);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.cs-btn-danger { color: var(--cs-danger); border-color: var(--cs-danger-line); }
|
|
854
|
+
.cs-btn-danger:hover { color: var(--cs-on-accent); background: var(--cs-danger); border-color: var(--cs-danger); }
|
|
855
|
+
|
|
856
|
+
/* Inline row actions shouldn't shout. */
|
|
857
|
+
.cs-table form { display: inline; margin: 0; }
|
|
858
|
+
|
|
859
|
+
/* ---- Scoped-role picker ----------------------------------------------- */
|
|
860
|
+
|
|
861
|
+
.cs-picker turbo-frame { display: block; }
|
|
862
|
+
.cs-picker-empty { max-width: 30rem; margin: 0 0 1rem; }
|
|
863
|
+
.cs-picker-empty code {
|
|
864
|
+
font-family: var(--cs-mono);
|
|
865
|
+
font-size: 0.8125rem;
|
|
866
|
+
background: var(--cs-surface-2);
|
|
867
|
+
border: 1px solid var(--cs-line);
|
|
868
|
+
border-radius: 3px;
|
|
869
|
+
padding: 0.0625rem 0.25rem;
|
|
870
|
+
}
|
|
871
|
+
.cs-picker-actions { display: flex; align-items: center; gap: 0.625rem; }
|
|
872
|
+
.cs-picker-refresh { font-size: 0.8125rem; color: var(--cs-muted); }
|
|
873
|
+
|
|
874
|
+
/* ---- Pagination ------------------------------------------------------- */
|
|
875
|
+
|
|
876
|
+
.cs-pagination {
|
|
877
|
+
display: flex;
|
|
878
|
+
align-items: center;
|
|
879
|
+
gap: 1rem;
|
|
880
|
+
margin-top: 1rem;
|
|
881
|
+
font-size: 0.8125rem;
|
|
882
|
+
color: var(--cs-muted);
|
|
883
|
+
}
|
|
884
|
+
.cs-pagination a { text-decoration: none; }
|
|
885
|
+
|
|
886
|
+
/* ---- Motion ----------------------------------------------------------- */
|
|
887
|
+
|
|
888
|
+
@media (prefers-reduced-motion: reduce) {
|
|
889
|
+
*, *::before, *::after { transition-duration: 0.01ms !important; }
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/* ---- Responsive ------------------------------------------------------- *
|
|
893
|
+
* Collapse the sidebar to a top strip on narrow viewports.
|
|
894
|
+
*/
|
|
895
|
+
|
|
896
|
+
@media (max-width: 820px) {
|
|
897
|
+
.cs-shell { grid-template-columns: 1fr; }
|
|
898
|
+
.cs-sidebar {
|
|
899
|
+
position: static;
|
|
900
|
+
height: auto;
|
|
901
|
+
flex-direction: row;
|
|
902
|
+
align-items: center;
|
|
903
|
+
flex-wrap: wrap;
|
|
904
|
+
gap: 0.25rem 0.5rem;
|
|
905
|
+
border-right: none;
|
|
906
|
+
border-bottom: 1px solid var(--cs-line);
|
|
907
|
+
}
|
|
908
|
+
.cs-brand { padding: 0.25rem 0.5rem; }
|
|
909
|
+
.cs-nav { flex-direction: row; flex-wrap: wrap; }
|
|
910
|
+
.cs-nav a[aria-current="page"]::before { display: none; }
|
|
911
|
+
.cs-sidebar-foot { display: none; }
|
|
912
|
+
.cs-content { padding: 1.25rem; }
|
|
913
|
+
.cs-topbar { padding: 0.75rem 1.25rem; }
|
|
914
|
+
/* Data tables take their natural width and scroll inside the flush card,
|
|
915
|
+
rather than shrinking columns until cells wrap into very tall rows. */
|
|
916
|
+
.cs-card--flush .cs-table { width: max-content; }
|
|
917
|
+
}
|