@kanunilabs/datagrid 1.0.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.
@@ -0,0 +1,3526 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ KANUNILABS DataGrid — isolated theme system
3
+ ─────────────────────────────────────────────────────────────
4
+ Every rule is scoped under .kanuni-datagrid-root so the host app's CSS
5
+ cannot bleed in and ours cannot bleed out. Tokens mirror the PivotGrid
6
+ vocabulary (--pivot-*) with a --dg-* prefix so the two products can be
7
+ themed side by side without collisions.
8
+ ───────────────────────────────────────────────────────────── */
9
+
10
+ /* ── Design tokens ───────────────────────────────────────────
11
+ Declared on more than the grid root because two things render OUTSIDE it
12
+ and still have to look like the grid: the enterprise wrapper (which owns
13
+ the row/form editor, a SIBLING of the root) and any layer portalled to
14
+ <body> (the lookup list, tagged `.kanuni-datagrid-detached`).
15
+
16
+ Tokens defined only on the root left those resolving `var(--dg-surface)` to
17
+ nothing — which is not a fallback but an INVALID declaration, so the popup
18
+ editor rendered fully transparent and unreadable over the grid.
19
+
20
+ Only the custom properties live here. The grid's own box (border, radius,
21
+ overflow, flex) stays on the root below: giving it to the wrapper would draw
22
+ a second border around the whole component and clip the editor. */
23
+ .kanuni-datagrid-root,
24
+ .kanuni-datagrid-enterprise,
25
+ .kanuni-datagrid-detached {
26
+ /* ── Color tokens ── */
27
+ --dg-bg: #ffffff;
28
+ --dg-surface: #ffffff;
29
+ --dg-surface-header: #f8fafc; /* slate-50 */
30
+ --dg-surface-hover: #f1f5f9; /* slate-100 */
31
+ --dg-surface-stripe: #fafafa;
32
+ --dg-border: #e2e8f0; /* slate-200 */
33
+ --dg-border-strong: #cbd5e1; /* slate-300 */
34
+ --dg-text: #0f172a; /* slate-900 */
35
+ --dg-text-muted: #64748b; /* slate-500 */
36
+ /* sky-700, and the depth is the point rather than the hue.
37
+ This was sky-500 (#0ea5e9), which is the accent EVERY foreground in the
38
+ grid is drawn from: the sort caret over the header, the active funnel, the
39
+ editor underline, the detail rule, and the white label on a group chip or
40
+ a primary button. On a light surface sky-500 measures 2.77:1 under white
41
+ and 2.65:1 as a caret on the header — under the 4.5:1 that text needs and
42
+ under the 3:1 that an icon needs. sky-600 was not enough either (4.10:1
43
+ white-on). sky-700 clears both: 5.93:1 under white, 5.67:1 on the header.
44
+ The dark palette below keeps sky-400 — it is already at 8.33:1 there,
45
+ because the problem was never the hue, it was using a mid-tone accent
46
+ against a white background. */
47
+ --dg-primary: #0369a1; /* sky-700 */
48
+ --dg-primary-soft: #e0f2fe; /* sky-100 */
49
+ --dg-focus-ring: #0369a1; /* matches the accent */
50
+ /* Cell flash (P1.7). Translucent so the row's own background — striped,
51
+ selected, dirty — stays visible under the pulse rather than being
52
+ replaced by it. */
53
+ --dg-flash-bg: rgba(3, 105, 161, 0.35);
54
+ --dg-flash-duration: 700ms;
55
+ /* Cell note marker (P3.3). Red on purpose and NOT the accent: the accent
56
+ means "the grid did this" (focus, selection, sort), and a note means
57
+ "a person put something here" — two different kinds of signal. */
58
+ --dg-note-marker: #dc2626;
59
+
60
+ /* ── Semantic colours ──────────────────────────────────────
61
+ Every default below is the literal that used to be written inline at the
62
+ point of use, so a grid with no theme renders exactly as before. They are
63
+ tokens now because a theme has to be able to move them: `--dg-on-primary`
64
+ is the clearest case — the KanuniLabs accent is a GOLD, and the white that
65
+ reads fine on sky-500 measures about 2:1 on it. A theme that can recolour
66
+ the chip but not the text on the chip is a theme that ships unreadable
67
+ buttons. */
68
+ /* The header row is its own surface, separate from `--dg-surface-header`.
69
+ They default to the same value, and every theme here but one keeps them
70
+ that way — but a corporate theme with an INK header still wants the group
71
+ panel, the filter row and the footer to stay light. Without the split, the
72
+ one token dragged all four along and the panel hints (muted text) went
73
+ unreadable on the dark fill. */
74
+ --dg-header-bg: var(--dg-surface-header);
75
+ --dg-header-text: var(--dg-text);
76
+ --dg-header-text-muted: var(--dg-text-muted);
77
+ --dg-header-hover: var(--dg-surface-hover);
78
+ --dg-on-primary: #ffffff;
79
+ --dg-danger: #dc2626; /* red-600 — borders, solid fills */
80
+ --dg-danger-text: #b91c1c; /* red-700 — text on a light surface */
81
+ --dg-on-danger: #ffffff; /* text ON a solid --dg-danger fill */
82
+ --dg-warning: #b45309; /* amber-700 */
83
+ --dg-selected-bg: var(--dg-primary-soft);
84
+ /* Search highlight (P2.4). Amber rather than the accent: the accent already
85
+ means "selected" and "focused" in this grid, and a third meaning on the
86
+ same colour would make a highlighted cell read as a selected one. */
87
+ --dg-highlight-bg: #fde68a; /* amber-200 */
88
+ --dg-highlight-text: #713f12; /* amber-900 — 8.9:1 on the above */
89
+ --dg-group-bg: var(--dg-surface-header);
90
+ --dg-footer-bg: var(--dg-surface-header);
91
+ --dg-overlay: rgba(15, 23, 42, 0.55);
92
+ --dg-scrollbar-thumb: var(--dg-border-strong);
93
+ --dg-scrollbar-track: transparent;
94
+
95
+ /* ── Elevation ──
96
+ A flat theme sets these to `none`; Material raises them. */
97
+ --dg-shadow-popup: 0 10px 25px -5px rgba(15, 23, 42, 0.25);
98
+ --dg-shadow-popup-sm: 0 8px 24px rgba(15, 23, 42, 0.16);
99
+ --dg-shadow-menu: 0 12px 28px rgba(15, 23, 42, 0.22);
100
+ --dg-shadow-card: 0 12px 32px rgba(15, 23, 42, 0.18);
101
+ --dg-shadow-panel: 0 8px 20px rgba(0, 0, 0, 0.12);
102
+ --dg-shadow-dialog: 0 24px 48px rgba(0, 0, 0, 0.28);
103
+
104
+ /* ── Metric tokens ── */
105
+ --dg-row-height: 28px;
106
+ --dg-header-height: 32px;
107
+ --dg-cell-padding-x: 8px;
108
+ --dg-font-size: 13px;
109
+ --dg-header-font-size: 12px;
110
+ --dg-radius: 6px;
111
+ --dg-shadow-pinned: 2px 0 4px -2px rgba(15, 23, 42, 0.18);
112
+ --dg-shadow-pinned-right: -2px 0 4px -2px rgba(15, 23, 42, 0.18);
113
+
114
+ /* ── Line work ─────────────────────────────────────────────
115
+ Whole SHORTHANDS rather than a colour and a width, so a theme can drop a
116
+ line entirely (`none`) instead of only fading it. This is most of what
117
+ separates the presets: Balham rules every column and every row, Material
118
+ rules rows only, Quartz keeps the rows faint and skips the columns. */
119
+ --dg-cell-border: 1px solid var(--dg-border);
120
+ --dg-row-border: 1px solid var(--dg-border);
121
+ --dg-header-cell-border: 1px solid var(--dg-border);
122
+ --dg-header-border: 1px solid var(--dg-border-strong);
123
+ /* A band's EDGE is not a column separator, so it gets its own token rather
124
+ than riding on `--dg-header-cell-border`. Quartz and Material set that one
125
+ to `none` on purpose — they rule rows, not columns — and the bands went
126
+ with it: "Where" and "Sales" sat over their columns with nothing marking
127
+ where either one started or stopped. Same argument the band row's
128
+ `border-bottom` already makes: a band boundary is structural. A theme can
129
+ still override this token if it wants a different line.
130
+
131
+ Mixed from the header TEXT rather than taken from a border token, because
132
+ both border tokens are too faint for a 24px vertical segment: measured on
133
+ Quartz against the header background, `--dg-border` gives 1.17:1 and
134
+ `--dg-border-strong` 1.39:1 — lines that are technically drawn and
135
+ practically invisible, i.e. a fix that changes nothing the user sees. The
136
+ mix lands near 2:1 and follows the theme in both light and dark, since the
137
+ header text is what already has to be readable on that ground. */
138
+ --dg-band-cell-border: 1px solid color-mix(in srgb, var(--dg-header-text) 32%, transparent);
139
+
140
+ /* ── Header treatment ── */
141
+ --dg-header-weight: 600;
142
+ --dg-header-transform: none;
143
+ --dg-header-tracking: normal;
144
+
145
+ /* ── Chrome (toolbar, pager, panels, popups) ──
146
+ Sized separately from the data surface: a compact grid shrinks its rows
147
+ hard, but a 9px button is not usable at any density. */
148
+ --dg-chrome-font-size: 12px;
149
+ --dg-chrome-padding-y: 6px;
150
+ --dg-control-height: 26px;
151
+ --dg-control-font-size: 12px;
152
+ --dg-filter-input-height: 22px;
153
+ --dg-radius-control: 4px;
154
+
155
+ /* The line box a WRAPPED cell lays its text out on, and the height an editor
156
+ takes in one. Whole pixels — see the note on `--wrap` below. */
157
+ --dg-wrap-line-height: 18px;
158
+ }
159
+
160
+ /* Detached surfaces carry the foreground as well as the tokens, for the reason
161
+ the enterprise wrapper does: a portalled layer is a child of <body>, so
162
+ anything inside it written as `color: inherit` would resolve against the host
163
+ page rather than against the grid it belongs to. The dialog and the row
164
+ editor set their own colour, but the box AROUND them is the safety net. */
165
+ .kanuni-datagrid-detached {
166
+ color: var(--dg-text);
167
+ }
168
+
169
+ /* The grid's own box. */
170
+ .kanuni-datagrid-root {
171
+ box-sizing: border-box;
172
+ font-family: inherit;
173
+ -webkit-font-smoothing: antialiased;
174
+ -moz-osx-font-smoothing: grayscale;
175
+ position: relative;
176
+ display: flex;
177
+ flex-direction: column;
178
+ overflow: hidden;
179
+ border: 1px solid var(--dg-border);
180
+ border-radius: var(--dg-radius);
181
+ background: var(--dg-bg);
182
+ color: var(--dg-text);
183
+ font-size: var(--dg-font-size);
184
+ line-height: 1.4;
185
+ }
186
+
187
+ .kanuni-datagrid-root *,
188
+ .kanuni-datagrid-root *::before,
189
+ .kanuni-datagrid-root *::after {
190
+ box-sizing: border-box;
191
+ }
192
+
193
+ /* Dark theme: opt in with .kanuni-datagrid-root.kanuni-datagrid-dark */
194
+ .kanuni-datagrid-root.kanuni-datagrid-dark,
195
+ .kanuni-datagrid-enterprise.kanuni-datagrid-dark,
196
+ .kanuni-datagrid-detached.kanuni-datagrid-dark {
197
+ --dg-bg: #0b1120;
198
+ --dg-surface: #0f172a;
199
+ --dg-surface-header: #16233c;
200
+ --dg-surface-hover: #1e293b;
201
+ --dg-surface-stripe: #101a2e;
202
+ --dg-border: #24344d;
203
+ --dg-border-strong: #334155;
204
+ --dg-text: #e2e8f0;
205
+ --dg-text-muted: #94a3b8;
206
+ --dg-primary: #38bdf8;
207
+ --dg-primary-soft: #0c4a6e;
208
+ --dg-focus-ring: #7dd3fc;
209
+ /* Lifted for a dark surface: crimson on near-black reads as brown. */
210
+ --dg-note-marker: #f87171;
211
+ --dg-shadow-pinned: 2px 0 4px -2px rgba(0, 0, 0, 0.55);
212
+ --dg-shadow-pinned-right: -2px 0 4px -2px rgba(0, 0, 0, 0.55);
213
+
214
+ /* The semantic set, re-pointed for a dark surface. `--dg-on-primary` goes
215
+ DARK here because the dark accent is a light sky-400. */
216
+ --dg-on-primary: #082f49;
217
+ /* Dark: a muted amber fill with light text, so the mark reads as emphasis
218
+ instead of glowing off a dark surface the way amber-200 would. */
219
+ --dg-highlight-bg: #78350f; /* amber-900 */
220
+ --dg-highlight-text: #fef3c7; /* amber-100 — 9.6:1 on the above */
221
+ --dg-danger: #f87171;
222
+ --dg-danger-text: #fca5a5;
223
+ /* The dark danger is a LIGHT red, so what sits on it has to be dark. White
224
+ on #f87171 measures 2.4:1 — the delete button's own label. */
225
+ --dg-on-danger: #450a0a;
226
+ --dg-warning: #fbbf24;
227
+ --dg-overlay: rgba(2, 6, 23, 0.7);
228
+ --dg-scrollbar-thumb: #475569;
229
+ --dg-shadow-popup: 0 10px 25px -5px rgba(0, 0, 0, 0.6);
230
+ --dg-shadow-popup-sm: 0 8px 24px rgba(0, 0, 0, 0.5);
231
+ --dg-shadow-menu: 0 12px 28px rgba(0, 0, 0, 0.55);
232
+ --dg-shadow-card: 0 12px 32px rgba(0, 0, 0, 0.55);
233
+ --dg-shadow-panel: 0 8px 20px rgba(0, 0, 0, 0.45);
234
+ --dg-shadow-dialog: 0 24px 48px rgba(0, 0, 0, 0.65);
235
+ }
236
+
237
+ /* ── Single scroll container (§7.4: NO dual-table architecture) ────────
238
+ The header and the body live in ONE scroller, so horizontal alignment
239
+ cannot drift: there is no second element to keep in sync. */
240
+ .kanuni-datagrid-scroller {
241
+ position: relative;
242
+ flex: 1 1 auto;
243
+ overflow: auto;
244
+ overscroll-behavior: contain;
245
+ /* Scroll anchoring must be OFF in a virtualized scroller. Under scroll
246
+ compression the rows legitimately move relative to the canvas on every
247
+ scroll; Chrome's anchoring reads that as "content shifted under the user"
248
+ and walks scrollTop back to chase the anchor row. During a scrollbar drag
249
+ the correction is deferred and applied on mouse RELEASE — the reported
250
+ symptom (2026-08-14, 1M rows): the last rows peek out while dragging and
251
+ snap back when the button is let go, leaving the true bottom unreachable
252
+ by user scrolling while programmatic probes see nothing wrong. */
253
+ overflow-anchor: none;
254
+ background: var(--dg-surface);
255
+ }
256
+
257
+ /* Sized to the full virtual extent so the native scrollbar is accurate. */
258
+ .kanuni-datagrid-canvas {
259
+ position: relative;
260
+ }
261
+
262
+ /* ── Header ───────────────────────────────────────────────── */
263
+ /* ── Header bands (multi-level headers) ──────────────────────
264
+ Siblings of the header row, each sticky at its own offset so the whole
265
+ stack stays put while the rows scroll under it. */
266
+ .kanuni-datagrid-band-row {
267
+ position: sticky;
268
+ z-index: 4;
269
+ display: flex;
270
+ background: var(--dg-header-bg);
271
+ /* Deliberately a literal, not `--dg-header-cell-border`: that token is the
272
+ COLUMN separator, which some themes drop entirely. The line under a band
273
+ row is structural — without it a two-level header reads as one tall box. */
274
+ border-bottom: 1px solid var(--dg-border);
275
+ }
276
+
277
+ .kanuni-datagrid-band-cell {
278
+ box-sizing: border-box;
279
+ display: flex;
280
+ align-items: center;
281
+ justify-content: center;
282
+ min-width: 0;
283
+ padding: 0 8px;
284
+ background: var(--dg-header-bg);
285
+ color: var(--dg-header-text);
286
+ font-size: calc(var(--dg-font-size) - 1px);
287
+ font-weight: var(--dg-header-weight);
288
+ }
289
+
290
+ /* A real band is drawn as a bounded box, so its extent is readable: the right
291
+ edge on every one, and the left edge only where the thing before it is NOT
292
+ another band. Bands that touch therefore share one line instead of drawing
293
+ two beside each other. */
294
+ .kanuni-datagrid-band-cell:not(.kanuni-datagrid-band-cell--spacer) {
295
+ border-inline-end: var(--dg-band-cell-border);
296
+ }
297
+
298
+ .kanuni-datagrid-band-cell--spacer + .kanuni-datagrid-band-cell:not(.kanuni-datagrid-band-cell--spacer),
299
+ .kanuni-datagrid-band-cell:first-child:not(.kanuni-datagrid-band-cell--spacer) {
300
+ border-inline-start: var(--dg-band-cell-border);
301
+ }
302
+
303
+ /* No band at this level: the column's own header reaches up through here, so
304
+ the box must be INVISIBLE rather than an empty cell with borders. */
305
+ .kanuni-datagrid-band-cell--spacer {
306
+ border-inline-end: none;
307
+ border-inline-start: none;
308
+ }
309
+
310
+ .kanuni-datagrid-band-cell--pinned {
311
+ position: sticky;
312
+ z-index: 5;
313
+ }
314
+
315
+ .kanuni-datagrid-band-label {
316
+ overflow: hidden;
317
+ text-overflow: ellipsis;
318
+ white-space: nowrap;
319
+ }
320
+
321
+ .kanuni-datagrid-header {
322
+ position: sticky;
323
+ top: 0;
324
+ z-index: 3;
325
+ display: flex;
326
+ height: var(--dg-header-height);
327
+ background: var(--dg-header-bg);
328
+ border-bottom: var(--dg-header-border);
329
+ }
330
+
331
+ .kanuni-datagrid-header-cell {
332
+ /* Anchors the resize handle. Pinned headers override this with `sticky`,
333
+ which is also a positioned ancestor, so the handle works there too. */
334
+ position: relative;
335
+ display: flex;
336
+ align-items: center;
337
+ gap: 4px;
338
+ flex: none;
339
+ height: 100%;
340
+ padding: 0 var(--dg-cell-padding-x);
341
+ background: var(--dg-header-bg);
342
+ border-inline-end: var(--dg-header-cell-border);
343
+ font-size: var(--dg-header-font-size);
344
+ font-weight: var(--dg-header-weight);
345
+ letter-spacing: var(--dg-header-tracking);
346
+ text-transform: var(--dg-header-transform);
347
+ color: var(--dg-header-text);
348
+ white-space: nowrap;
349
+ overflow: hidden;
350
+ user-select: none;
351
+ }
352
+
353
+ .kanuni-datagrid-header-cell--sortable {
354
+ cursor: pointer;
355
+ }
356
+
357
+ .kanuni-datagrid-header-cell--sortable:hover {
358
+ background: var(--dg-header-hover);
359
+ }
360
+
361
+ .kanuni-datagrid-header-label {
362
+ overflow: hidden;
363
+ text-overflow: ellipsis;
364
+ white-space: nowrap;
365
+ }
366
+
367
+ .kanuni-datagrid-sort-indicator {
368
+ flex: none;
369
+ font-size: 10px;
370
+ line-height: 1;
371
+ color: var(--dg-primary);
372
+ }
373
+
374
+ .kanuni-datagrid-sort-order {
375
+ flex: none;
376
+ font-size: 9px;
377
+ color: var(--dg-header-text-muted);
378
+ }
379
+
380
+ /* ── Rows and cells ───────────────────────────────────────── */
381
+ .kanuni-datagrid-viewport {
382
+ position: absolute;
383
+ top: 0;
384
+ left: 0;
385
+ width: 100%;
386
+ will-change: transform;
387
+ }
388
+
389
+ .kanuni-datagrid-row {
390
+ display: flex;
391
+ height: var(--dg-row-height);
392
+ background: var(--dg-surface);
393
+ border-bottom: var(--dg-row-border);
394
+ /* Dragging a CELL RANGE must not also drag a text selection. The browser
395
+ starts one on any mousedown-and-move over text, so the user got two
396
+ overlapping highlights at once — the grid's range in pale blue and the
397
+ native selection in solid blue — with no way to tell which one the totals
398
+ strip was summing. The header and footer already do this; the data rows
399
+ were the gap. Copying is not lost: the range owns Ctrl+C. */
400
+ user-select: none;
401
+ }
402
+
403
+ /* ── Auto-height rows ──────────────────────────────────────
404
+ A grid with an `autoHeight` column lets its rows grow. The row's height is
405
+ NOT written inline here: it is measured after layout and handed to the window
406
+ engine, which owns the scroll canvas and every row offset. Setting it inline
407
+ as well would make the measurement read back whatever was set — the row would
408
+ agree with itself and never grow.
409
+
410
+ `min-height` keeps a one-line row exactly the height it was, so turning the
411
+ feature on does not resize the rows that did not need it. */
412
+ .kanuni-datagrid-root[data-auto-height] .kanuni-datagrid-row {
413
+ height: auto;
414
+ min-height: var(--dg-row-height);
415
+ }
416
+
417
+ /* Every OTHER cell of an autoHeight grid must stretch to its row.
418
+ The base rule's `height: 100%` cannot resolve against the row's
419
+ `height: auto`, so it computed as content height and every single-line cell
420
+ collapsed to its text — 18px inside a 28px row, worse inside a wrapped one.
421
+ Two user-visible symptoms (report, 2026-08-14): the focus rectangle hugged
422
+ the TEXT instead of the cell, and a click in the lower band of a cell landed
423
+ on the bare row — no column resolved, so nothing got focused. Stretching is
424
+ what the base rule always meant; `align-items: center` then keeps single-line
425
+ text centred exactly as in a fixed-height grid. The wrap cell is excluded:
426
+ its own block sizes it to its text on purpose — that is how the row learns
427
+ how tall to be. */
428
+ .kanuni-datagrid-root[data-auto-height] .kanuni-datagrid-row .kanuni-datagrid-cell:not(.kanuni-datagrid-cell--wrap) {
429
+ height: auto;
430
+ align-self: stretch;
431
+ }
432
+
433
+ /* Anything the user genuinely types into, or reads as prose, gets it back. */
434
+ .kanuni-datagrid-row input,
435
+ .kanuni-datagrid-row textarea,
436
+ .kanuni-datagrid-row select,
437
+ .kanuni-datagrid-detail-cell,
438
+ .kanuni-datagrid-editform-cell {
439
+ user-select: text;
440
+ }
441
+
442
+ .kanuni-datagrid-row--odd {
443
+ background: var(--dg-surface-stripe);
444
+ }
445
+
446
+ .kanuni-datagrid-row:hover,
447
+ .kanuni-datagrid-row:hover .kanuni-datagrid-cell--pinned {
448
+ background: var(--dg-surface-hover);
449
+ }
450
+
451
+ .kanuni-datagrid-cell {
452
+ display: flex;
453
+ align-items: center;
454
+ flex: none;
455
+ height: 100%;
456
+ padding: 0 var(--dg-cell-padding-x);
457
+ border-inline-end: var(--dg-cell-border);
458
+ white-space: nowrap;
459
+ overflow: hidden;
460
+ text-overflow: ellipsis;
461
+ }
462
+
463
+ /* ── autoHeight: the columns that wrap ─────────────────────
464
+ Only the columns that asked for it. Everything else keeps its single line and
465
+ its ellipsis — wrapping a whole grid because one column needed to is how a
466
+ dense table turns into a wall of text.
467
+
468
+ The doubled class is deliberate and load-bearing. `.kanuni-datagrid-cell--wrap`
469
+ alone has the same specificity as the base rule directly above, so whichever
470
+ came LAST in the file won; written earlier in the sheet it lost silently and
471
+ the cells kept `white-space: nowrap` while everything else about the feature
472
+ worked. Two classes settle it regardless of where this block sits. */
473
+ .kanuni-datagrid-cell.kanuni-datagrid-cell--wrap {
474
+ white-space: normal;
475
+ overflow: visible;
476
+ text-overflow: clip;
477
+ /* `anywhere` and not `break-word`: an unbroken 200-character token (a URL, a
478
+ serial number) otherwise overflows its column and lands on the next one. */
479
+ overflow-wrap: anywhere;
480
+ align-items: flex-start;
481
+ /* Not `height: 100%` — the cell has to be as tall as its own text, or the row
482
+ it is supposed to stretch never learns it needs to grow. */
483
+ height: auto;
484
+ align-self: stretch;
485
+ padding-block: 4px;
486
+ /* An INTEGER line box, not a ratio. `1.35` against the 13px body font lays
487
+ out at 17.55px, so every wrapped row landed on a fractional boundary and
488
+ its 1px border rendered a pixel thick on some rows and two on others.
489
+ A token now, because a theme moves the body font under it — 18px is right
490
+ for 13px text and cramped for Material's 14px. */
491
+ line-height: var(--dg-wrap-line-height);
492
+ }
493
+
494
+ .kanuni-datagrid-cell--number {
495
+ justify-content: flex-end;
496
+ font-variant-numeric: tabular-nums;
497
+ }
498
+
499
+ .kanuni-datagrid-cell--center {
500
+ justify-content: center;
501
+ }
502
+
503
+ /* ── Pinned columns = sticky cells inside the same row (§7.4) ──────────
504
+ Compositor-friendly and layout-free; structurally immune to the
505
+ header/body desync bug class of a dual-table design. */
506
+ .kanuni-datagrid-cell--pinned,
507
+ .kanuni-datagrid-header-cell--pinned {
508
+ position: sticky;
509
+ z-index: 2;
510
+ background: inherit;
511
+ }
512
+
513
+ /* HARDENING: a pinned cell is sticky, whatever else it becomes.
514
+ State classes keep anchoring their markers with `position: relative` —
515
+ the dirty corner, the range overlay, whatever comes next — and on a pinned
516
+ cell that override turns `insetInlineStart` from a parking threshold into a
517
+ literal shift: the cell jumps sideways by its whole inset and parks over its
518
+ neighbour. It happened twice from two different classes in one day
519
+ (range-anchor on click, dirty on commit), so the guarantee lives HERE at
520
+ three-class specificity instead of as a `:not(--pinned)` chased onto every
521
+ state rule. Sticky is itself a positioned ancestor, so those markers lose
522
+ nothing. */
523
+ .kanuni-datagrid-root .kanuni-datagrid-cell.kanuni-datagrid-cell--pinned {
524
+ position: sticky;
525
+ }
526
+
527
+ /* A STICKY column sits in the scrolling order and parks itself only once it
528
+ would leave the viewport. It needs a lower z-index than a truly pinned
529
+ column: when the two meet, the pinned one is the fixed landmark and has to
530
+ win, or the sticky column would slide over the top of it. */
531
+ .kanuni-datagrid-cell--sticky {
532
+ z-index: 1;
533
+ }
534
+
535
+ .kanuni-datagrid-header-cell--pinned {
536
+ z-index: 4;
537
+ background: var(--dg-header-bg);
538
+ }
539
+
540
+ .kanuni-datagrid-cell--pinned-left:last-of-type,
541
+ .kanuni-datagrid-header-cell--pinned-left:last-of-type {
542
+ box-shadow: var(--dg-shadow-pinned);
543
+ }
544
+
545
+ .kanuni-datagrid-pin-edge-left {
546
+ box-shadow: var(--dg-shadow-pinned);
547
+ }
548
+
549
+ .kanuni-datagrid-pin-edge-right {
550
+ box-shadow: var(--dg-shadow-pinned-right);
551
+ }
552
+
553
+ /* Spacer that reserves the width of columns outside the horizontal window. */
554
+ .kanuni-datagrid-spacer {
555
+ flex: none;
556
+ height: 100%;
557
+ }
558
+
559
+ /* ── Toolbar ──────────────────────────────────────────────── */
560
+ .kanuni-datagrid-toolbar {
561
+ display: flex;
562
+ align-items: center;
563
+ gap: 6px;
564
+ flex: none;
565
+ padding: var(--dg-chrome-padding-y) 8px;
566
+ background: var(--dg-surface);
567
+ border-bottom: 1px solid var(--dg-border);
568
+ font-size: var(--dg-chrome-font-size);
569
+ }
570
+
571
+ .kanuni-datagrid-toolbar-spacer {
572
+ flex: 1 1 auto;
573
+ }
574
+
575
+ .kanuni-datagrid-toolbar-search {
576
+ width: min(260px, 40%);
577
+ height: var(--dg-control-height);
578
+ padding: 0 8px;
579
+ border: 1px solid var(--dg-border-strong);
580
+ border-radius: var(--dg-radius-control);
581
+ background: var(--dg-surface);
582
+ color: var(--dg-text);
583
+ font: inherit;
584
+ font-size: var(--dg-control-font-size);
585
+ }
586
+
587
+ .kanuni-datagrid-toolbar-search:focus {
588
+ outline: none;
589
+ border-color: var(--dg-primary);
590
+ }
591
+
592
+ .kanuni-datagrid-toolbar > button {
593
+ padding: 3px 10px;
594
+ border: 1px solid var(--dg-border-strong);
595
+ border-radius: var(--dg-radius-control);
596
+ background: var(--dg-surface);
597
+ color: inherit;
598
+ font: inherit;
599
+ font-size: var(--dg-control-font-size);
600
+ cursor: pointer;
601
+ }
602
+
603
+ .kanuni-datagrid-toolbar > button:hover:not(:disabled) {
604
+ background: var(--dg-surface-hover);
605
+ }
606
+
607
+ .kanuni-datagrid-toolbar > button:disabled {
608
+ opacity: 0.6;
609
+ cursor: default;
610
+ }
611
+
612
+ .kanuni-datagrid-toolbar-error {
613
+ color: var(--dg-danger-text);
614
+ font-size: 11px;
615
+ }
616
+
617
+ /*
618
+ * The same message, but floating where a context menu just was — the cell
619
+ * menu's Export entries have nowhere else to report a missing optional peer.
620
+ * Sized and boxed like a menu so it reads as coming from one; it is a BUTTON
621
+ * because clicking it dismisses it, and a click target has to be one.
622
+ */
623
+ .kanuni-datagrid-menu-error {
624
+ position: fixed;
625
+ /* Same layer as the context menu it replaces — see the rule further down. */
626
+ z-index: 1200;
627
+ max-width: 320px;
628
+ padding: 8px 10px;
629
+ text-align: left;
630
+ background: var(--dg-surface);
631
+ border: 1px solid var(--dg-danger-text);
632
+ border-radius: var(--dg-radius);
633
+ box-shadow: var(--dg-shadow-menu);
634
+ font-size: var(--dg-font-size);
635
+ cursor: pointer;
636
+ }
637
+
638
+ .kanuni-datagrid-column-chooser .kanuni-datagrid-filter-item {
639
+ cursor: pointer;
640
+ }
641
+
642
+ /* ── Pager ────────────────────────────────────────────────── */
643
+ .kanuni-datagrid-pager {
644
+ display: flex;
645
+ align-items: center;
646
+ flex-wrap: wrap;
647
+ gap: 12px;
648
+ flex: none;
649
+ padding: 5px 8px;
650
+ background: var(--dg-surface);
651
+ border-top: 1px solid var(--dg-border);
652
+ font-size: var(--dg-chrome-font-size);
653
+ }
654
+
655
+ .kanuni-datagrid-pager-size {
656
+ display: inline-flex;
657
+ align-items: center;
658
+ gap: 6px;
659
+ color: var(--dg-text-muted);
660
+ }
661
+
662
+ .kanuni-datagrid-pager-size select {
663
+ padding: 2px 4px;
664
+ border: 1px solid var(--dg-border-strong);
665
+ border-radius: var(--dg-radius-control);
666
+ background: var(--dg-surface);
667
+ color: var(--dg-text);
668
+ font: inherit;
669
+ font-size: var(--dg-control-font-size);
670
+ }
671
+
672
+ .kanuni-datagrid-pager-range {
673
+ color: var(--dg-text-muted);
674
+ font-variant-numeric: tabular-nums;
675
+ }
676
+
677
+ .kanuni-datagrid-pager-buttons {
678
+ display: inline-flex;
679
+ align-items: center;
680
+ gap: 4px;
681
+ margin-left: auto;
682
+ }
683
+
684
+ .kanuni-datagrid-pager-buttons button {
685
+ min-width: 26px;
686
+ height: calc(var(--dg-control-height) - 2px);
687
+ border: 1px solid var(--dg-border-strong);
688
+ border-radius: var(--dg-radius-control);
689
+ background: var(--dg-surface);
690
+ color: inherit;
691
+ font: inherit;
692
+ cursor: pointer;
693
+ }
694
+
695
+ .kanuni-datagrid-pager-buttons button:hover:not(:disabled) {
696
+ background: var(--dg-surface-hover);
697
+ }
698
+
699
+ .kanuni-datagrid-pager-buttons button:disabled {
700
+ opacity: 0.45;
701
+ cursor: default;
702
+ }
703
+
704
+ .kanuni-datagrid-pager-current {
705
+ padding: 0 6px;
706
+ font-variant-numeric: tabular-nums;
707
+ }
708
+
709
+ /* ── Group panel ──────────────────────────────────────────── */
710
+ .kanuni-datagrid-group-panel {
711
+ display: flex;
712
+ align-items: flex-start;
713
+ flex-wrap: wrap;
714
+ gap: 8px;
715
+ flex: none;
716
+ padding: var(--dg-chrome-padding-y) 8px;
717
+ background: var(--dg-surface-header);
718
+ border-bottom: 1px solid var(--dg-border);
719
+ font-size: var(--dg-chrome-font-size);
720
+ }
721
+
722
+ /* ── Cascading levels ──────────────────────────────────────
723
+ One row per level, each indented past the one above and joined to it by an
724
+ elbow. Grouping is a hierarchy; a flat row of chips said nothing about which
725
+ column was the OUTER level, which is the only thing the order encodes. */
726
+ .kanuni-datagrid-group-levels {
727
+ display: flex;
728
+ flex-direction: column;
729
+ gap: 2px;
730
+ min-width: 0;
731
+ }
732
+
733
+ .kanuni-datagrid-group-level {
734
+ position: relative;
735
+ display: flex;
736
+ align-items: center;
737
+ /* --dg-group-level is set per chip from its index. */
738
+ padding-inline-start: calc(var(--dg-group-level, 0) * 18px);
739
+ }
740
+
741
+ /* The elbow: down from the parent chip, then right into this one. */
742
+ .kanuni-datagrid-group-level:not(:first-child)::before {
743
+ content: '';
744
+ position: absolute;
745
+ inset-inline-start: calc((var(--dg-group-level, 1) - 1) * 18px + 9px);
746
+ top: -3px;
747
+ bottom: 50%;
748
+ width: 9px;
749
+ border-inline-start: 1px solid var(--dg-border-strong);
750
+ border-block-end: 1px solid var(--dg-border-strong);
751
+ border-end-start-radius: 4px;
752
+ pointer-events: none;
753
+ }
754
+
755
+ /* Drop indicator while a level is dragged over another. */
756
+ .kanuni-datagrid-group-level--drop-before::after,
757
+ .kanuni-datagrid-group-level--drop-after::after {
758
+ content: '';
759
+ position: absolute;
760
+ inset-inline: calc(var(--dg-group-level, 0) * 18px) 0;
761
+ height: 2px;
762
+ border-radius: 1px;
763
+ background: var(--dg-primary);
764
+ pointer-events: none;
765
+ }
766
+
767
+ .kanuni-datagrid-group-level--drop-before::after {
768
+ top: -2px;
769
+ }
770
+
771
+ .kanuni-datagrid-group-level--drop-after::after {
772
+ bottom: -2px;
773
+ }
774
+
775
+ .kanuni-datagrid-group-panel-actions {
776
+ display: flex;
777
+ align-items: center;
778
+ gap: 8px;
779
+ margin-inline-start: auto;
780
+ }
781
+
782
+ .kanuni-datagrid-group-panel--over {
783
+ background: var(--dg-primary-soft);
784
+ outline: 2px dashed var(--dg-primary);
785
+ outline-offset: -3px;
786
+ }
787
+
788
+ .kanuni-datagrid-group-panel-hint {
789
+ color: var(--dg-text-muted);
790
+ }
791
+
792
+ .kanuni-datagrid-group-chip {
793
+ display: inline-flex;
794
+ align-items: center;
795
+ gap: 6px;
796
+ padding: 2px 6px 2px 10px;
797
+ border-radius: 999px;
798
+ background: var(--dg-primary);
799
+ color: var(--dg-on-primary);
800
+ font-weight: 600;
801
+ white-space: nowrap;
802
+ }
803
+
804
+ .kanuni-datagrid-group-chip[draggable='true'] {
805
+ cursor: grab;
806
+ }
807
+
808
+ .kanuni-datagrid-group-chip:focus-visible {
809
+ outline: 2px solid var(--dg-focus-ring);
810
+ outline-offset: 2px;
811
+ }
812
+
813
+ .kanuni-datagrid-group-chip button {
814
+ border: none;
815
+ background: transparent;
816
+ color: inherit;
817
+ font-size: 14px;
818
+ line-height: 1;
819
+ cursor: pointer;
820
+ }
821
+
822
+ .kanuni-datagrid-group-panel-actions > button {
823
+ padding: 2px 8px;
824
+ border: 1px solid var(--dg-border-strong);
825
+ border-radius: var(--dg-radius-control);
826
+ background: var(--dg-surface);
827
+ color: inherit;
828
+ font: inherit;
829
+ font-size: 12px;
830
+ cursor: pointer;
831
+ }
832
+
833
+ .kanuni-datagrid-group-select select {
834
+ padding: 2px 6px;
835
+ border: 1px solid var(--dg-border-strong);
836
+ border-radius: var(--dg-radius-control);
837
+ background: var(--dg-surface);
838
+ color: inherit;
839
+ font: inherit;
840
+ font-size: 12px;
841
+ }
842
+
843
+ /* ── Group rows ───────────────────────────────────────────── */
844
+ .kanuni-datagrid-group-row {
845
+ background: var(--dg-group-bg);
846
+ cursor: pointer;
847
+ font-weight: 600;
848
+ }
849
+
850
+ .kanuni-datagrid-group-row:hover {
851
+ background: var(--dg-surface-hover);
852
+ }
853
+
854
+ .kanuni-datagrid-group-cell {
855
+ /* Sticky so the label stays readable while scrolling horizontally. */
856
+ position: sticky;
857
+ inset-inline-start: 0;
858
+ display: flex;
859
+ align-items: center;
860
+ gap: 6px;
861
+ height: 100%;
862
+ padding: 0 var(--dg-cell-padding-x);
863
+ white-space: nowrap;
864
+ }
865
+
866
+ .kanuni-datagrid-group-twisty {
867
+ display: inline-block;
868
+ width: 12px;
869
+ color: var(--dg-text-muted);
870
+ }
871
+
872
+ .kanuni-datagrid-group-caption {
873
+ color: var(--dg-text-muted);
874
+ font-weight: 400;
875
+ }
876
+
877
+ .kanuni-datagrid-group-count {
878
+ color: var(--dg-text-muted);
879
+ font-weight: 400;
880
+ }
881
+
882
+ /* Nested levels step back toward the body colour so depth reads at a glance
883
+ without needing to count the indent. */
884
+ .kanuni-datagrid-group-row--level-1 {
885
+ background: color-mix(in srgb, var(--dg-surface-header) 60%, var(--dg-surface));
886
+ }
887
+
888
+ .kanuni-datagrid-group-row--level-2 {
889
+ background: color-mix(in srgb, var(--dg-surface-header) 35%, var(--dg-surface));
890
+ }
891
+
892
+ .kanuni-datagrid-group-row--level-3 {
893
+ background: color-mix(in srgb, var(--dg-surface-header) 18%, var(--dg-surface));
894
+ }
895
+
896
+ /* Aggregates trail the label; they must not push the count off-screen, so the
897
+ whole strip stays on one line and simply clips at the viewport edge. */
898
+ .kanuni-datagrid-group-aggregate {
899
+ margin-left: 12px;
900
+ font-weight: 600;
901
+ font-variant-numeric: tabular-nums;
902
+ }
903
+
904
+ .kanuni-datagrid-group-aggregate-label {
905
+ color: var(--dg-text-muted);
906
+ font-weight: 400;
907
+ }
908
+
909
+ .kanuni-datagrid-group-chip-level {
910
+ display: inline-flex;
911
+ align-items: center;
912
+ justify-content: center;
913
+ min-width: 15px;
914
+ height: 15px;
915
+ border-radius: 999px;
916
+ /* A wash of the chip's own FOREGROUND, not a fixed white: on a gold chip the
917
+ text is near-black, and a white pip on it disappeared. */
918
+ background: color-mix(in srgb, var(--dg-on-primary) 25%, transparent);
919
+ font-size: 10px;
920
+ font-weight: 700;
921
+ }
922
+
923
+ .kanuni-datagrid-group-chip button[disabled] {
924
+ opacity: 0.4;
925
+ cursor: default;
926
+ }
927
+
928
+ .kanuni-datagrid-group-row--focused {
929
+ outline: 2px solid var(--dg-focus-ring);
930
+ outline-offset: -2px;
931
+ }
932
+
933
+ /* ── Master-detail ────────────────────────────────────────── */
934
+ .kanuni-datagrid-detail-toggle-cell {
935
+ padding: 0;
936
+ }
937
+
938
+ .kanuni-datagrid-detail-toggle {
939
+ width: 100%;
940
+ height: 100%;
941
+ border: none;
942
+ background: transparent;
943
+ color: var(--dg-text-muted);
944
+ font-size: 11px;
945
+ line-height: 1;
946
+ cursor: pointer;
947
+ }
948
+
949
+ .kanuni-datagrid-detail-toggle:hover {
950
+ color: var(--dg-text);
951
+ }
952
+
953
+ /* The panel is sized in whole rows by the component (span x rowHeight); this
954
+ rule only styles it. Overriding the height here would desync it from the
955
+ filler slots the virtualizer reserved. */
956
+ .kanuni-datagrid-detail-row {
957
+ background: var(--dg-surface);
958
+ border-bottom: 1px solid var(--dg-border);
959
+ }
960
+
961
+ .kanuni-datagrid-detail-cell {
962
+ position: sticky;
963
+ inset-inline-start: 0;
964
+ box-sizing: border-box;
965
+ width: 100%;
966
+ height: 100%;
967
+ padding: 8px 12px;
968
+ overflow: auto;
969
+ border-inline-start: 3px solid var(--dg-primary);
970
+ }
971
+
972
+ /* ── In-grid edit form (mode: 'form') ─────────────────────── */
973
+ /* Sticky to the scroller's inline start so the form stays put while the
974
+ columns scroll under it — a form that slides off to the left with column 40
975
+ is a form the user has to chase. Width is the VIEWPORT's, not the canvas's,
976
+ for the same reason. */
977
+ .kanuni-datagrid-editform-row {
978
+ background: var(--dg-surface);
979
+ border-bottom: 1px solid var(--dg-border);
980
+ }
981
+
982
+ .kanuni-datagrid-editform-cell {
983
+ position: sticky;
984
+ inset-inline-start: 0;
985
+ box-sizing: border-box;
986
+ /* The VIEWPORT's width, not the row's — see `--dg-viewport-width`. Falls
987
+ back to the row when nothing has measured yet (SSR, first paint). */
988
+ width: var(--dg-viewport-width, 100%);
989
+ height: 100%;
990
+ padding: 0;
991
+ border-inline-start: 3px solid var(--dg-primary);
992
+ }
993
+
994
+ /* ── Summary bar ──────────────────────────────────────────── */
995
+ .kanuni-datagrid-summary-bar {
996
+ display: flex;
997
+ align-items: center;
998
+ flex-wrap: wrap;
999
+ gap: 16px;
1000
+ flex: none;
1001
+ padding: 5px 8px;
1002
+ background: var(--dg-footer-bg);
1003
+ border-top: var(--dg-header-border);
1004
+ font-size: var(--dg-chrome-font-size);
1005
+ }
1006
+
1007
+ .kanuni-datagrid-summary-item {
1008
+ display: inline-flex;
1009
+ align-items: baseline;
1010
+ gap: 4px;
1011
+ }
1012
+
1013
+ .kanuni-datagrid-summary-label {
1014
+ color: var(--dg-text-muted);
1015
+ }
1016
+
1017
+ /* ── Selection ────────────────────────────────────────────── */
1018
+ .kanuni-datagrid-row--selected,
1019
+ .kanuni-datagrid-row--selected .kanuni-datagrid-cell--pinned {
1020
+ background: var(--dg-selected-bg);
1021
+ }
1022
+
1023
+ .kanuni-datagrid-row--selected:hover,
1024
+ .kanuni-datagrid-row--selected:hover .kanuni-datagrid-cell--pinned {
1025
+ background: var(--dg-selected-bg);
1026
+ filter: brightness(0.97);
1027
+ }
1028
+
1029
+ .kanuni-datagrid-select-cell {
1030
+ justify-content: center;
1031
+ padding: 0;
1032
+ }
1033
+
1034
+ .kanuni-datagrid-select-cell input {
1035
+ accent-color: var(--dg-primary);
1036
+ cursor: pointer;
1037
+ }
1038
+
1039
+ /* ── Row numbers (gap analysis P3.5) ───────────────────────── */
1040
+ /* Muted and tabular on purpose. The number is an address, not data: at full
1041
+ text weight it competes with the first real column, and proportional digits
1042
+ make a column of numbers ragged down its right edge. */
1043
+ .kanuni-datagrid-rownum-cell {
1044
+ color: var(--dg-text-muted);
1045
+ font-variant-numeric: tabular-nums;
1046
+ background: var(--dg-header-bg);
1047
+ user-select: none;
1048
+ }
1049
+
1050
+ .kanuni-datagrid-row--selected .kanuni-datagrid-rownum-cell {
1051
+ background: var(--dg-selected-bg);
1052
+ }
1053
+
1054
+ /* ── Keyboard focus ───────────────────────────────────────── */
1055
+ /* Focus is virtual (aria-activedescendant), so it must be drawn explicitly —
1056
+ there is no browser focus ring on the cell itself. `inset` keeps the outline
1057
+ from being clipped by the neighbouring cell. */
1058
+ .kanuni-datagrid-cell--focused {
1059
+ outline: 2px solid var(--dg-focus-ring);
1060
+ outline-offset: -2px;
1061
+ }
1062
+
1063
+ .kanuni-datagrid-scroller:focus {
1064
+ outline: none;
1065
+ }
1066
+
1067
+ .kanuni-datagrid-scroller:focus-visible {
1068
+ outline: 2px solid var(--dg-focus-ring);
1069
+ outline-offset: -2px;
1070
+ }
1071
+
1072
+ /* Visually hidden but still read aloud. */
1073
+ .kanuni-datagrid-sr-only {
1074
+ position: absolute;
1075
+ width: 1px;
1076
+ height: 1px;
1077
+ margin: -1px;
1078
+ padding: 0;
1079
+ overflow: hidden;
1080
+ clip: rect(0, 0, 0, 0);
1081
+ white-space: nowrap;
1082
+ border: 0;
1083
+ }
1084
+
1085
+ /* ── Header filter button ──────────────────────────────────
1086
+ Hidden until the header is hovered or focused (`filterIconMode: 'hover'`,
1087
+ the default). `visibility` rather than `display` so the icon does not push
1088
+ the label around when it appears — the space is reserved either way. */
1089
+ .kanuni-datagrid-filter-button {
1090
+ display: inline-flex;
1091
+ align-items: center;
1092
+ justify-content: center;
1093
+ flex: none;
1094
+ margin-inline-start: auto;
1095
+ padding: 1px 3px;
1096
+ border: none;
1097
+ border-radius: 3px;
1098
+ background: transparent;
1099
+ /* Lives INSIDE the header cell, so it follows the header's foreground: on an
1100
+ ink header the body's muted slate is barely visible. */
1101
+ color: var(--dg-header-text-muted);
1102
+ line-height: 0;
1103
+ cursor: pointer;
1104
+ visibility: hidden;
1105
+ }
1106
+
1107
+ .kanuni-datagrid-header-cell:hover > .kanuni-datagrid-filter-button,
1108
+ .kanuni-datagrid-header-cell:focus-within > .kanuni-datagrid-filter-button,
1109
+ .kanuni-datagrid-filter-button--pinned,
1110
+ /* An ACTIVE filter is the only sign that rows are being withheld, so it stays
1111
+ visible in both modes — hiding it would make the grid look simply empty. */
1112
+ .kanuni-datagrid-filter-button--active {
1113
+ visibility: visible;
1114
+ }
1115
+
1116
+ .kanuni-datagrid-filter-button:hover {
1117
+ background: var(--dg-primary-soft);
1118
+ color: var(--dg-text);
1119
+ /* `--dg-primary-soft` is a LIGHT tint in every light theme, so the label on
1120
+ it is the body text, not the header's — this is the one spot in the header
1121
+ that deliberately does not follow `--dg-header-text`. */
1122
+ }
1123
+
1124
+ .kanuni-datagrid-filter-button--active {
1125
+ color: var(--dg-primary);
1126
+ background: var(--dg-primary-soft);
1127
+ }
1128
+
1129
+ /* ── Column reorder (drag & drop) ──────────────────────────
1130
+ The indicator is an inset shadow, not a border: a border would change the
1131
+ cell's box and shift every column to its right by a pixel mid-drag. */
1132
+ .kanuni-datagrid-header-cell--drop-before {
1133
+ box-shadow: inset 3px 0 0 -1px var(--dg-primary);
1134
+ }
1135
+
1136
+ .kanuni-datagrid-header-cell--drop-after {
1137
+ box-shadow: inset -3px 0 0 -1px var(--dg-primary);
1138
+ }
1139
+
1140
+ [dir='rtl'] .kanuni-datagrid-header-cell--drop-before {
1141
+ box-shadow: inset -3px 0 0 -1px var(--dg-primary);
1142
+ }
1143
+
1144
+ [dir='rtl'] .kanuni-datagrid-header-cell--drop-after {
1145
+ box-shadow: inset 3px 0 0 -1px var(--dg-primary);
1146
+ }
1147
+
1148
+ /* ── Column resize handle ─────────────────────────────────── */
1149
+ .kanuni-datagrid-resize-handle {
1150
+ position: absolute;
1151
+ inset-block: 0;
1152
+ inset-inline-end: -3px;
1153
+ width: 7px;
1154
+ z-index: 5;
1155
+ cursor: col-resize;
1156
+ /* Invisible by default; the line under the cursor is what marks the seam. */
1157
+ background: transparent;
1158
+ touch-action: none;
1159
+ }
1160
+
1161
+ .kanuni-datagrid-resize-handle:hover::after,
1162
+ .kanuni-datagrid-header-cell--resizing .kanuni-datagrid-resize-handle::after {
1163
+ content: '';
1164
+ position: absolute;
1165
+ inset-block: 4px;
1166
+ inset-inline-start: 3px;
1167
+ width: 2px;
1168
+ border-radius: 1px;
1169
+ background: var(--dg-primary);
1170
+ }
1171
+
1172
+ /* ── Loading overlay (busy + percentage) ──────────────────── */
1173
+ .kanuni-datagrid-loading-overlay {
1174
+ position: absolute;
1175
+ inset: 0;
1176
+ z-index: 20;
1177
+ display: flex;
1178
+ align-items: center;
1179
+ justify-content: center;
1180
+ background: color-mix(in srgb, var(--dg-bg) 62%, transparent);
1181
+ /* Pointer-transparent: a recompute triggered by typing must not swallow the
1182
+ next keystroke aimed at the filter box. */
1183
+ pointer-events: none;
1184
+ }
1185
+
1186
+ /* Exporting is the one phase that takes the grid away, so it looks like it.
1187
+ The sheet is nearly opaque instead of a hint, it swallows clicks instead of
1188
+ letting them fall through to a grid that cannot answer them, and the cursor
1189
+ says wait. See the comment on `data-blocking` in GridLoadingOverlay.tsx. */
1190
+ .kanuni-datagrid-loading-overlay[data-blocking='true'] {
1191
+ background: color-mix(in srgb, var(--dg-bg) 92%, transparent);
1192
+ pointer-events: auto;
1193
+ cursor: progress;
1194
+ }
1195
+
1196
+ .kanuni-datagrid-loading-card {
1197
+ display: flex;
1198
+ flex-direction: column;
1199
+ align-items: center;
1200
+ gap: 10px;
1201
+ padding: 18px 24px;
1202
+ border: 1px solid var(--dg-border);
1203
+ border-radius: calc(var(--dg-radius) + 4px);
1204
+ background: var(--dg-surface);
1205
+ box-shadow: var(--dg-shadow-card);
1206
+ color: var(--dg-text);
1207
+ font-size: var(--dg-chrome-font-size);
1208
+ }
1209
+
1210
+ .kanuni-datagrid-loading-spinner {
1211
+ width: 26px;
1212
+ height: 26px;
1213
+ border: 2px solid transparent;
1214
+ border-bottom-color: var(--dg-primary);
1215
+ border-radius: 50%;
1216
+ animation: kanuni-datagrid-spin 1s linear infinite;
1217
+ }
1218
+
1219
+ .kanuni-datagrid-loading-label {
1220
+ font-weight: 500;
1221
+ color: var(--dg-text-muted);
1222
+ }
1223
+
1224
+ .kanuni-datagrid-loading-progress {
1225
+ display: flex;
1226
+ flex-direction: column;
1227
+ align-items: center;
1228
+ gap: 6px;
1229
+ width: 220px;
1230
+ }
1231
+
1232
+ .kanuni-datagrid-loading-track {
1233
+ width: 100%;
1234
+ height: 6px;
1235
+ border-radius: 999px;
1236
+ overflow: hidden;
1237
+ background: color-mix(in srgb, var(--dg-border) 70%, transparent);
1238
+ }
1239
+
1240
+ .kanuni-datagrid-loading-fill {
1241
+ height: 100%;
1242
+ border-radius: 999px;
1243
+ background: var(--dg-primary);
1244
+ transition: width 160ms ease-out;
1245
+ }
1246
+
1247
+ .kanuni-datagrid-loading-percent {
1248
+ font-size: 11px;
1249
+ font-weight: 600;
1250
+ font-variant-numeric: tabular-nums;
1251
+ }
1252
+
1253
+ /* ── Filter panel (cxGrid) ────────────────────────────────────
1254
+ Under the footer, outside the scroller: the conditions describe the whole
1255
+ result, so they must not scroll away with the rows. */
1256
+ .kanuni-datagrid-filter-panel {
1257
+ display: flex;
1258
+ align-items: center;
1259
+ gap: 6px;
1260
+ flex: none;
1261
+ padding: 4px 8px;
1262
+ background: var(--dg-surface-header);
1263
+ border-top: 1px solid var(--dg-border);
1264
+ font-size: var(--dg-chrome-font-size);
1265
+ color: var(--dg-text);
1266
+ }
1267
+
1268
+ /* Suspended: the conditions are still there, just not being applied. Dimmed
1269
+ rather than hidden — there has to be something to switch back on. */
1270
+ .kanuni-datagrid-filter-panel--suspended .kanuni-datagrid-filter-panel-text {
1271
+ opacity: 0.5;
1272
+ text-decoration: line-through;
1273
+ }
1274
+
1275
+ .kanuni-datagrid-filter-panel-clear,
1276
+ .kanuni-datagrid-filter-panel-builder {
1277
+ display: inline-flex;
1278
+ align-items: center;
1279
+ justify-content: center;
1280
+ flex: none;
1281
+ width: 18px;
1282
+ height: 18px;
1283
+ padding: 0;
1284
+ border: 1px solid transparent;
1285
+ border-radius: var(--dg-radius-control);
1286
+ background: transparent;
1287
+ color: var(--dg-text-muted);
1288
+ font: inherit;
1289
+ line-height: 1;
1290
+ cursor: pointer;
1291
+ }
1292
+
1293
+ .kanuni-datagrid-filter-panel-clear:hover:not(:disabled),
1294
+ .kanuni-datagrid-filter-panel-builder:hover:not(:disabled) {
1295
+ background: var(--dg-surface-hover);
1296
+ border-color: var(--dg-border-strong);
1297
+ color: var(--dg-text);
1298
+ }
1299
+
1300
+ .kanuni-datagrid-filter-panel-clear:disabled,
1301
+ .kanuni-datagrid-filter-panel-builder:disabled {
1302
+ opacity: 0.4;
1303
+ cursor: default;
1304
+ }
1305
+
1306
+ .kanuni-datagrid-filter-panel-toggle {
1307
+ flex: none;
1308
+ margin: 0;
1309
+ accent-color: var(--dg-primary);
1310
+ cursor: pointer;
1311
+ }
1312
+
1313
+ .kanuni-datagrid-filter-panel-text {
1314
+ flex: 1 1 auto;
1315
+ min-width: 0;
1316
+ overflow: hidden;
1317
+ text-overflow: ellipsis;
1318
+ white-space: nowrap;
1319
+ }
1320
+
1321
+ .kanuni-datagrid-filter-panel-builder {
1322
+ margin-inline-start: auto;
1323
+ }
1324
+
1325
+ /* Shown only for phases that can actually stop — see CANCELLABLE in the overlay. */
1326
+ .kanuni-datagrid-loading-cancel {
1327
+ margin-top: 8px;
1328
+ padding: 3px 12px;
1329
+ border: 1px solid var(--dg-border);
1330
+ border-radius: var(--dg-radius);
1331
+ background: var(--dg-surface);
1332
+ color: inherit;
1333
+ font: inherit;
1334
+ font-size: 11px;
1335
+ cursor: pointer;
1336
+ }
1337
+
1338
+ .kanuni-datagrid-loading-cancel:hover {
1339
+ background: var(--dg-surface-hover);
1340
+ }
1341
+
1342
+ @media (prefers-reduced-motion: reduce) {
1343
+ .kanuni-datagrid-loading-spinner {
1344
+ animation-duration: 3s;
1345
+ }
1346
+
1347
+ .kanuni-datagrid-loading-fill {
1348
+ transition: none;
1349
+ }
1350
+ }
1351
+
1352
+ /* ── Filter row (under the header, same sticky geometry) ──── */
1353
+ .kanuni-datagrid-filter-row {
1354
+ position: sticky;
1355
+ /* Clears the BANDS as well as the header. `--dg-band-offset` is published by
1356
+ the component (0 when there are no bands); the header height stays a
1357
+ variable here so density keeps working. Sticking at the header height alone
1358
+ parked this row inside the header on any banded grid, and the two drew over
1359
+ each other as soon as the user scrolled. */
1360
+ top: calc(var(--dg-band-offset, 0px) + var(--dg-header-height));
1361
+ z-index: 3;
1362
+ display: flex;
1363
+ height: var(--dg-header-height);
1364
+ background: var(--dg-surface-header);
1365
+ border-bottom: var(--dg-header-border);
1366
+ }
1367
+
1368
+ .kanuni-datagrid-filter-cell {
1369
+ display: flex;
1370
+ align-items: center;
1371
+ flex: none;
1372
+ height: 100%;
1373
+ padding: 0 4px;
1374
+ background: var(--dg-surface-header);
1375
+ border-inline-end: var(--dg-header-cell-border);
1376
+ overflow: hidden;
1377
+ }
1378
+
1379
+ .kanuni-datagrid-filter-cell--pinned {
1380
+ position: sticky;
1381
+ z-index: 4;
1382
+ background: var(--dg-surface-header);
1383
+ }
1384
+
1385
+ .kanuni-datagrid-filter-input {
1386
+ width: 100%;
1387
+ min-width: 0;
1388
+ height: var(--dg-filter-input-height);
1389
+ padding: 0 6px;
1390
+ border: 1px solid var(--dg-border-strong);
1391
+ border-radius: var(--dg-radius-control);
1392
+ background: var(--dg-surface);
1393
+ color: var(--dg-text);
1394
+ font: inherit;
1395
+ font-size: var(--dg-control-font-size);
1396
+ }
1397
+
1398
+ .kanuni-datagrid-filter-input:focus {
1399
+ outline: none;
1400
+ border-color: var(--dg-primary);
1401
+ }
1402
+
1403
+ /* ── Header filter popup (portaled — see GridPortal) ──────── */
1404
+ .kanuni-datagrid-portal {
1405
+ /* The portal host must not paint or capture anything itself. */
1406
+ position: static;
1407
+ border: none;
1408
+ background: transparent;
1409
+ overflow: visible;
1410
+ }
1411
+
1412
+ /* ── Band fold toggle (P2.6) ──────────────────────────────── */
1413
+ .kanuni-datagrid-band-toggle {
1414
+ display: inline-flex;
1415
+ align-items: center;
1416
+ justify-content: center;
1417
+ flex: none;
1418
+ width: 16px;
1419
+ height: 16px;
1420
+ margin-inline-end: 4px;
1421
+ padding: 0;
1422
+ border: none;
1423
+ border-radius: 3px;
1424
+ background: transparent;
1425
+ color: var(--dg-text-muted);
1426
+ font-size: 10px;
1427
+ line-height: 1;
1428
+ cursor: pointer;
1429
+ }
1430
+
1431
+ .kanuni-datagrid-band-toggle:hover {
1432
+ background: var(--dg-primary-soft);
1433
+ color: var(--dg-text);
1434
+ }
1435
+
1436
+ .kanuni-datagrid-band-toggle:focus-visible {
1437
+ outline: 2px solid var(--dg-focus-ring);
1438
+ outline-offset: -1px;
1439
+ }
1440
+
1441
+ /* ── Search highlight (P2.4) ──────────────────────────────── */
1442
+ /* A <mark>, so the match is in the accessibility tree as emphasis rather than
1443
+ as a colour only. The browser default is a hard yellow that fights every
1444
+ theme; these follow the accent instead. */
1445
+ .kanuni-datagrid-highlight {
1446
+ padding: 0;
1447
+ border-radius: 2px;
1448
+ background: var(--dg-highlight-bg);
1449
+ color: var(--dg-highlight-text);
1450
+ font-weight: 600;
1451
+ }
1452
+
1453
+ /* ── Cell / header tooltip (P2.1) ─────────────────────────── */
1454
+ .kanuni-datagrid-tooltip {
1455
+ position: fixed;
1456
+ /* Above every popup: a tooltip describes whatever is under the pointer, and
1457
+ the pointer can be over a popup. */
1458
+ z-index: 1100;
1459
+ /* The tooltip must never be the hover target — it appears UNDER the pointer's
1460
+ path, and taking the pointer would fire mouseout on the cell that asked for
1461
+ it, hiding the tooltip, which un-hovers it, which shows it again. */
1462
+ pointer-events: none;
1463
+ max-width: 420px;
1464
+ padding: 4px 8px;
1465
+ border: 1px solid var(--dg-border-strong);
1466
+ border-radius: var(--dg-radius);
1467
+ background: var(--dg-surface);
1468
+ color: var(--dg-text);
1469
+ box-shadow: var(--dg-shadow-popup);
1470
+ font-size: var(--dg-font-size);
1471
+ line-height: 1.45;
1472
+ /* Long values wrap instead of running off-screen; a pasted paragraph in a
1473
+ cell is exactly the case the tooltip exists for. */
1474
+ white-space: pre-wrap;
1475
+ overflow-wrap: anywhere;
1476
+ }
1477
+
1478
+ .kanuni-datagrid-filter-popup {
1479
+ position: fixed;
1480
+ z-index: 1000;
1481
+ display: flex;
1482
+ flex-direction: column;
1483
+ gap: 6px;
1484
+ width: 260px;
1485
+ max-height: 340px;
1486
+ padding: 8px;
1487
+ border: 1px solid var(--dg-border-strong);
1488
+ border-radius: var(--dg-radius);
1489
+ /* Explicit background: a transparent popup was the PivotGrid drill-down bug. */
1490
+ background: var(--dg-surface);
1491
+ color: var(--dg-text);
1492
+ box-shadow: var(--dg-shadow-popup);
1493
+ font-size: var(--dg-font-size);
1494
+ }
1495
+
1496
+ .kanuni-datagrid-filter-search {
1497
+ width: 100%;
1498
+ height: var(--dg-control-height);
1499
+ padding: 0 8px;
1500
+ border: 1px solid var(--dg-border-strong);
1501
+ border-radius: var(--dg-radius-control);
1502
+ background: var(--dg-surface);
1503
+ color: var(--dg-text);
1504
+ font: inherit;
1505
+ font-size: var(--dg-control-font-size);
1506
+ }
1507
+
1508
+ /* "There are more values than these." Sits directly under the search box
1509
+ because the search box is the answer to it — on a capped list, search scans
1510
+ the whole dataset rather than the values already on screen. */
1511
+ .kanuni-datagrid-filter-truncated {
1512
+ margin: 6px 0 0;
1513
+ font-size: 11px;
1514
+ line-height: 1.4;
1515
+ color: var(--dg-text-muted);
1516
+ }
1517
+
1518
+ .kanuni-datagrid-filter-actions {
1519
+ display: flex;
1520
+ gap: 6px;
1521
+ }
1522
+
1523
+ .kanuni-datagrid-filter-actions button,
1524
+ .kanuni-datagrid-filter-footer button {
1525
+ padding: 3px 8px;
1526
+ border: 1px solid var(--dg-border-strong);
1527
+ border-radius: var(--dg-radius-control);
1528
+ background: var(--dg-surface);
1529
+ color: var(--dg-text);
1530
+ font: inherit;
1531
+ font-size: var(--dg-control-font-size);
1532
+ cursor: pointer;
1533
+ }
1534
+
1535
+ .kanuni-datagrid-filter-actions button:hover,
1536
+ .kanuni-datagrid-filter-footer button:hover {
1537
+ background: var(--dg-surface-hover);
1538
+ }
1539
+
1540
+ .kanuni-datagrid-filter-primary {
1541
+ background: var(--dg-primary) !important;
1542
+ border-color: var(--dg-primary) !important;
1543
+ color: var(--dg-on-primary) !important;
1544
+ }
1545
+
1546
+ .kanuni-datagrid-filter-list {
1547
+ flex: 1 1 auto;
1548
+ overflow-y: auto;
1549
+ /* Windowed list — scroll anchoring off for the same reason as the main
1550
+ scroller: rows repositioning during scroll read as content shifts and the
1551
+ browser fights the gesture. See `.kanuni-datagrid-scroller`. */
1552
+ overflow-anchor: none;
1553
+ /* Stop the wheel from CHAINING into the grid once the list hits its end.
1554
+ Without this, wheeling to the bottom of the list keeps going and scrolls
1555
+ the rows underneath — which moves the anchor and closes the popup, in the
1556
+ middle of a gesture the user meant for the list. */
1557
+ overscroll-behavior: contain;
1558
+ border: 1px solid var(--dg-border);
1559
+ border-radius: var(--dg-radius-control);
1560
+ }
1561
+
1562
+ .kanuni-datagrid-filter-item {
1563
+ display: flex;
1564
+ align-items: center;
1565
+ gap: 6px;
1566
+ padding: 3px 6px;
1567
+ cursor: pointer;
1568
+ white-space: nowrap;
1569
+ overflow: hidden;
1570
+ text-overflow: ellipsis;
1571
+ /* FIXED, and the virtualizer depends on it: the list renders only the rows in
1572
+ view and positions them by `index x FILTER_ITEM_HEIGHT`. A label that
1573
+ wrapped to two lines would put every row below it out of place. Single-line
1574
+ is already enforced by `white-space: nowrap` above; this makes the height
1575
+ the popup computes with a fact rather than an assumption.
1576
+ Keep in sync with FILTER_ITEM_HEIGHT in GridHeaderFilterPopup.tsx. */
1577
+ height: 25px;
1578
+ box-sizing: border-box;
1579
+ flex: none;
1580
+ }
1581
+
1582
+ .kanuni-datagrid-filter-item:hover,
1583
+ .kanuni-datagrid-filter-item--active {
1584
+ background: var(--dg-surface-hover);
1585
+ }
1586
+
1587
+ .kanuni-datagrid-filter-item input {
1588
+ flex: none;
1589
+ accent-color: var(--dg-primary);
1590
+ }
1591
+
1592
+ .kanuni-datagrid-filter-empty {
1593
+ padding: 10px;
1594
+ color: var(--dg-text-muted);
1595
+ text-align: center;
1596
+ }
1597
+
1598
+ .kanuni-datagrid-filter-footer {
1599
+ display: flex;
1600
+ justify-content: flex-end;
1601
+ gap: 6px;
1602
+ }
1603
+
1604
+ /* ── Empty state ──────────────────────────────────────────── */
1605
+ .kanuni-datagrid-empty {
1606
+ display: flex;
1607
+ align-items: center;
1608
+ justify-content: center;
1609
+ padding: 32px 16px;
1610
+ color: var(--dg-text-muted);
1611
+ font-size: var(--dg-font-size);
1612
+ }
1613
+
1614
+ /* ── Accessibility ────────────────────────────────────────── */
1615
+ .kanuni-datagrid-root :focus-visible {
1616
+ outline: 2px solid var(--dg-focus-ring);
1617
+ outline-offset: -2px;
1618
+ }
1619
+
1620
+ @media (prefers-reduced-motion: reduce) {
1621
+ .kanuni-datagrid-viewport {
1622
+ will-change: auto;
1623
+ }
1624
+ }
1625
+
1626
+ /* ── Remote loading rows (P10) ────────────────────────────── */
1627
+ /* A row that exists on the server but has not arrived. It keeps its slot so
1628
+ the scrollbar and every row index stay exact while the block is in flight. */
1629
+ .kanuni-datagrid-row--loading {
1630
+ align-items: center;
1631
+ }
1632
+
1633
+ .kanuni-datagrid-loading-cell {
1634
+ position: sticky;
1635
+ inset-inline-start: 0;
1636
+ flex: 1;
1637
+ border-inline-end: none;
1638
+ }
1639
+
1640
+ .kanuni-datagrid-loading-bar {
1641
+ display: block;
1642
+ width: 40%;
1643
+ max-width: 240px;
1644
+ height: 8px;
1645
+ border-radius: 4px;
1646
+ background: var(--dg-surface-hover);
1647
+ animation: kanuni-datagrid-pulse 1.2s ease-in-out infinite;
1648
+ }
1649
+
1650
+ @keyframes kanuni-datagrid-pulse {
1651
+ 0%,
1652
+ 100% {
1653
+ opacity: 0.45;
1654
+ }
1655
+ 50% {
1656
+ opacity: 1;
1657
+ }
1658
+ }
1659
+
1660
+ /* Respect a reduced-motion preference: the pulse is decoration, and a user who
1661
+ asked for stillness should get a static placeholder. */
1662
+ @media (prefers-reduced-motion: reduce) {
1663
+ .kanuni-datagrid-loading-bar {
1664
+ animation: none;
1665
+ }
1666
+ }
1667
+
1668
+
1669
+ /* ── RTL ──────────────────────────────────────────────────────
1670
+ Insets are logical (inset-inline-*), so sticky columns flip on their own.
1671
+ What does NOT flip automatically is a directional SHADOW: the edge shadow
1672
+ that separates a left-pinned block from the scrolling body has to fall on
1673
+ the other side when the writing direction reverses. */
1674
+ .kanuni-datagrid-root[dir='rtl'] .kanuni-datagrid-pin-edge-left,
1675
+ [dir='rtl'] .kanuni-datagrid-root .kanuni-datagrid-pin-edge-left {
1676
+ box-shadow: var(--dg-shadow-pinned-right);
1677
+ }
1678
+
1679
+ .kanuni-datagrid-root[dir='rtl'] .kanuni-datagrid-pin-edge-right,
1680
+ [dir='rtl'] .kanuni-datagrid-root .kanuni-datagrid-pin-edge-right {
1681
+ box-shadow: var(--dg-shadow-pinned);
1682
+ }
1683
+
1684
+ /* The sort caret and the resize handle are positioned, not flowed. */
1685
+ .kanuni-datagrid-root[dir='rtl'] .kanuni-datagrid-resize-handle,
1686
+ [dir='rtl'] .kanuni-datagrid-root .kanuni-datagrid-resize-handle {
1687
+ right: auto;
1688
+ left: 0;
1689
+ }
1690
+
1691
+ /* ── Density presets ──────────────────────────────────────────
1692
+ The ROW HEIGHT is not set here: it is a core value (the virtualizer runs on
1693
+ it), so the component resolves the preset into `rowHeight` and core feeds it
1694
+ back as --dg-row-height. What lives here is everything CSS owns — header
1695
+ height, padding and type scale — so the two can never disagree about the
1696
+ row geometry. */
1697
+ .kanuni-datagrid-root.kanuni-datagrid-density-compact {
1698
+ --dg-header-height: 28px;
1699
+ --dg-cell-padding-x: 6px;
1700
+ --dg-font-size: 12px;
1701
+ --dg-header-font-size: 11px;
1702
+ }
1703
+
1704
+ .kanuni-datagrid-root.kanuni-datagrid-density-comfortable {
1705
+ --dg-header-height: 40px;
1706
+ --dg-cell-padding-x: 12px;
1707
+ --dg-font-size: 14px;
1708
+ --dg-header-font-size: 13px;
1709
+ }
1710
+
1711
+ /* ── Row context menu ─────────────────────────────────────── */
1712
+ .kanuni-datagrid-context-menu {
1713
+ position: fixed;
1714
+ z-index: 1200;
1715
+ min-width: 180px;
1716
+ max-width: 320px;
1717
+ /*
1718
+ * A menu longer than the screen scrolls instead of running off it: the
1719
+ * clamp in `GridContextMenu` can only move a menu that FITS, and an app row
1720
+ * menu with a dozen entries plus the built-ins does not.
1721
+ *
1722
+ * This is safe only because the SUBMENU is portaled as a sibling. While it
1723
+ * was a child, any overflow value other than `visible` clipped it away
1724
+ * entirely — which is why this rule was removed once before.
1725
+ */
1726
+ max-height: calc(100vh - 16px);
1727
+ overflow-y: auto;
1728
+ padding: 4px;
1729
+ border: 1px solid var(--dg-border-strong);
1730
+ border-radius: var(--dg-radius);
1731
+ background: var(--dg-surface);
1732
+ color: var(--dg-text);
1733
+ box-shadow: var(--dg-shadow-menu);
1734
+ font-size: var(--dg-font-size);
1735
+ }
1736
+
1737
+ .kanuni-datagrid-context-menu:focus {
1738
+ outline: none;
1739
+ }
1740
+
1741
+ .kanuni-datagrid-menu-row {
1742
+ position: relative;
1743
+ }
1744
+
1745
+ .kanuni-datagrid-menu-item {
1746
+ display: flex;
1747
+ align-items: center;
1748
+ gap: 8px;
1749
+ width: 100%;
1750
+ padding: 5px 8px;
1751
+ border: none;
1752
+ border-radius: var(--dg-radius-control);
1753
+ background: transparent;
1754
+ color: inherit;
1755
+ font: inherit;
1756
+ text-align: start;
1757
+ cursor: pointer;
1758
+ }
1759
+
1760
+ /* Hover and keyboard highlight share one class so the two navigation modes
1761
+ look identical — a menu where the arrow keys highlight differently from the
1762
+ mouse reads as two different menus. */
1763
+ .kanuni-datagrid-menu-item:hover:not(:disabled),
1764
+ .kanuni-datagrid-menu-item--active:not(:disabled) {
1765
+ background: var(--dg-surface-hover);
1766
+ }
1767
+
1768
+ .kanuni-datagrid-menu-item:disabled {
1769
+ opacity: 0.45;
1770
+ cursor: default;
1771
+ }
1772
+
1773
+ .kanuni-datagrid-menu-icon {
1774
+ display: inline-flex;
1775
+ align-items: center;
1776
+ flex: none;
1777
+ width: 16px;
1778
+ }
1779
+
1780
+ .kanuni-datagrid-menu-label {
1781
+ flex: 1 1 auto;
1782
+ overflow: hidden;
1783
+ text-overflow: ellipsis;
1784
+ white-space: nowrap;
1785
+ }
1786
+
1787
+ .kanuni-datagrid-menu-shortcut {
1788
+ flex: none;
1789
+ color: var(--dg-text-muted);
1790
+ font-size: 11px;
1791
+ }
1792
+
1793
+ .kanuni-datagrid-menu-caret {
1794
+ flex: none;
1795
+ color: var(--dg-text-muted);
1796
+ }
1797
+
1798
+ .kanuni-datagrid-menu-separator {
1799
+ height: 1px;
1800
+ margin: 4px 6px;
1801
+ background: var(--dg-border);
1802
+ }
1803
+
1804
+ /* Submenus open toward the inline end, so they flip in RTL for free. */
1805
+ /*
1806
+ * Positioned in VIEWPORT coordinates by `GridContextMenu`, because it is a
1807
+ * sibling of the menu rather than a child — see the comment there. It inherits
1808
+ * the menu's box from the shared class; only the positioning differs.
1809
+ */
1810
+ .kanuni-datagrid-submenu {
1811
+ position: fixed;
1812
+ }
1813
+
1814
+ /* ── Toolbar icon button + export menu ────────────────────── */
1815
+ .kanuni-datagrid-toolbar-icon-button {
1816
+ display: inline-flex;
1817
+ align-items: center;
1818
+ justify-content: center;
1819
+ width: 28px;
1820
+ height: 24px;
1821
+ padding: 0;
1822
+ }
1823
+
1824
+ /* Spinner shown in place of the icon while an export runs, so the button does
1825
+ not change size and the toolbar does not reflow mid-export. */
1826
+ .kanuni-datagrid-toolbar-busy {
1827
+ width: 12px;
1828
+ height: 12px;
1829
+ border: 2px solid var(--dg-border-strong);
1830
+ border-top-color: var(--dg-primary);
1831
+ border-radius: 50%;
1832
+ animation: kanuni-datagrid-spin 0.7s linear infinite;
1833
+ }
1834
+
1835
+ @keyframes kanuni-datagrid-spin {
1836
+ to {
1837
+ transform: rotate(360deg);
1838
+ }
1839
+ }
1840
+
1841
+ @media (prefers-reduced-motion: reduce) {
1842
+ .kanuni-datagrid-toolbar-busy {
1843
+ animation: none;
1844
+ }
1845
+ }
1846
+
1847
+ /* The export menu reuses the popup shell but is a plain list of actions. */
1848
+ .kanuni-datagrid-export-menu {
1849
+ width: 190px;
1850
+ gap: 2px;
1851
+ padding: 4px;
1852
+ }
1853
+
1854
+ .kanuni-datagrid-chooser-empty {
1855
+ padding: 8px 4px;
1856
+ color: var(--dg-text-muted);
1857
+ font-style: italic;
1858
+ }
1859
+
1860
+ /* ── Column-aligned footer row (punch #18) ─────────────────
1861
+ Sticky to the BOTTOM of the same scroller the header sticks to the top of, so
1862
+ it scrolls horizontally with the columns and stays put vertically. */
1863
+ /* ── Pinned top/bottom rows (P2.3) ────────────────────────── */
1864
+ /* Sticky in the same stack as the header and the footer. `--dg-pinned-gap`
1865
+ counts the header-height rows between this block and its edge (header +
1866
+ filter row above, footer below), so the offset follows the THEME's header
1867
+ height instead of a number measured in JS at the previous theme's size. */
1868
+ .kanuni-datagrid-pinned-rows {
1869
+ position: sticky;
1870
+ /* Above the data rows, below the header — a pinned row must not cover the
1871
+ header when the grid is scrolled to the very top. */
1872
+ z-index: 2;
1873
+ display: flex;
1874
+ flex-direction: column;
1875
+ background: var(--dg-surface-header);
1876
+ }
1877
+
1878
+ .kanuni-datagrid-pinned-rows--top {
1879
+ top: calc(var(--dg-band-offset, 0px) + var(--dg-header-height) * var(--dg-pinned-gap, 1));
1880
+ border-bottom: var(--dg-header-border);
1881
+ }
1882
+
1883
+ .kanuni-datagrid-pinned-rows--bottom {
1884
+ bottom: calc(var(--dg-header-height) * var(--dg-pinned-gap, 0));
1885
+ border-top: var(--dg-header-border);
1886
+ }
1887
+
1888
+ /* The rows inside keep the grid's cell geometry but not its striping — these
1889
+ are not part of the alternating rhythm of the data. */
1890
+ .kanuni-datagrid-pinned-rows .kanuni-datagrid-row,
1891
+ .kanuni-datagrid-pinned-rows .kanuni-datagrid-cell {
1892
+ background: var(--dg-surface-header);
1893
+ }
1894
+
1895
+ .kanuni-datagrid-footer-row {
1896
+ position: sticky;
1897
+ bottom: 0;
1898
+ z-index: 3;
1899
+ display: flex;
1900
+ height: var(--dg-header-height);
1901
+ background: var(--dg-footer-bg);
1902
+ border-top: var(--dg-header-border);
1903
+ }
1904
+
1905
+ /* Totals at the TOP (P2.8) — same row, other edge. The border swaps sides so
1906
+ the rule always separates the totals from the DATA, not from the header. */
1907
+ .kanuni-datagrid-footer-row--top {
1908
+ top: calc(var(--dg-band-offset, 0px) + var(--dg-header-height) * var(--dg-pinned-gap, 1));
1909
+ bottom: auto;
1910
+ border-top: none;
1911
+ border-bottom: var(--dg-header-border);
1912
+ }
1913
+
1914
+ .kanuni-datagrid-footer-cell {
1915
+ display: flex;
1916
+ align-items: center;
1917
+ gap: 5px;
1918
+ flex: none;
1919
+ height: 100%;
1920
+ padding: 0 var(--dg-cell-padding-x);
1921
+ background: var(--dg-footer-bg);
1922
+ border-inline-end: var(--dg-header-cell-border);
1923
+ font-size: var(--dg-header-font-size);
1924
+ white-space: nowrap;
1925
+ overflow: hidden;
1926
+ user-select: none;
1927
+ }
1928
+
1929
+ .kanuni-datagrid-footer-cell--number {
1930
+ justify-content: flex-end;
1931
+ font-variant-numeric: tabular-nums;
1932
+ }
1933
+
1934
+ .kanuni-datagrid-footer-cell--pinned {
1935
+ position: sticky;
1936
+ /* Above the unpinned footer cells, matching the header's pinned layer. */
1937
+ z-index: 4;
1938
+ }
1939
+
1940
+ .kanuni-datagrid-footer-cell:hover {
1941
+ background: var(--dg-surface-hover);
1942
+ cursor: context-menu;
1943
+ }
1944
+
1945
+ .kanuni-datagrid-footer-value {
1946
+ overflow: hidden;
1947
+ text-overflow: ellipsis;
1948
+ font-weight: 600;
1949
+ color: var(--dg-text);
1950
+ }
1951
+
1952
+ /* The affordance in an empty cell: barely there until hovered, so an untouched
1953
+ footer reads as blank rather than as a row of dots. */
1954
+ .kanuni-datagrid-footer-hint {
1955
+ margin-inline-start: auto;
1956
+ color: var(--dg-border-strong);
1957
+ opacity: 0;
1958
+ }
1959
+
1960
+ .kanuni-datagrid-footer-cell:hover .kanuni-datagrid-footer-hint {
1961
+ opacity: 1;
1962
+ }
1963
+
1964
+ /* ── Focused row (gap G6) ──────────────────────────────────
1965
+ Deliberately NOT the selection colour: focus is where the keyboard is,
1966
+ selection is what an action applies to, and a user who cannot tell them
1967
+ apart cannot tell what "delete selected" is about to do. An inset bar on the
1968
+ leading edge reads as "you are here" without competing with the row tint. */
1969
+ /* Mixed into the row's OWN colour, never into `transparent`.
1970
+ Pinned cells take `background: inherit`, so a see-through row tint made them
1971
+ see-through too — and a sticky cell you can see through shows the columns
1972
+ sliding along underneath it, which reads as two rows of text printed on top
1973
+ of each other. Both variants below are opaque, and keep the stripe. */
1974
+ .kanuni-datagrid-row--focused {
1975
+ background: color-mix(in srgb, var(--dg-primary) 7%, var(--dg-surface));
1976
+ box-shadow: inset 3px 0 0 0 var(--dg-primary);
1977
+ }
1978
+
1979
+ .kanuni-datagrid-row--odd.kanuni-datagrid-row--focused {
1980
+ background: color-mix(in srgb, var(--dg-primary) 7%, var(--dg-surface-stripe));
1981
+ }
1982
+
1983
+ /* Selected AND focused: the selection tint wins, the bar still marks the row. */
1984
+ .kanuni-datagrid-row--focused.kanuni-datagrid-row--selected {
1985
+ box-shadow: inset 3px 0 0 0 var(--dg-primary);
1986
+ }
1987
+
1988
+ [dir='rtl'] .kanuni-datagrid-row--focused,
1989
+ [dir='rtl'] .kanuni-datagrid-row--focused.kanuni-datagrid-row--selected {
1990
+ box-shadow: inset -3px 0 0 0 var(--dg-primary);
1991
+ }
1992
+
1993
+ /* ── Row drag & drop (G7) ─────────────────────────────────── */
1994
+ /*
1995
+ * A sticky GRIP overlaid at the row's inline start, not a lead column: a third
1996
+ * lead column would have to be threaded through `leadColumnWidth`, every
1997
+ * sticky offset and every aria-colindex across six components, and those
1998
+ * drifting apart is exactly what desynchronizes header and body.
1999
+ */
2000
+ .kanuni-datagrid-row-grip {
2001
+ position: sticky;
2002
+ inset-inline-start: 0;
2003
+ z-index: 5;
2004
+ display: flex;
2005
+ align-items: center;
2006
+ justify-content: center;
2007
+ flex: none;
2008
+ width: 0;
2009
+ overflow: visible;
2010
+ color: var(--dg-text-muted, #94a3b8);
2011
+ cursor: grab;
2012
+ opacity: 0;
2013
+ transition: opacity 120ms ease;
2014
+ }
2015
+
2016
+ /* Takes no horizontal space (width: 0) so column geometry is untouched; the
2017
+ glyph is painted into the row's own leading padding. */
2018
+ .kanuni-datagrid-row-grip > svg {
2019
+ position: absolute;
2020
+ /* Past the row-number column when one is showing (audit finding #4): the
2021
+ glyph used to sit at 1px unconditionally, which put it on top of the
2022
+ digits once P3.5 parked a number column at the inline start — visible at
2023
+ six-digit row counts, where the right-aligned number reaches the cell's
2024
+ left edge. The adapter sets the variable; without it nothing changes. */
2025
+ inset-inline-start: calc(var(--dg-grip-inset, 0px) + 1px);
2026
+ }
2027
+
2028
+ .kanuni-datagrid-row:hover .kanuni-datagrid-row-grip {
2029
+ opacity: 1;
2030
+ }
2031
+
2032
+ .kanuni-datagrid-row-grip:active {
2033
+ cursor: grabbing;
2034
+ }
2035
+
2036
+ /* The drop line. An INSET box-shadow rather than a border: a border would add
2037
+ a pixel to the row and shift every row below it by one as the pointer moves,
2038
+ which reads as the whole grid twitching. */
2039
+ .kanuni-datagrid-row--drop-before {
2040
+ box-shadow: inset 0 2px 0 0 var(--dg-primary);
2041
+ }
2042
+
2043
+ .kanuni-datagrid-row--drop-after {
2044
+ box-shadow: inset 0 -2px 0 0 var(--dg-primary);
2045
+ }
2046
+
2047
+ /* ── Tree data (P1.4) ──────────────────────────────────────
2048
+ Indentation is inline on the cell (depth is data); everything visual lives
2049
+ here so all six themes and dark mode restyle the toggle for free. */
2050
+ .kanuni-datagrid-tree-cell {
2051
+ display: inline-flex;
2052
+ align-items: center;
2053
+ gap: 4px;
2054
+ min-width: 0;
2055
+ }
2056
+
2057
+ .kanuni-datagrid-tree-toggle {
2058
+ flex: none;
2059
+ width: 16px;
2060
+ height: 16px;
2061
+ padding: 0;
2062
+ border: 0;
2063
+ background: transparent;
2064
+ color: var(--dg-text-muted, #64748b);
2065
+ font-size: 10px;
2066
+ line-height: 16px;
2067
+ cursor: pointer;
2068
+ border-radius: var(--dg-radius-control, 4px);
2069
+ }
2070
+
2071
+ .kanuni-datagrid-tree-toggle:hover {
2072
+ color: var(--dg-text, #0f172a);
2073
+ background: var(--dg-surface-hover, #f1f5f9);
2074
+ }
2075
+
2076
+ .kanuni-datagrid-tree-toggle--leaf {
2077
+ cursor: default;
2078
+ pointer-events: none;
2079
+ }
2080
+
2081
+ /* Shown only because a descendant matched the filter — visibly the path,
2082
+ not a match. */
2083
+ .kanuni-datagrid-tree-cell--ancestor {
2084
+ color: var(--dg-text-muted, #64748b);
2085
+ }
2086
+ /* ── Cell flash (P1.7) ─────────────────────────────────────
2087
+ The "this number just changed" pulse. Driven by a class the adapter adds
2088
+ and removes; the colour is a token so all six themes and dark mode inherit
2089
+ it, and the animation is what fades — no JS frame loop. */
2090
+ .kanuni-datagrid-cell--flash {
2091
+ animation: kanuni-datagrid-flash var(--dg-flash-duration, 700ms) ease-out;
2092
+ }
2093
+
2094
+ @keyframes kanuni-datagrid-flash {
2095
+ from {
2096
+ background-color: var(--dg-flash-bg, rgba(3, 105, 161, 0.35));
2097
+ }
2098
+ to {
2099
+ background-color: transparent;
2100
+ }
2101
+ }
2102
+
2103
+ /* A flashing grid is exactly the kind of motion that triggers vestibular
2104
+ symptoms. Reduced-motion keeps the SIGNAL — the cell still marks itself —
2105
+ and drops the animation. */
2106
+ @media (prefers-reduced-motion: reduce) {
2107
+ .kanuni-datagrid-cell--flash {
2108
+ animation: none;
2109
+ background-color: var(--dg-flash-bg, rgba(3, 105, 161, 0.35));
2110
+ }
2111
+ }
2112
+ /* ── Print layout (gap analysis P3.4) ──────────────────────────
2113
+ Everything that makes the grid fast on screen is what ruins it on paper. The
2114
+ scroller is a clipped, fixed-height box holding one screenful; the print
2115
+ engine gets exactly that and paginates twenty rows out of a hundred thousand.
2116
+
2117
+ This class undoes the five things that clip the content, in the order they
2118
+ clip it: the root's own box, the scroll container, the canvas that carries
2119
+ the virtual height, the absolutely-positioned viewport, and the block
2120
+ transform that parks the rendered window at its scroll offset.
2121
+
2122
+ The class is set by the ADAPTER, not by a bare `@media print`, because the
2123
+ rows themselves have to be re-rendered — CSS cannot un-virtualize a list that
2124
+ does not exist in the DOM. */
2125
+ .kanuni-datagrid-print {
2126
+ height: auto !important;
2127
+ max-height: none !important;
2128
+ overflow: visible !important;
2129
+ /* Apps mount the grid as a flex child that fills the page — the shape a
2130
+ screen layout wants. Flex sizing beats `height: auto`, so the box kept the
2131
+ viewport's height and the rows below the fold printed on top of whatever
2132
+ came next. `flex: none` hands the sizing back to the content.
2133
+ A host that constrains the grid from further out still has to release it;
2134
+ the grid can only unclip itself. */
2135
+ flex: none !important;
2136
+ }
2137
+
2138
+ .kanuni-datagrid-print .kanuni-datagrid-scroller {
2139
+ height: auto !important;
2140
+ max-height: none !important;
2141
+ overflow: visible !important;
2142
+ }
2143
+
2144
+ .kanuni-datagrid-print .kanuni-datagrid-canvas,
2145
+ .kanuni-datagrid-print .kanuni-datagrid-rows {
2146
+ /* The virtual extent, in a document that is no longer virtual.
2147
+ `.kanuni-datagrid-rows` is the one that matters and the one that was
2148
+ missed first: it carries the scroll canvas's height inline, and with the
2149
+ viewport put back in flow inside it, that inline height decided how much
2150
+ of the grid reached the page — five hundred rows printed as two hundred
2151
+ pixels of paper. The canvas is here for the same reason one level up. */
2152
+ height: auto !important;
2153
+ }
2154
+
2155
+ .kanuni-datagrid-print .kanuni-datagrid-viewport {
2156
+ position: static;
2157
+ /* The block offset is a scroll position. On paper there is no scrolling, and
2158
+ leaving it in pushed the first row down by however far the user happened to
2159
+ have scrolled. */
2160
+ transform: none !important;
2161
+ will-change: auto;
2162
+ }
2163
+
2164
+ /* Sticky is meaningless without a scrolling ancestor, but it still creates the
2165
+ stacking and offset behaviour — a pinned column would print on top of the
2166
+ cell it sits beside. In flow, each cell keeps its own width and order. */
2167
+ .kanuni-datagrid-print .kanuni-datagrid-header,
2168
+ .kanuni-datagrid-print .kanuni-datagrid-band-row,
2169
+ .kanuni-datagrid-print .kanuni-datagrid-filter-row,
2170
+ .kanuni-datagrid-print .kanuni-datagrid-pinned-rows,
2171
+ .kanuni-datagrid-print .kanuni-datagrid-footer-row {
2172
+ position: static !important;
2173
+ }
2174
+
2175
+ /* Rules that only make sense once the ink is on paper. */
2176
+ @media print {
2177
+ /* Release the page around the grid.
2178
+ A grid is normally mounted as a flex child filling a viewport-height shell,
2179
+ and every ancestor in that chain holds it at one screen — the grid can
2180
+ unclip itself and still print one page. The adapter marks the ROOT ELEMENT
2181
+ while print layout is on, because CSS cannot reach an ancestor from a class
2182
+ on the grid, and only for the duration.
2183
+
2184
+ The grid's OWN wrappers are named; the host's are not, so an app with its
2185
+ own fixed-height shell needs the same two lines on it. */
2186
+ .kanuni-datagrid-printing,
2187
+ .kanuni-datagrid-printing body,
2188
+ .kanuni-datagrid-printing .kanuni-datagrid-enterprise,
2189
+ .kanuni-datagrid-printing .kanuni-datagrid-main {
2190
+ height: auto !important;
2191
+ max-height: none !important;
2192
+ overflow: visible !important;
2193
+ flex: none !important;
2194
+ }
2195
+
2196
+ /* A row split across a page break is unreadable — the top half of the text on
2197
+ one sheet and the bottom half on the next. */
2198
+ .kanuni-datagrid-print .kanuni-datagrid-row,
2199
+ .kanuni-datagrid-print .kanuni-datagrid-group-row {
2200
+ break-inside: avoid;
2201
+ page-break-inside: avoid;
2202
+ }
2203
+
2204
+ /* Chrome and Safari drop background colours by default, which turns the
2205
+ header, the group banners and the row striping into undifferentiated white.
2206
+ The structure IS the meaning here, so ask for them back. */
2207
+ .kanuni-datagrid-print {
2208
+ -webkit-print-color-adjust: exact;
2209
+ print-color-adjust: exact;
2210
+ }
2211
+
2212
+ /* Controls that print as furniture: nothing on paper can be clicked, and each
2213
+ one costs a column of width that the data needs. */
2214
+ .kanuni-datagrid-print .kanuni-datagrid-toolbar,
2215
+ .kanuni-datagrid-print .kanuni-datagrid-filter-panel,
2216
+ .kanuni-datagrid-print .kanuni-datagrid-pager,
2217
+ .kanuni-datagrid-print .kanuni-datagrid-status-bar,
2218
+ .kanuni-datagrid-print .kanuni-datagrid-sidebar,
2219
+ .kanuni-datagrid-print .kanuni-datagrid-resize-handle,
2220
+ .kanuni-datagrid-print .kanuni-datagrid-filter-button {
2221
+ display: none !important;
2222
+ }
2223
+ }
2224
+
2225
+ /* ── Cell notes (gap analysis P3.3) ────────────────────────────
2226
+ A note is something a PERSON attached to this cell, and the whole value of
2227
+ the marker is that you can find those cells without hovering every one of
2228
+ them. Excel's corner triangle, for the same reason and in the same corner. */
2229
+ .kanuni-datagrid-cell--noted {
2230
+ position: relative;
2231
+ }
2232
+
2233
+ .kanuni-datagrid-cell--noted::after {
2234
+ content: '';
2235
+ position: absolute;
2236
+ top: 0;
2237
+ /* Logical, not `right`: in an RTL grid the marker belongs in the corner the
2238
+ reader's eye ends at, which is the other one. */
2239
+ inset-inline-end: 0;
2240
+ width: 0;
2241
+ height: 0;
2242
+ /* A triangle from borders — no glyph, no font dependency, no extra element in
2243
+ a cell the row memo is built to keep cheap. */
2244
+ border-top: 6px solid var(--dg-note-marker);
2245
+ border-inline-start: 6px solid transparent;
2246
+ pointer-events: none;
2247
+ }
2248
+
2249
+ .kanuni-datagrid-note-editor {
2250
+ display: flex;
2251
+ flex-direction: column;
2252
+ gap: 8px;
2253
+ padding: 8px;
2254
+ min-width: 260px;
2255
+ }
2256
+
2257
+ .kanuni-datagrid-note-input {
2258
+ font: inherit;
2259
+ color: var(--dg-text);
2260
+ background: var(--dg-surface);
2261
+ border: 1px solid var(--dg-border);
2262
+ border-radius: var(--dg-radius);
2263
+ padding: 6px 8px;
2264
+ resize: vertical;
2265
+ min-height: 64px;
2266
+ }
2267
+
2268
+ .kanuni-datagrid-note-input:focus-visible {
2269
+ outline: 2px solid var(--dg-focus-ring);
2270
+ outline-offset: -1px;
2271
+ }
2272
+
2273
+ .kanuni-datagrid-note-actions {
2274
+ display: flex;
2275
+ justify-content: flex-end;
2276
+ gap: 6px;
2277
+ }
2278
+
2279
+ /* ── Cell spans (gap analysis P3.1) ────────────────────────────
2280
+ The block is an absolutely-positioned child of the cell, not a taller cell.
2281
+ A taller cell would change the height of its ROW, and a grid with an
2282
+ `autoHeight` column measures rows and feeds the result back to the engine —
2283
+ block inflates row, row moves offsets, offsets resize block, React ends it
2284
+ with "Maximum update depth exceeded". Out of the flow, the cell stays the
2285
+ height it always was. */
2286
+ .kanuni-datagrid-cell--span {
2287
+ position: relative;
2288
+ /* The box reaches past the cell's own bottom edge on purpose. */
2289
+ overflow: visible;
2290
+ }
2291
+
2292
+ .kanuni-datagrid-span-box {
2293
+ position: absolute;
2294
+ top: 0;
2295
+ inset-inline: 0;
2296
+ display: flex;
2297
+ /* Top-aligned, not centred: a two-row block and a fifty-row block should put
2298
+ the value in the same place, and a reader looks for it beside the first row
2299
+ of the group. */
2300
+ align-items: flex-start;
2301
+ /* Horizontal padding matches an ordinary cell so the value lines up with the
2302
+ column above and below. The top padding centres it within the block's FIRST
2303
+ row — the grid's line-height is 1.4, so half the leftover row height puts
2304
+ it exactly where an unmerged cell would sit. */
2305
+ padding: calc((var(--dg-row-height) - 1.4em) / 2) var(--dg-cell-padding-x);
2306
+ /* Its own background: the row underneath ends where the row does, and the
2307
+ rest of the block would show whatever is behind it. */
2308
+ background: var(--dg-surface);
2309
+ /* Above the STATIC center cells of later rows (which would otherwise paint
2310
+ their backgrounds over everything below the block's first row) — but below
2311
+ the pinned columns' 2. The audit caught 3 here: a center column's block
2312
+ painted OVER the left-pinned column during horizontal scroll, because the
2313
+ box competes in the viewport's stacking context while a box inside a
2314
+ pinned cell is scoped by its parent's own context. */
2315
+ z-index: 1;
2316
+ border-bottom: var(--dg-row-border);
2317
+ overflow: hidden;
2318
+ white-space: nowrap;
2319
+ text-overflow: ellipsis;
2320
+ }
2321
+
2322
+ .kanuni-datagrid-cell--span-covered {
2323
+ /* No content, no rules. The block above owns this space. */
2324
+ border-bottom: none;
2325
+ background: transparent;
2326
+ }
2327
+
2328
+ /* The FOCUSED covered cell lifts over the block and shows its own content —
2329
+ this is what makes an editor opened on it visible (audit finding #3), and
2330
+ arrowing through a merge gives per-row feedback instead of a void. */
2331
+ .kanuni-datagrid-cell--span-covered.kanuni-datagrid-cell--focused {
2332
+ position: relative;
2333
+ z-index: 2;
2334
+ background: var(--dg-surface);
2335
+ border-bottom: var(--dg-row-border);
2336
+ }
2337
+
2338
+ /* A merged block sits over rows that stripe, get selected and get hovered.
2339
+ Their treatment must not show THROUGH the block — only beside it. */
2340
+ .kanuni-datagrid-row--odd .kanuni-datagrid-span-box,
2341
+ .kanuni-datagrid-row--selected .kanuni-datagrid-span-box {
2342
+ background: var(--dg-surface);
2343
+ }
2344
+
2345
+ /* ── Filter popup resize handle (user request, 2026-08-14) ─────
2346
+ One corner, chosen by where there is room to grow: bottom-right normally,
2347
+ bottom-left when the popup is clamped against the viewport's right edge.
2348
+ Drawn as the familiar diagonal ridges so it reads as "drag me". */
2349
+ .kanuni-datagrid-popup-resize {
2350
+ position: absolute;
2351
+ bottom: 0;
2352
+ width: 14px;
2353
+ height: 14px;
2354
+ z-index: 2;
2355
+ background:
2356
+ repeating-linear-gradient(
2357
+ -45deg,
2358
+ transparent 0 3px,
2359
+ var(--dg-border-strong) 3px 4px
2360
+ );
2361
+ opacity: 0.7;
2362
+ }
2363
+
2364
+ .kanuni-datagrid-popup-resize--right {
2365
+ right: 0;
2366
+ cursor: nwse-resize;
2367
+ clip-path: polygon(100% 0, 100% 100%, 0 100%);
2368
+ }
2369
+
2370
+ .kanuni-datagrid-popup-resize--left {
2371
+ left: 0;
2372
+ cursor: nesw-resize;
2373
+ clip-path: polygon(0 0, 100% 100%, 0 100%);
2374
+ background:
2375
+ repeating-linear-gradient(
2376
+ 45deg,
2377
+ transparent 0 3px,
2378
+ var(--dg-border-strong) 3px 4px
2379
+ );
2380
+ }
2381
+
2382
+ /* ─────────────────────────────────────────────────────────────
2383
+ KANUNILABS DataGrid — packaged themes
2384
+ ─────────────────────────────────────────────────────────────
2385
+ Six presets, each in a light and a dark palette, each at three densities.
2386
+ Nothing here is copied from a third-party stylesheet: every rule is a
2387
+ KanuniLabs class overriding a KanuniLabs --dg-* token. The names are the
2388
+ industry's shared vocabulary for a grid's VISUAL DENSITY AND CHROME — the
2389
+ same sense in which a typeface is called "grotesque" — and the palettes,
2390
+ metrics and line work below are our own.
2391
+
2392
+ ── How a theme is expressed ───────────────────────────────
2393
+ A theme is a token override and (almost) nothing else. That is the whole
2394
+ design: the stylesheet that draws the grid already reads every colour,
2395
+ metric, shadow and border through a custom property, so a preset never
2396
+ restyles a component and can never fall behind one. It also costs the render
2397
+ path exactly nothing — the browser resolves the variables during style
2398
+ computation, and switching themes at runtime is one class swap on the root,
2399
+ not a re-render of a single cell.
2400
+
2401
+ ── Why the selectors are written out three times ──────────
2402
+ Tokens have to reach three roots: the grid itself, the enterprise wrapper
2403
+ (a SIBLING owns the row/form editor), and anything portalled to <body>
2404
+ (`.kanuni-datagrid-detached`). That is the same trio the base sheet declares
2405
+ its defaults on. Writing all three also lands the block at specificity
2406
+ (0,2,0) — equal to the base `.kanuni-datagrid-root.kanuni-datagrid-dark`
2407
+ block and later in the file, so a theme wins; and the theme's own dark
2408
+ variant, at (0,3,0), wins over both. Order and specificity are load-bearing
2409
+ here: this file MUST be concatenated after DataGridTheme.css.
2410
+ ───────────────────────────────────────────────────────────── */
2411
+
2412
+ /* ═══════════════════════════════════════════════════════════
2413
+ QUARTZ — modern, minimal chrome
2414
+ ═══════════════════════════════════════════════════════════
2415
+ The least furniture that still reads as a table: no column separators at
2416
+ all, hairline row rules, and a header that is the same white as the body,
2417
+ separated only by one firm line. Generous radii. For product UIs where the
2418
+ grid sits among cards rather than inside a terminal. */
2419
+ .kanuni-datagrid-root.kanuni-datagrid-theme-quartz,
2420
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-quartz,
2421
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-quartz {
2422
+ --dg-bg: #ffffff;
2423
+ --dg-surface: #ffffff;
2424
+ --dg-surface-header: #ffffff;
2425
+ --dg-surface-hover: #f6f8fa;
2426
+ --dg-surface-stripe: #fbfcfd;
2427
+ --dg-border: #eaeef2;
2428
+ --dg-border-strong: #d5dbe2;
2429
+ --dg-text: #101828;
2430
+ --dg-text-muted: #667085;
2431
+ --dg-primary: #2563eb;
2432
+ --dg-primary-soft: #dbeafe;
2433
+ --dg-on-primary: #ffffff; /* 5.17:1 */
2434
+ --dg-focus-ring: #1d4ed8;
2435
+ --dg-selected-bg: #dbeafe;
2436
+ --dg-group-bg: #f6f8fa;
2437
+ --dg-footer-bg: #f9fafb;
2438
+ --dg-scrollbar-thumb: #d5dbe2;
2439
+
2440
+ /* No vertical rules — the whole point of the preset. */
2441
+ --dg-cell-border: none;
2442
+ --dg-header-cell-border: none;
2443
+ --dg-row-border: 1px solid var(--dg-border);
2444
+ --dg-header-border: 1px solid var(--dg-border-strong);
2445
+
2446
+ --dg-radius: 8px;
2447
+ --dg-radius-control: 6px;
2448
+ --dg-shadow-popup: 0 8px 24px -6px rgba(16, 24, 40, 0.16);
2449
+ --dg-shadow-popup-sm: 0 6px 18px -4px rgba(16, 24, 40, 0.12);
2450
+ --dg-shadow-menu: 0 10px 28px -8px rgba(16, 24, 40, 0.2);
2451
+ --dg-shadow-card: 0 12px 32px -10px rgba(16, 24, 40, 0.2);
2452
+ --dg-shadow-pinned: 4px 0 8px -6px rgba(16, 24, 40, 0.28);
2453
+ --dg-shadow-pinned-right: -4px 0 8px -6px rgba(16, 24, 40, 0.28);
2454
+
2455
+ --dg-row-height: 36px;
2456
+ --dg-header-height: 40px;
2457
+ --dg-cell-padding-x: 12px;
2458
+ --dg-font-size: 13px;
2459
+ --dg-wrap-line-height: 18px;
2460
+ --dg-header-font-size: 12px;
2461
+ --dg-control-height: 30px;
2462
+ --dg-filter-input-height: 26px;
2463
+ --dg-chrome-padding-y: 8px;
2464
+ }
2465
+
2466
+ .kanuni-datagrid-root.kanuni-datagrid-theme-quartz.kanuni-datagrid-dark,
2467
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-quartz.kanuni-datagrid-dark,
2468
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-quartz.kanuni-datagrid-dark {
2469
+ --dg-bg: #0d1117;
2470
+ --dg-surface: #0d1117;
2471
+ --dg-surface-header: #0d1117;
2472
+ --dg-surface-hover: #161b22;
2473
+ --dg-surface-stripe: #10151c;
2474
+ --dg-border: #21262d;
2475
+ --dg-border-strong: #30363d;
2476
+ --dg-text: #e6edf3;
2477
+ --dg-text-muted: #8b949e;
2478
+ --dg-primary: #4493f8;
2479
+ --dg-primary-soft: #17325c;
2480
+ --dg-focus-ring: #79c0ff;
2481
+ --dg-selected-bg: #17325c;
2482
+ --dg-group-bg: #161b22;
2483
+ --dg-footer-bg: #12171e;
2484
+ --dg-on-primary: #05101f;
2485
+ --dg-scrollbar-thumb: #30363d;
2486
+ }
2487
+
2488
+ /* ═══════════════════════════════════════════════════════════
2489
+ BALHAM — dense, ruled, financial
2490
+ ═══════════════════════════════════════════════════════════
2491
+ Every column and every row is ruled, the type is small, the padding is
2492
+ tight and the corners are nearly square. Built for the case where the grid
2493
+ IS the application and the user is counting rows on screen. */
2494
+ .kanuni-datagrid-root.kanuni-datagrid-theme-balham,
2495
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-balham,
2496
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-balham {
2497
+ --dg-bg: #ffffff;
2498
+ --dg-surface: #ffffff;
2499
+ --dg-surface-header: #f2f4f6;
2500
+ --dg-surface-hover: #e6ecf2;
2501
+ --dg-surface-stripe: #f7f8fa;
2502
+ --dg-border: #c8ced4;
2503
+ --dg-border-strong: #98a2ac;
2504
+ --dg-text: #1c2126;
2505
+ --dg-text-muted: #5a6570;
2506
+ /* #0b7fc4 was the intended hue and it measured 4.33:1 against the white that
2507
+ sits on it — the group chip's own label, 12px semibold, so AA's large-text
2508
+ exemption does not apply. Two steps darker, same hue, 5.19:1. */
2509
+ --dg-primary: #0a72b0;
2510
+ --dg-primary-soft: #d3e7f5;
2511
+ --dg-on-primary: #ffffff; /* 5.19:1 */
2512
+ --dg-focus-ring: #0a72b0;
2513
+ --dg-selected-bg: #cfe4f3;
2514
+ --dg-group-bg: #e9edf1;
2515
+ --dg-footer-bg: #f2f4f6;
2516
+ --dg-scrollbar-thumb: #98a2ac;
2517
+
2518
+ /* Fully ruled: the grid character. */
2519
+ --dg-cell-border: 1px solid var(--dg-border);
2520
+ --dg-row-border: 1px solid var(--dg-border);
2521
+ --dg-header-cell-border: 1px solid var(--dg-border-strong);
2522
+ --dg-header-border: 1px solid var(--dg-border-strong);
2523
+
2524
+ --dg-radius: 2px;
2525
+ --dg-radius-control: 2px;
2526
+ /* Flat: a ruled grid does not need lift to separate its layers, and the
2527
+ shadow fought the lines. Popups keep a hard border instead. */
2528
+ --dg-shadow-popup: 0 3px 10px rgba(28, 33, 38, 0.22);
2529
+ --dg-shadow-popup-sm: 0 2px 8px rgba(28, 33, 38, 0.18);
2530
+ --dg-shadow-menu: 0 3px 10px rgba(28, 33, 38, 0.22);
2531
+ --dg-shadow-card: 0 4px 14px rgba(28, 33, 38, 0.22);
2532
+ --dg-shadow-panel: 0 3px 10px rgba(28, 33, 38, 0.18);
2533
+ --dg-shadow-dialog: 0 10px 32px rgba(28, 33, 38, 0.34);
2534
+ --dg-shadow-pinned: 2px 0 3px -2px rgba(28, 33, 38, 0.3);
2535
+ --dg-shadow-pinned-right: -2px 0 3px -2px rgba(28, 33, 38, 0.3);
2536
+
2537
+ --dg-row-height: 25px;
2538
+ --dg-header-height: 28px;
2539
+ --dg-cell-padding-x: 6px;
2540
+ --dg-font-size: 12px;
2541
+ --dg-wrap-line-height: 16px;
2542
+ --dg-header-font-size: 11px;
2543
+ --dg-header-weight: 700;
2544
+ --dg-control-height: 24px;
2545
+ --dg-control-font-size: 11px;
2546
+ --dg-chrome-font-size: 11px;
2547
+ --dg-filter-input-height: 20px;
2548
+ --dg-chrome-padding-y: 4px;
2549
+ }
2550
+
2551
+ .kanuni-datagrid-root.kanuni-datagrid-theme-balham.kanuni-datagrid-dark,
2552
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-balham.kanuni-datagrid-dark,
2553
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-balham.kanuni-datagrid-dark {
2554
+ --dg-bg: #1a2029;
2555
+ --dg-surface: #1a2029;
2556
+ --dg-surface-header: #232b36;
2557
+ --dg-surface-hover: #2c3644;
2558
+ --dg-surface-stripe: #1e242e;
2559
+ --dg-border: #39434f;
2560
+ --dg-border-strong: #566373;
2561
+ --dg-text: #e4e9ef;
2562
+ --dg-text-muted: #97a3b2;
2563
+ --dg-primary: #3fa8e8;
2564
+ --dg-primary-soft: #143349;
2565
+ --dg-focus-ring: #7cc7f2;
2566
+ --dg-selected-bg: #17415e;
2567
+ --dg-group-bg: #262f3b;
2568
+ --dg-footer-bg: #232b36;
2569
+ --dg-on-primary: #06192a;
2570
+ --dg-scrollbar-thumb: #566373;
2571
+ }
2572
+
2573
+ /* ═══════════════════════════════════════════════════════════
2574
+ MATERIAL — tall, quiet lines, real elevation
2575
+ ═══════════════════════════════════════════════════════════
2576
+ Rows only, no column rules; larger rhythm; a header set in medium weight
2577
+ with open tracking rather than bold; pill-shaped controls; and shadows that
2578
+ actually lift. The one preset whose type scale grows rather than shrinks. */
2579
+ .kanuni-datagrid-root.kanuni-datagrid-theme-material,
2580
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-material,
2581
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-material {
2582
+ --dg-bg: #ffffff;
2583
+ --dg-surface: #ffffff;
2584
+ --dg-surface-header: #ffffff;
2585
+ --dg-surface-hover: #f4f0f7;
2586
+ --dg-surface-stripe: #faf8fc;
2587
+ --dg-border: #e7e0ec;
2588
+ --dg-border-strong: #cac4d0;
2589
+ --dg-text: #1d1b20;
2590
+ --dg-text-muted: #49454f;
2591
+ --dg-primary: #6750a4;
2592
+ --dg-primary-soft: #e8def8;
2593
+ --dg-on-primary: #ffffff; /* 6.44:1 */
2594
+ --dg-focus-ring: #6750a4;
2595
+ --dg-selected-bg: #e8def8;
2596
+ --dg-group-bg: #f5eefc;
2597
+ --dg-footer-bg: #faf8fc;
2598
+ --dg-scrollbar-thumb: #cac4d0;
2599
+
2600
+ --dg-cell-border: none;
2601
+ --dg-header-cell-border: none;
2602
+ --dg-row-border: 1px solid var(--dg-border);
2603
+ --dg-header-border: 1px solid var(--dg-border-strong);
2604
+
2605
+ /* A label, not a heading: medium weight, opened up. */
2606
+ --dg-header-weight: 500;
2607
+ --dg-header-tracking: 0.04em;
2608
+
2609
+ --dg-radius: 12px;
2610
+ --dg-radius-control: 20px;
2611
+ --dg-shadow-popup: 0 4px 8px 3px rgba(29, 27, 32, 0.15), 0 1px 3px rgba(29, 27, 32, 0.3);
2612
+ --dg-shadow-popup-sm: 0 2px 6px 2px rgba(29, 27, 32, 0.15), 0 1px 2px rgba(29, 27, 32, 0.3);
2613
+ --dg-shadow-menu: 0 4px 8px 3px rgba(29, 27, 32, 0.15), 0 1px 3px rgba(29, 27, 32, 0.3);
2614
+ --dg-shadow-card: 0 6px 10px 4px rgba(29, 27, 32, 0.15), 0 2px 3px rgba(29, 27, 32, 0.3);
2615
+ --dg-shadow-panel: 0 4px 8px 3px rgba(29, 27, 32, 0.15), 0 1px 3px rgba(29, 27, 32, 0.3);
2616
+ --dg-shadow-dialog: 0 8px 12px 6px rgba(29, 27, 32, 0.15), 0 4px 4px rgba(29, 27, 32, 0.3);
2617
+ --dg-shadow-pinned: 4px 0 8px -5px rgba(29, 27, 32, 0.35);
2618
+ --dg-shadow-pinned-right: -4px 0 8px -5px rgba(29, 27, 32, 0.35);
2619
+
2620
+ --dg-row-height: 40px;
2621
+ --dg-header-height: 48px;
2622
+ --dg-cell-padding-x: 16px;
2623
+ --dg-font-size: 14px;
2624
+ --dg-wrap-line-height: 20px;
2625
+ --dg-header-font-size: 12px;
2626
+ --dg-control-height: 34px;
2627
+ --dg-control-font-size: 13px;
2628
+ --dg-chrome-font-size: 13px;
2629
+ --dg-filter-input-height: 28px;
2630
+ --dg-chrome-padding-y: 10px;
2631
+ }
2632
+
2633
+ .kanuni-datagrid-root.kanuni-datagrid-theme-material.kanuni-datagrid-dark,
2634
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-material.kanuni-datagrid-dark,
2635
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-material.kanuni-datagrid-dark {
2636
+ --dg-bg: #141218;
2637
+ --dg-surface: #141218;
2638
+ --dg-surface-header: #141218;
2639
+ --dg-surface-hover: #211f26;
2640
+ --dg-surface-stripe: #1a171f;
2641
+ --dg-border: #2b2930;
2642
+ --dg-border-strong: #49454f;
2643
+ --dg-text: #e6e0e9;
2644
+ --dg-text-muted: #cac4d0;
2645
+ --dg-primary: #d0bcff;
2646
+ --dg-primary-soft: #4f378b;
2647
+ --dg-focus-ring: #d0bcff;
2648
+ --dg-selected-bg: #4f378b;
2649
+ --dg-group-bg: #241f2e;
2650
+ --dg-footer-bg: #1a171f;
2651
+ /* The dark accent is a pale lilac: what sits on it must be near-black. */
2652
+ --dg-on-primary: #381e72;
2653
+ --dg-scrollbar-thumb: #49454f;
2654
+ }
2655
+
2656
+ /* ═══════════════════════════════════════════════════════════
2657
+ ALPINE — airy, fully framed, high-chroma accent
2658
+ ═══════════════════════════════════════════════════════════
2659
+ The middle position: both sets of rules are drawn but kept light, the rhythm
2660
+ is roomy, and the header is anchored by a doubled rule and a heavy weight so
2661
+ it reads as a distinct band even though it shares the body's palette. */
2662
+ .kanuni-datagrid-root.kanuni-datagrid-theme-alpine,
2663
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-alpine,
2664
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-alpine {
2665
+ --dg-bg: #ffffff;
2666
+ --dg-surface: #ffffff;
2667
+ --dg-surface-header: #f8f9fa;
2668
+ --dg-surface-hover: #eef3f8;
2669
+ --dg-surface-stripe: #fcfdfe;
2670
+ --dg-border: #dde2e8;
2671
+ --dg-border-strong: #b9c2cc;
2672
+ --dg-text: #181d1f;
2673
+ --dg-text-muted: #626b74;
2674
+ --dg-primary: #1a73c7;
2675
+ --dg-primary-soft: #dceafa;
2676
+ --dg-on-primary: #ffffff; /* 4.86:1 */
2677
+ --dg-focus-ring: #1a73c7;
2678
+ --dg-selected-bg: #dceafa;
2679
+ --dg-group-bg: #eef1f4;
2680
+ --dg-footer-bg: #f8f9fa;
2681
+ --dg-scrollbar-thumb: #b9c2cc;
2682
+
2683
+ --dg-cell-border: 1px solid var(--dg-border);
2684
+ --dg-row-border: 1px solid var(--dg-border);
2685
+ --dg-header-cell-border: 1px solid var(--dg-border);
2686
+ /* Doubled rather than coloured: the header band is defined by weight of
2687
+ LINE, which survives every palette including the dark one. */
2688
+ --dg-header-border: 2px solid var(--dg-border-strong);
2689
+ --dg-header-weight: 700;
2690
+
2691
+ --dg-radius: 6px;
2692
+ --dg-radius-control: 4px;
2693
+ --dg-shadow-popup: 0 6px 20px -4px rgba(24, 29, 31, 0.22);
2694
+ --dg-shadow-popup-sm: 0 4px 14px -3px rgba(24, 29, 31, 0.18);
2695
+ --dg-shadow-menu: 0 8px 22px -6px rgba(24, 29, 31, 0.24);
2696
+ --dg-shadow-card: 0 10px 28px -8px rgba(24, 29, 31, 0.24);
2697
+ --dg-shadow-pinned: 3px 0 6px -4px rgba(24, 29, 31, 0.3);
2698
+ --dg-shadow-pinned-right: -3px 0 6px -4px rgba(24, 29, 31, 0.3);
2699
+
2700
+ --dg-row-height: 34px;
2701
+ --dg-header-height: 38px;
2702
+ --dg-cell-padding-x: 12px;
2703
+ --dg-font-size: 13px;
2704
+ --dg-wrap-line-height: 18px;
2705
+ --dg-header-font-size: 12px;
2706
+ --dg-control-height: 28px;
2707
+ --dg-filter-input-height: 24px;
2708
+ --dg-chrome-padding-y: 7px;
2709
+ }
2710
+
2711
+ .kanuni-datagrid-root.kanuni-datagrid-theme-alpine.kanuni-datagrid-dark,
2712
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-alpine.kanuni-datagrid-dark,
2713
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-alpine.kanuni-datagrid-dark {
2714
+ --dg-bg: #181d1f;
2715
+ --dg-surface: #181d1f;
2716
+ --dg-surface-header: #222628;
2717
+ --dg-surface-hover: #2b3033;
2718
+ --dg-surface-stripe: #1c2123;
2719
+ --dg-border: #35393c;
2720
+ --dg-border-strong: #545a5f;
2721
+ --dg-text: #eceff1;
2722
+ --dg-text-muted: #9aa3aa;
2723
+ --dg-primary: #52a8f0;
2724
+ --dg-primary-soft: #123049;
2725
+ --dg-focus-ring: #8ec9f7;
2726
+ --dg-selected-bg: #16395a;
2727
+ --dg-group-bg: #252a2d;
2728
+ --dg-footer-bg: #222628;
2729
+ --dg-on-primary: #04182a;
2730
+ --dg-scrollbar-thumb: #545a5f;
2731
+ }
2732
+
2733
+ /* ═══════════════════════════════════════════════════════════
2734
+ KANUNI — the house theme, light
2735
+ ═══════════════════════════════════════════════════════════
2736
+ The KanuniLabs identity: an ink header carrying the brand gold, over a warm
2737
+ near-white body. It is the theme that made `--dg-on-primary` necessary —
2738
+ the accent is #C5A55A, and white on it measures 1.9:1, so everything that
2739
+ sits ON the accent (group chips, primary buttons, the selected calendar day)
2740
+ takes near-black instead. `--dg-header-*` is likewise doing real work here:
2741
+ only the header row goes ink; the group panel, filter row and footer stay
2742
+ light, because their muted text would be unreadable on the dark fill. */
2743
+ .kanuni-datagrid-root.kanuni-datagrid-theme-kanuni,
2744
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-kanuni,
2745
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-kanuni {
2746
+ --dg-bg: #ffffff;
2747
+ --dg-surface: #ffffff;
2748
+ --dg-surface-header: #faf6ed; /* brand-50 — panels, filter row, footer */
2749
+ --dg-surface-hover: #f7f2e6;
2750
+ --dg-surface-stripe: #fdfbf6;
2751
+ --dg-border: #e7e2d6;
2752
+ --dg-border-strong: #cfc6b2;
2753
+ --dg-text: #1c1917;
2754
+ --dg-text-muted: #6b6154;
2755
+ --dg-primary: #c5a55a; /* brand-500 */
2756
+ --dg-primary-soft: #f3ead3; /* brand-100 */
2757
+ /* NOT the accent: gold on white is 1.9:1 and a focus ring has to be seen.
2758
+ brand-900 measures 7.0:1 on white. */
2759
+ --dg-focus-ring: #6b5119;
2760
+ --dg-on-primary: #1c1917;
2761
+ --dg-selected-bg: #f3ead3;
2762
+ --dg-group-bg: #faf6ed;
2763
+ --dg-footer-bg: #faf6ed;
2764
+ --dg-scrollbar-thumb: #cfc6b2;
2765
+
2766
+ /* The header band: ink, with brand foreground. */
2767
+ --dg-header-bg: #232b33;
2768
+ --dg-header-text: #f6efdd;
2769
+ --dg-header-text-muted: #b6ab92;
2770
+ --dg-header-hover: #2f3941;
2771
+ --dg-header-cell-border: 1px solid #3a444d;
2772
+ --dg-header-border: 2px solid #c5a55a;
2773
+
2774
+ --dg-cell-border: none;
2775
+ --dg-row-border: 1px solid var(--dg-border);
2776
+
2777
+ --dg-radius: 6px;
2778
+ --dg-radius-control: 4px;
2779
+ --dg-shadow-popup: 0 10px 26px -8px rgba(35, 43, 51, 0.3);
2780
+ --dg-shadow-popup-sm: 0 6px 18px -6px rgba(35, 43, 51, 0.24);
2781
+ --dg-shadow-menu: 0 10px 26px -8px rgba(35, 43, 51, 0.3);
2782
+ --dg-shadow-card: 0 12px 30px -10px rgba(35, 43, 51, 0.3);
2783
+ --dg-shadow-pinned: 3px 0 6px -4px rgba(35, 43, 51, 0.28);
2784
+ --dg-shadow-pinned-right: -3px 0 6px -4px rgba(35, 43, 51, 0.28);
2785
+ --dg-overlay: rgba(28, 25, 23, 0.6);
2786
+
2787
+ --dg-row-height: 32px;
2788
+ --dg-header-height: 38px;
2789
+ --dg-cell-padding-x: 10px;
2790
+ --dg-font-size: 13px;
2791
+ --dg-wrap-line-height: 18px;
2792
+ --dg-header-font-size: 12px;
2793
+ --dg-header-tracking: 0.02em;
2794
+ --dg-control-height: 28px;
2795
+ --dg-filter-input-height: 24px;
2796
+ --dg-chrome-padding-y: 7px;
2797
+ }
2798
+
2799
+ /* `kanuni` in dark: the body follows the header down rather than the header
2800
+ coming up, so the gold stays the only warm thing on screen. */
2801
+ .kanuni-datagrid-root.kanuni-datagrid-theme-kanuni.kanuni-datagrid-dark,
2802
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-kanuni.kanuni-datagrid-dark,
2803
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-kanuni.kanuni-datagrid-dark,
2804
+ /* ═══════════════════════════════════════════════════════════
2805
+ KANUNI MIDNIGHT — the house theme, dark by default
2806
+ ═══════════════════════════════════════════════════════════
2807
+ Shares the palette below, but applies WITHOUT the `dark` flag. Listed in the
2808
+ same block with and without `.kanuni-datagrid-dark`, so passing `dark` to a
2809
+ grid that is already midnight is a no-op rather than a reversion to the
2810
+ packaged slate palette. */
2811
+ .kanuni-datagrid-root.kanuni-datagrid-theme-kanuni-midnight,
2812
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-kanuni-midnight,
2813
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-kanuni-midnight,
2814
+ .kanuni-datagrid-root.kanuni-datagrid-theme-kanuni-midnight.kanuni-datagrid-dark,
2815
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-kanuni-midnight.kanuni-datagrid-dark,
2816
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-kanuni-midnight.kanuni-datagrid-dark {
2817
+ --dg-bg: #0c0a09;
2818
+ --dg-surface: #16130f;
2819
+ --dg-surface-header: #221d17;
2820
+ --dg-surface-hover: #2b241c;
2821
+ --dg-surface-stripe: #191511;
2822
+ --dg-border: #332b22;
2823
+ --dg-border-strong: #4d4235;
2824
+ --dg-text: #f5f1e8;
2825
+ --dg-text-muted: #a89b86;
2826
+ --dg-primary: #c5a55a;
2827
+ --dg-primary-soft: #3d3117;
2828
+ --dg-focus-ring: #d9bf7e;
2829
+ --dg-on-primary: #1c1917;
2830
+ --dg-selected-bg: #3d3117;
2831
+ --dg-group-bg: #221d17;
2832
+ --dg-footer-bg: #221d17;
2833
+ --dg-scrollbar-thumb: #4d4235;
2834
+
2835
+ --dg-header-bg: #1c1811;
2836
+ --dg-header-text: #f6efdd;
2837
+ --dg-header-text-muted: #a89b86;
2838
+ --dg-header-hover: #2b241c;
2839
+ --dg-header-cell-border: 1px solid #332b22;
2840
+ --dg-header-border: 2px solid #c5a55a;
2841
+
2842
+ --dg-cell-border: none;
2843
+ --dg-row-border: 1px solid var(--dg-border);
2844
+
2845
+ --dg-danger: #f87171;
2846
+ --dg-danger-text: #fca5a5;
2847
+ --dg-on-danger: #450a0a;
2848
+ --dg-warning: #fbbf24;
2849
+ --dg-overlay: rgba(12, 10, 9, 0.72);
2850
+
2851
+ --dg-radius: 6px;
2852
+ --dg-radius-control: 4px;
2853
+ --dg-shadow-popup: 0 10px 26px -6px rgba(0, 0, 0, 0.7);
2854
+ --dg-shadow-popup-sm: 0 6px 18px -4px rgba(0, 0, 0, 0.6);
2855
+ --dg-shadow-menu: 0 10px 26px -6px rgba(0, 0, 0, 0.7);
2856
+ --dg-shadow-card: 0 12px 30px -8px rgba(0, 0, 0, 0.7);
2857
+ --dg-shadow-panel: 0 8px 20px rgba(0, 0, 0, 0.55);
2858
+ --dg-shadow-dialog: 0 24px 48px rgba(0, 0, 0, 0.75);
2859
+ --dg-shadow-pinned: 3px 0 6px -3px rgba(0, 0, 0, 0.7);
2860
+ --dg-shadow-pinned-right: -3px 0 6px -3px rgba(0, 0, 0, 0.7);
2861
+
2862
+ --dg-row-height: 32px;
2863
+ --dg-header-height: 38px;
2864
+ --dg-cell-padding-x: 10px;
2865
+ --dg-font-size: 13px;
2866
+ --dg-wrap-line-height: 18px;
2867
+ --dg-header-font-size: 12px;
2868
+ --dg-header-tracking: 0.02em;
2869
+ --dg-control-height: 28px;
2870
+ --dg-filter-input-height: 24px;
2871
+ --dg-chrome-padding-y: 7px;
2872
+ }
2873
+
2874
+ /* ═══════════════════════════════════════════════════════════
2875
+ DENSITY
2876
+ ═══════════════════════════════════════════════════════════
2877
+ Compact is not "the same grid in smaller type". Every vertical measure moves
2878
+ together — row, header, cell padding, control height, toolbar and pager
2879
+ padding, filter field — so the proportions hold and the grid stays usable
2880
+ rather than merely smaller. Each theme scales from its OWN normal, because a
2881
+ Material row starts at 40px and a Balham row at 25px; one shared compact
2882
+ value would have flattened the presets into each other at that density.
2883
+
2884
+ The ROW HEIGHT is deliberately absent here: it is a core value the
2885
+ virtualizer runs on, so the component resolves it in JS (THEME_ROW_HEIGHT)
2886
+ and core feeds it back as --dg-row-height. Everything CSS owns lives here,
2887
+ so the two can never disagree about the row geometry. Same split the base
2888
+ stylesheet already uses for the unthemed density presets.
2889
+
2890
+ All three roots are listed so a portalled popup gets the density too — the
2891
+ base sheet's presets are root-only, which left popup controls at their
2892
+ normal size whatever the grid was set to. */
2893
+
2894
+ /* ── Quartz ── */
2895
+ .kanuni-datagrid-root.kanuni-datagrid-theme-quartz.kanuni-datagrid-density-compact,
2896
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-quartz.kanuni-datagrid-density-compact,
2897
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-quartz.kanuni-datagrid-density-compact {
2898
+ --dg-header-height: 30px;
2899
+ --dg-cell-padding-x: 8px;
2900
+ --dg-font-size: 12px;
2901
+ --dg-wrap-line-height: 16px;
2902
+ --dg-header-font-size: 11px;
2903
+ --dg-control-height: 24px;
2904
+ --dg-control-font-size: 11px;
2905
+ --dg-chrome-font-size: 11px;
2906
+ --dg-filter-input-height: 20px;
2907
+ --dg-chrome-padding-y: 4px;
2908
+ }
2909
+
2910
+ .kanuni-datagrid-root.kanuni-datagrid-theme-quartz.kanuni-datagrid-density-comfortable,
2911
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-quartz.kanuni-datagrid-density-comfortable,
2912
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-quartz.kanuni-datagrid-density-comfortable {
2913
+ --dg-header-height: 48px;
2914
+ --dg-cell-padding-x: 16px;
2915
+ --dg-font-size: 14px;
2916
+ --dg-wrap-line-height: 20px;
2917
+ --dg-header-font-size: 13px;
2918
+ --dg-control-height: 34px;
2919
+ --dg-control-font-size: 13px;
2920
+ --dg-chrome-font-size: 13px;
2921
+ --dg-filter-input-height: 30px;
2922
+ --dg-chrome-padding-y: 10px;
2923
+ }
2924
+
2925
+ /* ── Balham ── */
2926
+ .kanuni-datagrid-root.kanuni-datagrid-theme-balham.kanuni-datagrid-density-compact,
2927
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-balham.kanuni-datagrid-density-compact,
2928
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-balham.kanuni-datagrid-density-compact {
2929
+ --dg-header-height: 22px;
2930
+ --dg-cell-padding-x: 4px;
2931
+ --dg-font-size: 11px;
2932
+ --dg-wrap-line-height: 16px;
2933
+ --dg-header-font-size: 10px;
2934
+ --dg-control-height: 20px;
2935
+ --dg-control-font-size: 10px;
2936
+ --dg-chrome-font-size: 10px;
2937
+ --dg-filter-input-height: 17px;
2938
+ --dg-chrome-padding-y: 2px;
2939
+ }
2940
+
2941
+ .kanuni-datagrid-root.kanuni-datagrid-theme-balham.kanuni-datagrid-density-comfortable,
2942
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-balham.kanuni-datagrid-density-comfortable,
2943
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-balham.kanuni-datagrid-density-comfortable {
2944
+ --dg-header-height: 36px;
2945
+ --dg-cell-padding-x: 10px;
2946
+ --dg-font-size: 13px;
2947
+ --dg-wrap-line-height: 18px;
2948
+ --dg-header-font-size: 12px;
2949
+ --dg-control-height: 28px;
2950
+ --dg-control-font-size: 12px;
2951
+ --dg-chrome-font-size: 12px;
2952
+ --dg-filter-input-height: 24px;
2953
+ --dg-chrome-padding-y: 6px;
2954
+ }
2955
+
2956
+ /* ── Material ── */
2957
+ .kanuni-datagrid-root.kanuni-datagrid-theme-material.kanuni-datagrid-density-compact,
2958
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-material.kanuni-datagrid-density-compact,
2959
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-material.kanuni-datagrid-density-compact {
2960
+ --dg-header-height: 36px;
2961
+ --dg-cell-padding-x: 12px;
2962
+ --dg-font-size: 13px;
2963
+ --dg-wrap-line-height: 18px;
2964
+ --dg-header-font-size: 11px;
2965
+ --dg-control-height: 28px;
2966
+ --dg-control-font-size: 12px;
2967
+ --dg-chrome-font-size: 12px;
2968
+ --dg-filter-input-height: 24px;
2969
+ --dg-chrome-padding-y: 6px;
2970
+ }
2971
+
2972
+ .kanuni-datagrid-root.kanuni-datagrid-theme-material.kanuni-datagrid-density-comfortable,
2973
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-material.kanuni-datagrid-density-comfortable,
2974
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-material.kanuni-datagrid-density-comfortable {
2975
+ --dg-header-height: 56px;
2976
+ --dg-cell-padding-x: 20px;
2977
+ --dg-font-size: 15px;
2978
+ --dg-wrap-line-height: 20px;
2979
+ --dg-header-font-size: 13px;
2980
+ --dg-control-height: 40px;
2981
+ --dg-control-font-size: 14px;
2982
+ --dg-chrome-font-size: 14px;
2983
+ --dg-filter-input-height: 32px;
2984
+ --dg-chrome-padding-y: 12px;
2985
+ }
2986
+
2987
+ /* ── Alpine ── */
2988
+ .kanuni-datagrid-root.kanuni-datagrid-theme-alpine.kanuni-datagrid-density-compact,
2989
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-alpine.kanuni-datagrid-density-compact,
2990
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-alpine.kanuni-datagrid-density-compact {
2991
+ --dg-header-height: 28px;
2992
+ --dg-cell-padding-x: 8px;
2993
+ --dg-font-size: 12px;
2994
+ --dg-wrap-line-height: 16px;
2995
+ --dg-header-font-size: 11px;
2996
+ --dg-control-height: 23px;
2997
+ --dg-control-font-size: 11px;
2998
+ --dg-chrome-font-size: 11px;
2999
+ --dg-filter-input-height: 19px;
3000
+ --dg-chrome-padding-y: 4px;
3001
+ }
3002
+
3003
+ .kanuni-datagrid-root.kanuni-datagrid-theme-alpine.kanuni-datagrid-density-comfortable,
3004
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-alpine.kanuni-datagrid-density-comfortable,
3005
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-alpine.kanuni-datagrid-density-comfortable {
3006
+ --dg-header-height: 46px;
3007
+ --dg-cell-padding-x: 16px;
3008
+ --dg-font-size: 14px;
3009
+ --dg-wrap-line-height: 20px;
3010
+ --dg-header-font-size: 13px;
3011
+ --dg-control-height: 34px;
3012
+ --dg-control-font-size: 13px;
3013
+ --dg-chrome-font-size: 13px;
3014
+ --dg-filter-input-height: 28px;
3015
+ --dg-chrome-padding-y: 10px;
3016
+ }
3017
+
3018
+ /* ── Kanuni + Kanuni Midnight (one rhythm, two palettes) ── */
3019
+ .kanuni-datagrid-root.kanuni-datagrid-theme-kanuni.kanuni-datagrid-density-compact,
3020
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-kanuni.kanuni-datagrid-density-compact,
3021
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-kanuni.kanuni-datagrid-density-compact,
3022
+ .kanuni-datagrid-root.kanuni-datagrid-theme-kanuni-midnight.kanuni-datagrid-density-compact,
3023
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-kanuni-midnight.kanuni-datagrid-density-compact,
3024
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-kanuni-midnight.kanuni-datagrid-density-compact {
3025
+ --dg-header-height: 30px;
3026
+ --dg-cell-padding-x: 7px;
3027
+ --dg-font-size: 12px;
3028
+ --dg-wrap-line-height: 16px;
3029
+ --dg-header-font-size: 11px;
3030
+ --dg-control-height: 24px;
3031
+ --dg-control-font-size: 11px;
3032
+ --dg-chrome-font-size: 11px;
3033
+ --dg-filter-input-height: 20px;
3034
+ --dg-chrome-padding-y: 4px;
3035
+ }
3036
+
3037
+ .kanuni-datagrid-root.kanuni-datagrid-theme-kanuni.kanuni-datagrid-density-comfortable,
3038
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-kanuni.kanuni-datagrid-density-comfortable,
3039
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-kanuni.kanuni-datagrid-density-comfortable,
3040
+ .kanuni-datagrid-root.kanuni-datagrid-theme-kanuni-midnight.kanuni-datagrid-density-comfortable,
3041
+ .kanuni-datagrid-enterprise.kanuni-datagrid-theme-kanuni-midnight.kanuni-datagrid-density-comfortable,
3042
+ .kanuni-datagrid-detached.kanuni-datagrid-theme-kanuni-midnight.kanuni-datagrid-density-comfortable {
3043
+ --dg-header-height: 46px;
3044
+ --dg-cell-padding-x: 14px;
3045
+ --dg-font-size: 14px;
3046
+ --dg-wrap-line-height: 20px;
3047
+ --dg-header-font-size: 13px;
3048
+ --dg-control-height: 34px;
3049
+ --dg-control-font-size: 13px;
3050
+ --dg-chrome-font-size: 13px;
3051
+ --dg-filter-input-height: 28px;
3052
+ --dg-chrome-padding-y: 10px;
3053
+ }
3054
+
3055
+ /* ═══════════════════════════════════════════════════════════
3056
+ SCROLLBAR
3057
+ ═══════════════════════════════════════════════════════════
3058
+ Only ever applied when a theme is chosen. Left to the platform otherwise:
3059
+ the unthemed grid must keep rendering exactly as it did, and a native
3060
+ scrollbar is a reasonable default in a component that is dropped into
3061
+ someone else's application.
3062
+
3063
+ `scrollbar-color` covers Firefox and current Chromium; the -webkit- rules
3064
+ cover Safari and older Chromium, which ignore the standard property. Both
3065
+ are pure paint — no extra element, nothing for the virtualizer to measure. */
3066
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-scroller,
3067
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-filter-list,
3068
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-detail-cell {
3069
+ scrollbar-color: var(--dg-scrollbar-thumb) var(--dg-scrollbar-track);
3070
+ scrollbar-width: thin;
3071
+ }
3072
+
3073
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-scroller::-webkit-scrollbar,
3074
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-filter-list::-webkit-scrollbar,
3075
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-detail-cell::-webkit-scrollbar {
3076
+ width: 11px;
3077
+ height: 11px;
3078
+ }
3079
+
3080
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-scroller::-webkit-scrollbar-track,
3081
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-filter-list::-webkit-scrollbar-track,
3082
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-detail-cell::-webkit-scrollbar-track {
3083
+ background: var(--dg-scrollbar-track);
3084
+ }
3085
+
3086
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-scroller::-webkit-scrollbar-thumb,
3087
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-filter-list::-webkit-scrollbar-thumb,
3088
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-detail-cell::-webkit-scrollbar-thumb {
3089
+ border: 3px solid transparent;
3090
+ border-radius: 999px;
3091
+ /* Painted into the padding box so the 3px transparent border reads as a gap
3092
+ rather than as a thumb that touches both edges of the gutter. */
3093
+ background-clip: padding-box;
3094
+ background-color: var(--dg-scrollbar-thumb);
3095
+ }
3096
+
3097
+ [class*='kanuni-datagrid-theme-'] .kanuni-datagrid-scroller::-webkit-scrollbar-corner {
3098
+ background: var(--dg-scrollbar-track);
3099
+ }
3100
+
3101
+ /* ═══════════════════════════════════════════════════════════
3102
+ THE ONE RULE A TOKEN CANNOT CARRY
3103
+ ═══════════════════════════════════════════════════════════
3104
+ Four presets set `--dg-cell-border: none`, which also removes the only line
3105
+ separating a PINNED block from the columns sliding under it. The edge shadow
3106
+ is still there, but a shadow alone is a soft seam on a surface with no other
3107
+ vertical rules — the frozen columns stopped reading as frozen. A hairline on
3108
+ the edge element (there is exactly one per side, not one per cell) restores
3109
+ it without bringing the column rules back.
3110
+
3111
+ Every ruled preset — Balham, Alpine — needs nothing here: the seam is
3112
+ already drawn by the cell borders they keep. */
3113
+ .kanuni-datagrid-theme-quartz .kanuni-datagrid-pin-edge-left,
3114
+ .kanuni-datagrid-theme-material .kanuni-datagrid-pin-edge-left,
3115
+ .kanuni-datagrid-theme-kanuni .kanuni-datagrid-pin-edge-left,
3116
+ .kanuni-datagrid-theme-kanuni-midnight .kanuni-datagrid-pin-edge-left {
3117
+ border-inline-end: 1px solid var(--dg-border-strong);
3118
+ }
3119
+
3120
+ .kanuni-datagrid-theme-quartz .kanuni-datagrid-pin-edge-right,
3121
+ .kanuni-datagrid-theme-material .kanuni-datagrid-pin-edge-right,
3122
+ .kanuni-datagrid-theme-kanuni .kanuni-datagrid-pin-edge-right,
3123
+ .kanuni-datagrid-theme-kanuni-midnight .kanuni-datagrid-pin-edge-right {
3124
+ border-inline-start: 1px solid var(--dg-border-strong);
3125
+ }
3126
+
3127
+ /* ─────────────────────────────────────────────────────────────
3128
+ KANUNILABS DataGrid — palettes
3129
+ ─────────────────────────────────────────────────────────────
3130
+ A palette is a SECOND, independent axis: it changes the colours and nothing
3131
+ else. The theme keeps deciding the structure — where the rules are drawn, how
3132
+ tall a row is, how round a control is, how much the popups lift — so
3133
+ `theme="balham" palette="nord"` is Balham's dense ruled grid in Nord's arctic
3134
+ blues, and `theme="material" palette="nord"` is Material's tall airy one in
3135
+ the same blues. Four themes and six palettes is twenty-four looks out of ten
3136
+ class names, not twenty-four stylesheets.
3137
+
3138
+ That split is only possible because the token layer already separates the two
3139
+ — the theme blocks set metrics AND colours, and everything here overrides the
3140
+ colour half of that.
3141
+
3142
+ ── Where these colours come from ──────────────────────────
3143
+ The six are the colour schemes people already recognise from their editors
3144
+ and terminals, and each is used the way its authors published it: the hues
3145
+ below are the scheme's own, mapped onto our token vocabulary. What is OURS is
3146
+ the mapping — which of a scheme's colours becomes the header, the stripe, the
3147
+ accent, the thing that has to stay readable on the accent — and every
3148
+ contrast decision that follows from it. Nothing here is imported, and no
3149
+ stylesheet from any of those projects is involved.
3150
+
3151
+ Several are dark-first schemes. Where the authors published a light
3152
+ counterpart it is used (Solarized Light, Catppuccin Latte, Gruvbox light,
3153
+ Tokyo Night Day); where they did not, the light block is our own reading of
3154
+ the same hue family, and says so.
3155
+
3156
+ ── Ordering ───────────────────────────────────────────────
3157
+ Concatenated AFTER DataGridThemes.css. A palette block matches the theme
3158
+ blocks' specificity exactly — three roots, two classes — so source order is
3159
+ what makes the palette win, and the dark variants sit one class higher for
3160
+ the same reason. Getting this backwards would leave the palette silently
3161
+ ignored, which is why `themes.test.ts` checks the file order.
3162
+ ───────────────────────────────────────────────────────────── */
3163
+
3164
+ /* ═══════════════════════════════════════════════════════════
3165
+ NORD — arctic, desaturated blue-grey
3166
+ ═══════════════════════════════════════════════════════════
3167
+ Polar Night for the dark surfaces, Snow Storm for the light ones, Frost for
3168
+ the accent. The calmest of the six: low saturation everywhere, so a dense
3169
+ grid of numbers does not vibrate. */
3170
+ .kanuni-datagrid-root.kanuni-datagrid-palette-nord,
3171
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-nord,
3172
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-nord {
3173
+ --dg-bg: #ffffff;
3174
+ --dg-surface: #ffffff;
3175
+ --dg-surface-header: #eceff4; /* Snow Storm nord6 */
3176
+ --dg-surface-hover: #e5e9f0; /* nord5 */
3177
+ --dg-surface-stripe: #f7f9fb;
3178
+ --dg-border: #dde3ec;
3179
+ --dg-border-strong: #b8c2d0;
3180
+ --dg-text: #2e3440; /* Polar Night nord0 */
3181
+ --dg-text-muted: #55607a;
3182
+ /* Frost nord10 (#5e81ac) is the scheme's accent and measures 4.29:1 under
3183
+ white — just under the threshold for the chip label that sits on it. One
3184
+ step deeper, same Frost hue. */
3185
+ --dg-primary: #4c6d94;
3186
+ --dg-primary-soft: #dbe3ee;
3187
+ --dg-on-primary: #ffffff; /* 5.24:1 */
3188
+ --dg-focus-ring: #4c6d94;
3189
+ --dg-selected-bg: #dbe3ee;
3190
+ --dg-group-bg: #e9edf3;
3191
+ --dg-footer-bg: #eceff4;
3192
+ --dg-scrollbar-thumb: #b8c2d0;
3193
+ --dg-danger: #a2515a; /* Aurora nord11, deepened for white text */
3194
+ --dg-danger-text: #8f4149;
3195
+ --dg-on-danger: #ffffff;
3196
+ --dg-warning: #8a6220; /* nord13 family */
3197
+ }
3198
+
3199
+ .kanuni-datagrid-root.kanuni-datagrid-palette-nord.kanuni-datagrid-dark,
3200
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-nord.kanuni-datagrid-dark,
3201
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-nord.kanuni-datagrid-dark {
3202
+ --dg-bg: #2e3440; /* nord0 */
3203
+ --dg-surface: #2e3440;
3204
+ --dg-surface-header: #3b4252; /* nord1 */
3205
+ --dg-surface-hover: #434c5e; /* nord2 */
3206
+ --dg-surface-stripe: #333a48;
3207
+ --dg-border: #434c5e;
3208
+ --dg-border-strong: #4c566a; /* nord3 */
3209
+ --dg-text: #eceff4; /* nord6 */
3210
+ --dg-text-muted: #b3bccb;
3211
+ --dg-primary: #88c0d0; /* Frost nord8 */
3212
+ --dg-primary-soft: #3c5464;
3213
+ --dg-on-primary: #17222b; /* 9.11:1 */
3214
+ --dg-focus-ring: #8fbcbb;
3215
+ --dg-selected-bg: #3c5464;
3216
+ --dg-group-bg: #39404f;
3217
+ --dg-footer-bg: #3b4252;
3218
+ --dg-scrollbar-thumb: #4c566a;
3219
+ --dg-danger: #bf616a; /* nord11 */
3220
+ --dg-danger-text: #d9919a;
3221
+ /* nord11 is a muted red, so the label on it has to go further down than the
3222
+ usual near-black: #2b1113 measured 4.31:1 and this is 5.17:1. */
3223
+ --dg-on-danger: #170709;
3224
+ --dg-warning: #ebcb8b; /* nord13 */
3225
+ --dg-overlay: rgba(24, 28, 36, 0.72);
3226
+ }
3227
+
3228
+ /* ═══════════════════════════════════════════════════════════
3229
+ DRACULA — near-black violet with high-chroma accents
3230
+ ═══════════════════════════════════════════════════════════
3231
+ The scheme is dark by design. Its light counterpart here is ours: the same
3232
+ purple/pink family lifted onto a white ground, so switching `dark` does not
3233
+ jump to an unrelated hue. */
3234
+ .kanuni-datagrid-root.kanuni-datagrid-palette-dracula,
3235
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-dracula,
3236
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-dracula {
3237
+ --dg-bg: #ffffff;
3238
+ --dg-surface: #ffffff;
3239
+ --dg-surface-header: #f3eefb;
3240
+ --dg-surface-hover: #ebe3f7;
3241
+ --dg-surface-stripe: #faf7fe;
3242
+ --dg-border: #e4dcf0;
3243
+ --dg-border-strong: #c3b5da;
3244
+ --dg-text: #282a36; /* Dracula background, used as ink here */
3245
+ --dg-text-muted: #5d5f73;
3246
+ --dg-primary: #7449a8; /* Purple #bd93f9, deepened for white text */
3247
+ --dg-primary-soft: #ece2fa;
3248
+ --dg-on-primary: #ffffff; /* 6.87:1 */
3249
+ --dg-focus-ring: #7449a8;
3250
+ --dg-selected-bg: #ece2fa;
3251
+ --dg-group-bg: #f1ebfa;
3252
+ --dg-footer-bg: #f3eefb;
3253
+ --dg-scrollbar-thumb: #c3b5da;
3254
+ --dg-danger: #b03a5b; /* Pink #ff79c6 / Red #ff5555 family */
3255
+ --dg-danger-text: #98304d;
3256
+ --dg-on-danger: #ffffff;
3257
+ --dg-warning: #8a5a1c;
3258
+ }
3259
+
3260
+ .kanuni-datagrid-root.kanuni-datagrid-palette-dracula.kanuni-datagrid-dark,
3261
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-dracula.kanuni-datagrid-dark,
3262
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-dracula.kanuni-datagrid-dark {
3263
+ --dg-bg: #282a36; /* Background */
3264
+ --dg-surface: #282a36;
3265
+ --dg-surface-header: #343746; /* Current Line, lightened */
3266
+ --dg-surface-hover: #44475a; /* Current Line */
3267
+ --dg-surface-stripe: #2d2f3d;
3268
+ --dg-border: #44475a;
3269
+ --dg-border-strong: #565a72;
3270
+ --dg-text: #f8f8f2; /* Foreground */
3271
+ --dg-text-muted: #a7abc4; /* Comment #6272a4, lifted to clear AA */
3272
+ --dg-primary: #bd93f9; /* Purple */
3273
+ --dg-primary-soft: #453860;
3274
+ --dg-on-primary: #241638; /* 8.66:1 */
3275
+ --dg-focus-ring: #ff79c6; /* Pink */
3276
+ --dg-selected-bg: #453860;
3277
+ --dg-group-bg: #32354a;
3278
+ --dg-footer-bg: #343746;
3279
+ --dg-scrollbar-thumb: #565a72;
3280
+ --dg-danger: #ff5555; /* Red */
3281
+ --dg-danger-text: #ff8a8a;
3282
+ --dg-on-danger: #3b0000;
3283
+ --dg-warning: #f1fa8c; /* Yellow */
3284
+ --dg-overlay: rgba(18, 19, 26, 0.74);
3285
+ }
3286
+
3287
+ /* ═══════════════════════════════════════════════════════════
3288
+ SOLARIZED — the fixed-lightness classic
3289
+ ═══════════════════════════════════════════════════════════
3290
+ The one scheme designed as a matched light/dark PAIR, which is exactly what
3291
+ a grid with a `dark` switch wants: the accents are identical in both and only
3292
+ the eight base tones flip. */
3293
+ .kanuni-datagrid-root.kanuni-datagrid-palette-solarized,
3294
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-solarized,
3295
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-solarized {
3296
+ --dg-bg: #fdf6e3; /* base3 */
3297
+ --dg-surface: #fdf6e3;
3298
+ --dg-surface-header: #eee8d5; /* base2 */
3299
+ --dg-surface-hover: #e7e0cc;
3300
+ --dg-surface-stripe: #f7f0dd;
3301
+ --dg-border: #ddd6c1;
3302
+ --dg-border-strong: #bdb7a4;
3303
+ --dg-text: #073642; /* base02 */
3304
+ --dg-text-muted: #56707a; /* base00/base01 family */
3305
+ --dg-primary: #1f6f8b; /* blue #268bd2, deepened for white text */
3306
+ --dg-primary-soft: #dbe7ea;
3307
+ --dg-on-primary: #ffffff; /* 5.90:1 */
3308
+ --dg-focus-ring: #1f6f8b;
3309
+ --dg-selected-bg: #dbe7ea;
3310
+ --dg-group-bg: #eae3cf;
3311
+ --dg-footer-bg: #eee8d5;
3312
+ --dg-scrollbar-thumb: #bdb7a4;
3313
+ --dg-danger: #b3372c; /* red #dc322f, deepened */
3314
+ --dg-danger-text: #9c2f25;
3315
+ --dg-on-danger: #ffffff;
3316
+ --dg-warning: #8a5a00; /* yellow #b58900, deepened */
3317
+ }
3318
+
3319
+ .kanuni-datagrid-root.kanuni-datagrid-palette-solarized.kanuni-datagrid-dark,
3320
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-solarized.kanuni-datagrid-dark,
3321
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-solarized.kanuni-datagrid-dark {
3322
+ --dg-bg: #002b36; /* base03 */
3323
+ --dg-surface: #002b36;
3324
+ --dg-surface-header: #073642; /* base02 */
3325
+ --dg-surface-hover: #0a4351;
3326
+ --dg-surface-stripe: #01313d;
3327
+ --dg-border: #0d4a58;
3328
+ --dg-border-strong: #23606c;
3329
+ --dg-text: #eee8d5; /* base2 */
3330
+ --dg-text-muted: #a4b3b6;
3331
+ --dg-primary: #4fa3d9; /* blue, lifted for a dark ground */
3332
+ --dg-primary-soft: #0d4457;
3333
+ --dg-on-primary: #00212b; /* 7.62:1 */
3334
+ --dg-focus-ring: #6fc3df;
3335
+ --dg-selected-bg: #0d4457;
3336
+ --dg-group-bg: #05323d;
3337
+ --dg-footer-bg: #073642;
3338
+ --dg-scrollbar-thumb: #23606c;
3339
+ --dg-danger: #e07a72;
3340
+ --dg-danger-text: #f0a49e;
3341
+ --dg-on-danger: #34110d;
3342
+ --dg-warning: #d5a63b;
3343
+ --dg-overlay: rgba(0, 20, 26, 0.74);
3344
+ }
3345
+
3346
+ /* ═══════════════════════════════════════════════════════════
3347
+ GRUVBOX — warm retro, high contrast
3348
+ ═══════════════════════════════════════════════════════════
3349
+ The warmest of the six, and the only one whose light mode is genuinely warm
3350
+ rather than a cooled-off white. Both modes are the scheme's own. */
3351
+ .kanuni-datagrid-root.kanuni-datagrid-palette-gruvbox,
3352
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-gruvbox,
3353
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-gruvbox {
3354
+ --dg-bg: #fbf1c7; /* light bg0 */
3355
+ --dg-surface: #fbf1c7;
3356
+ --dg-surface-header: #ebdbb2; /* bg1 */
3357
+ --dg-surface-hover: #e3d3a8;
3358
+ --dg-surface-stripe: #f5ecc0;
3359
+ --dg-border: #ddcfa6;
3360
+ --dg-border-strong: #bdae8b;
3361
+ --dg-text: #3c3836; /* fg0 */
3362
+ --dg-text-muted: #665c54; /* fg3 */
3363
+ --dg-primary: #076678; /* light blue */
3364
+ --dg-primary-soft: #d9e5e6;
3365
+ --dg-on-primary: #ffffff; /* 7.71:1 */
3366
+ --dg-focus-ring: #076678;
3367
+ --dg-selected-bg: #d9e5e6;
3368
+ --dg-group-bg: #e8d9b0;
3369
+ --dg-footer-bg: #ebdbb2;
3370
+ --dg-scrollbar-thumb: #bdae8b;
3371
+ --dg-danger: #9d0006; /* light red */
3372
+ --dg-danger-text: #9d0006;
3373
+ --dg-on-danger: #ffffff;
3374
+ /* The scheme's light yellow (#b57614) is 3.33:1 on this cream ground — the
3375
+ warmth that makes Gruvbox work is also what makes its yellow weak here.
3376
+ Same hue, two steps down, 4.72:1. */
3377
+ --dg-warning: #8f5c0a;
3378
+ }
3379
+
3380
+ .kanuni-datagrid-root.kanuni-datagrid-palette-gruvbox.kanuni-datagrid-dark,
3381
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-gruvbox.kanuni-datagrid-dark,
3382
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-gruvbox.kanuni-datagrid-dark {
3383
+ --dg-bg: #282828; /* dark bg0 */
3384
+ --dg-surface: #282828;
3385
+ --dg-surface-header: #3c3836; /* bg1 */
3386
+ --dg-surface-hover: #504945; /* bg2 */
3387
+ --dg-surface-stripe: #2f2c2b;
3388
+ --dg-border: #504945;
3389
+ --dg-border-strong: #665c54; /* bg3 */
3390
+ --dg-text: #fbf1c7; /* fg0 */
3391
+ --dg-text-muted: #bdae93; /* fg3 */
3392
+ --dg-primary: #83a598; /* dark blue */
3393
+ --dg-primary-soft: #3b4a45;
3394
+ --dg-on-primary: #16211d; /* 8.24:1 */
3395
+ --dg-focus-ring: #8ec07c; /* aqua */
3396
+ --dg-selected-bg: #3b4a45;
3397
+ --dg-group-bg: #37332f;
3398
+ --dg-footer-bg: #3c3836;
3399
+ --dg-scrollbar-thumb: #665c54;
3400
+ --dg-danger: #fb4934; /* dark red */
3401
+ --dg-danger-text: #fb8b7f;
3402
+ --dg-on-danger: #2c0500;
3403
+ --dg-warning: #fabd2f; /* dark yellow */
3404
+ --dg-overlay: rgba(20, 20, 20, 0.74);
3405
+ }
3406
+
3407
+ /* ═══════════════════════════════════════════════════════════
3408
+ CATPPUCCIN — pastel, low contrast between surfaces
3409
+ ═══════════════════════════════════════════════════════════
3410
+ Latte for light, Mocha for dark. The softest surfaces of the six: the
3411
+ stripe, hover and header sit very close together, which suits a grid the
3412
+ user reads for a long stretch. */
3413
+ .kanuni-datagrid-root.kanuni-datagrid-palette-catppuccin,
3414
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-catppuccin,
3415
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-catppuccin {
3416
+ --dg-bg: #eff1f5; /* Latte base */
3417
+ --dg-surface: #eff1f5;
3418
+ --dg-surface-header: #e6e9ef; /* mantle */
3419
+ --dg-surface-hover: #dce0e8; /* crust */
3420
+ --dg-surface-stripe: #eaecf1;
3421
+ --dg-border: #d6dae4;
3422
+ --dg-border-strong: #bcc0cc; /* surface1 */
3423
+ --dg-text: #4c4f69; /* text */
3424
+ --dg-text-muted: #5c5f77; /* subtext1 */
3425
+ --dg-primary: #1e66f5; /* blue */
3426
+ --dg-primary-soft: #dce4fb;
3427
+ --dg-on-primary: #ffffff; /* 4.88:1 */
3428
+ --dg-focus-ring: #1e66f5;
3429
+ --dg-selected-bg: #dce4fb;
3430
+ --dg-group-bg: #e4e7ee;
3431
+ --dg-footer-bg: #e6e9ef;
3432
+ --dg-scrollbar-thumb: #bcc0cc;
3433
+ --dg-danger: #c4304a; /* red #d20f39, deepened for white text */
3434
+ --dg-danger-text: #b02540;
3435
+ --dg-on-danger: #ffffff;
3436
+ --dg-warning: #995f14; /* peach, deepened */
3437
+ }
3438
+
3439
+ .kanuni-datagrid-root.kanuni-datagrid-palette-catppuccin.kanuni-datagrid-dark,
3440
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-catppuccin.kanuni-datagrid-dark,
3441
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-catppuccin.kanuni-datagrid-dark {
3442
+ --dg-bg: #1e1e2e; /* Mocha base */
3443
+ --dg-surface: #1e1e2e;
3444
+ --dg-surface-header: #313244; /* surface0 */
3445
+ --dg-surface-hover: #45475a; /* surface1 */
3446
+ --dg-surface-stripe: #242436;
3447
+ --dg-border: #45475a;
3448
+ --dg-border-strong: #585b70; /* surface2 */
3449
+ --dg-text: #cdd6f4; /* text */
3450
+ --dg-text-muted: #a6adc8; /* subtext0 */
3451
+ --dg-primary: #89b4fa; /* blue */
3452
+ --dg-primary-soft: #33405e;
3453
+ --dg-on-primary: #11182b; /* 8.83:1 */
3454
+ --dg-focus-ring: #b4befe; /* lavender */
3455
+ --dg-selected-bg: #33405e;
3456
+ --dg-group-bg: #2a2b3c;
3457
+ --dg-footer-bg: #313244;
3458
+ --dg-scrollbar-thumb: #585b70;
3459
+ --dg-danger: #f38ba8; /* red */
3460
+ --dg-danger-text: #f5a3ba;
3461
+ --dg-on-danger: #3d0b1a;
3462
+ --dg-warning: #fab387; /* peach */
3463
+ --dg-overlay: rgba(14, 14, 22, 0.74);
3464
+ }
3465
+
3466
+ /* ═══════════════════════════════════════════════════════════
3467
+ TOKYO NIGHT — deep navy with violet and cyan
3468
+ ═══════════════════════════════════════════════════════════
3469
+ The most saturated of the dark six, and the one whose accent reads as a
3470
+ colour rather than a tint. Tokyo Night Day for the light mode. */
3471
+ .kanuni-datagrid-root.kanuni-datagrid-palette-tokyo-night,
3472
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-tokyo-night,
3473
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-tokyo-night {
3474
+ --dg-bg: #e1e2e7; /* Day base */
3475
+ --dg-surface: #e1e2e7;
3476
+ --dg-surface-header: #d4d6de;
3477
+ --dg-surface-hover: #c9cbd6;
3478
+ --dg-surface-stripe: #dcdde3;
3479
+ --dg-border: #c3c6d1;
3480
+ --dg-border-strong: #a3a7b8;
3481
+ /* Tokyo Night Day's #3760bf is its KEYWORD blue, not its body ink — using it
3482
+ as the text colour measured 4.03:1 on the header and 4.26:1 on a selected
3483
+ row. The scheme's actual foreground is this darker slate-blue, which keeps
3484
+ the character and clears every surface. */
3485
+ --dg-text: #343b58;
3486
+ --dg-text-muted: #4c5478;
3487
+ --dg-primary: #2e5cb8;
3488
+ --dg-primary-soft: #d2dcf3;
3489
+ --dg-on-primary: #ffffff; /* 5.64:1 */
3490
+ --dg-focus-ring: #2e5cb8;
3491
+ --dg-selected-bg: #d2dcf3;
3492
+ --dg-group-bg: #d8dae2;
3493
+ --dg-footer-bg: #d4d6de;
3494
+ --dg-scrollbar-thumb: #a3a7b8;
3495
+ --dg-danger: #b1364a;
3496
+ --dg-danger-text: #9d2c3f;
3497
+ --dg-on-danger: #ffffff;
3498
+ --dg-warning: #8a5a12;
3499
+ }
3500
+
3501
+ .kanuni-datagrid-root.kanuni-datagrid-palette-tokyo-night.kanuni-datagrid-dark,
3502
+ .kanuni-datagrid-enterprise.kanuni-datagrid-palette-tokyo-night.kanuni-datagrid-dark,
3503
+ .kanuni-datagrid-detached.kanuni-datagrid-palette-tokyo-night.kanuni-datagrid-dark {
3504
+ --dg-bg: #1a1b26; /* Night bg */
3505
+ --dg-surface: #1a1b26;
3506
+ --dg-surface-header: #24283b; /* Storm bg */
3507
+ --dg-surface-hover: #2f334d;
3508
+ --dg-surface-stripe: #1f2030;
3509
+ --dg-border: #2f334d;
3510
+ --dg-border-strong: #414868;
3511
+ --dg-text: #c0caf5; /* fg */
3512
+ --dg-text-muted: #9aa5ce;
3513
+ --dg-primary: #7aa2f7; /* blue */
3514
+ --dg-primary-soft: #2b3a5e;
3515
+ --dg-on-primary: #0d1220; /* 8.05:1 */
3516
+ --dg-focus-ring: #7dcfff; /* cyan */
3517
+ --dg-selected-bg: #2b3a5e;
3518
+ --dg-group-bg: #212335;
3519
+ --dg-footer-bg: #24283b;
3520
+ --dg-scrollbar-thumb: #414868;
3521
+ --dg-danger: #f7768e; /* red */
3522
+ --dg-danger-text: #f993a6;
3523
+ --dg-on-danger: #3a0713;
3524
+ --dg-warning: #e0af68; /* yellow */
3525
+ --dg-overlay: rgba(10, 11, 18, 0.76);
3526
+ }