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