@kanunilabs/datagrid-react 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/styles.css CHANGED
@@ -1,2354 +1,2383 @@
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
- }
1
+ /* ─────────────────────────────────────────────────────────────
2
+ KANUNILABS DataGrid — isolated theme system
3
+ ─────────────────────────────────────────────────────────────
4
+ Every rule is scoped under .kanuni-datagrid-root so the host app's CSS
5
+ cannot bleed in and ours cannot bleed out. Tokens mirror the PivotGrid
6
+ vocabulary (--pivot-*) with a --dg-* prefix so the two products can be
7
+ themed side by side without collisions.
8
+ ───────────────────────────────────────────────────────────── */
9
+
10
+ /* ── Design tokens ───────────────────────────────────────────
11
+ Declared on more than the grid root because two things render OUTSIDE it
12
+ and still have to look like the grid: the enterprise wrapper (which owns
13
+ the row/form editor, a SIBLING of the root) and any layer portalled to
14
+ <body> (the lookup list, tagged `.kanuni-datagrid-detached`).
15
+
16
+ Tokens defined only on the root left those resolving `var(--dg-surface)` to
17
+ nothing — which is not a fallback but an INVALID declaration, so the popup
18
+ editor rendered fully transparent and unreadable over the grid.
19
+
20
+ Only the custom properties live here. The grid's own box (border, radius,
21
+ overflow, flex) stays on the root below: giving it to the wrapper would draw
22
+ a second border around the whole component and clip the editor. */
23
+ .kanuni-datagrid-root,
24
+ .kanuni-datagrid-enterprise,
25
+ .kanuni-datagrid-detached {
26
+ /* ── Color tokens ── */
27
+ --dg-bg: #ffffff;
28
+ --dg-surface: #ffffff;
29
+ --dg-surface-header: #f8fafc; /* slate-50 */
30
+ --dg-surface-hover: #f1f5f9; /* slate-100 */
31
+ --dg-surface-stripe: #fafafa;
32
+ --dg-border: #e2e8f0; /* slate-200 */
33
+ --dg-border-strong: #cbd5e1; /* slate-300 */
34
+ --dg-text: #0f172a; /* slate-900 */
35
+ --dg-text-muted: #64748b; /* slate-500 */
36
+ /* sky-700, and the depth is the point rather than the hue.
37
+ This was sky-500 (#0ea5e9), which is the accent EVERY foreground in the
38
+ grid is drawn from: the sort caret over the header, the active funnel, the
39
+ editor underline, the detail rule, and the white label on a group chip or
40
+ a primary button. On a light surface sky-500 measures 2.77:1 under white
41
+ and 2.65:1 as a caret on the header — under the 4.5:1 that text needs and
42
+ under the 3:1 that an icon needs. sky-600 was not enough either (4.10:1
43
+ white-on). sky-700 clears both: 5.93:1 under white, 5.67:1 on the header.
44
+ The dark palette below keeps sky-400 — it is already at 8.33:1 there,
45
+ because the problem was never the hue, it was using a mid-tone accent
46
+ against a white background. */
47
+ --dg-primary: #0369a1; /* sky-700 */
48
+ --dg-primary-soft: #e0f2fe; /* sky-100 */
49
+ --dg-focus-ring: #0369a1; /* matches the accent */
50
+ /* Cell flash (P1.7). Translucent so the row's own background — striped,
51
+ selected, dirty — stays visible under the pulse rather than being
52
+ replaced by it. */
53
+ --dg-flash-bg: rgba(3, 105, 161, 0.35);
54
+ --dg-flash-duration: 700ms;
55
+ /* Cell note marker (P3.3). Red on purpose and NOT the accent: the accent
56
+ means "the grid did this" (focus, selection, sort), and a note means
57
+ "a person put something here" — two different kinds of signal. */
58
+ --dg-note-marker: #dc2626;
59
+
60
+ /* ── Semantic colours ──────────────────────────────────────
61
+ Every default below is the literal that used to be written inline at the
62
+ point of use, so a grid with no theme renders exactly as before. They are
63
+ tokens now because a theme has to be able to move them: `--dg-on-primary`
64
+ is the clearest case — the KanuniLabs accent is a GOLD, and the white that
65
+ reads fine on sky-500 measures about 2:1 on it. A theme that can recolour
66
+ the chip but not the text on the chip is a theme that ships unreadable
67
+ buttons. */
68
+ /* The header row is its own surface, separate from `--dg-surface-header`.
69
+ They default to the same value, and every theme here but one keeps them
70
+ that way — but a corporate theme with an INK header still wants the group
71
+ panel, the filter row and the footer to stay light. Without the split, the
72
+ one token dragged all four along and the panel hints (muted text) went
73
+ unreadable on the dark fill. */
74
+ --dg-header-bg: var(--dg-surface-header);
75
+ --dg-header-text: var(--dg-text);
76
+ --dg-header-text-muted: var(--dg-text-muted);
77
+ --dg-header-hover: var(--dg-surface-hover);
78
+ --dg-on-primary: #ffffff;
79
+ --dg-danger: #dc2626; /* red-600 — borders, solid fills */
80
+ --dg-danger-text: #b91c1c; /* red-700 — text on a light surface */
81
+ --dg-on-danger: #ffffff; /* text ON a solid --dg-danger fill */
82
+ --dg-warning: #b45309; /* amber-700 */
83
+ --dg-selected-bg: var(--dg-primary-soft);
84
+ /* Search highlight (P2.4). Amber rather than the accent: the accent already
85
+ means "selected" and "focused" in this grid, and a third meaning on the
86
+ same colour would make a highlighted cell read as a selected one. */
87
+ --dg-highlight-bg: #fde68a; /* amber-200 */
88
+ --dg-highlight-text: #713f12; /* amber-900 — 8.9:1 on the above */
89
+ --dg-group-bg: var(--dg-surface-header);
90
+ --dg-footer-bg: var(--dg-surface-header);
91
+ --dg-overlay: rgba(15, 23, 42, 0.55);
92
+ --dg-scrollbar-thumb: var(--dg-border-strong);
93
+ --dg-scrollbar-track: transparent;
94
+
95
+ /* ── Elevation ──
96
+ A flat theme sets these to `none`; Material raises them. */
97
+ --dg-shadow-popup: 0 10px 25px -5px rgba(15, 23, 42, 0.25);
98
+ --dg-shadow-popup-sm: 0 8px 24px rgba(15, 23, 42, 0.16);
99
+ --dg-shadow-menu: 0 12px 28px rgba(15, 23, 42, 0.22);
100
+ --dg-shadow-card: 0 12px 32px rgba(15, 23, 42, 0.18);
101
+ --dg-shadow-panel: 0 8px 20px rgba(0, 0, 0, 0.12);
102
+ --dg-shadow-dialog: 0 24px 48px rgba(0, 0, 0, 0.28);
103
+
104
+ /* ── Metric tokens ── */
105
+ --dg-row-height: 28px;
106
+ --dg-header-height: 32px;
107
+ --dg-cell-padding-x: 8px;
108
+ --dg-font-size: 13px;
109
+ --dg-header-font-size: 12px;
110
+ --dg-radius: 6px;
111
+ --dg-shadow-pinned: 2px 0 4px -2px rgba(15, 23, 42, 0.18);
112
+ --dg-shadow-pinned-right: -2px 0 4px -2px rgba(15, 23, 42, 0.18);
113
+
114
+ /* ── Line work ─────────────────────────────────────────────
115
+ Whole SHORTHANDS rather than a colour and a width, so a theme can drop a
116
+ line entirely (`none`) instead of only fading it. This is most of what
117
+ separates the presets: Balham rules every column and every row, Material
118
+ rules rows only, Quartz keeps the rows faint and skips the columns. */
119
+ --dg-cell-border: 1px solid var(--dg-border);
120
+ --dg-row-border: 1px solid var(--dg-border);
121
+ --dg-header-cell-border: 1px solid var(--dg-border);
122
+ --dg-header-border: 1px solid var(--dg-border-strong);
123
+ /* A band's EDGE is not a column separator, so it gets its own token rather
124
+ than riding on `--dg-header-cell-border`. Quartz and Material set that one
125
+ to `none` on purpose — they rule rows, not columns — and the bands went
126
+ with it: "Where" and "Sales" sat over their columns with nothing marking
127
+ where either one started or stopped. Same argument the band row's
128
+ `border-bottom` already makes: a band boundary is structural. A theme can
129
+ still override this token if it wants a different line.
130
+
131
+ Mixed from the header TEXT rather than taken from a border token, because
132
+ both border tokens are too faint for a 24px vertical segment: measured on
133
+ Quartz against the header background, `--dg-border` gives 1.17:1 and
134
+ `--dg-border-strong` 1.39:1 — lines that are technically drawn and
135
+ practically invisible, i.e. a fix that changes nothing the user sees. The
136
+ mix lands near 2:1 and follows the theme in both light and dark, since the
137
+ header text is what already has to be readable on that ground. */
138
+ --dg-band-cell-border: 1px solid color-mix(in srgb, var(--dg-header-text) 32%, transparent);
139
+
140
+ /* ── Header treatment ── */
141
+ --dg-header-weight: 600;
142
+ --dg-header-transform: none;
143
+ --dg-header-tracking: normal;
144
+
145
+ /* ── Chrome (toolbar, pager, panels, popups) ──
146
+ Sized separately from the data surface: a compact grid shrinks its rows
147
+ hard, but a 9px button is not usable at any density. */
148
+ --dg-chrome-font-size: 12px;
149
+ --dg-chrome-padding-y: 6px;
150
+ --dg-control-height: 26px;
151
+ --dg-control-font-size: 12px;
152
+ --dg-filter-input-height: 22px;
153
+ --dg-radius-control: 4px;
154
+
155
+ /* The line box a WRAPPED cell lays its text out on, and the height an editor
156
+ takes in one. Whole pixels — see the note on `--wrap` below. */
157
+ --dg-wrap-line-height: 18px;
158
+ }
159
+
160
+ /* Detached surfaces carry the foreground as well as the tokens, for the reason
161
+ the enterprise wrapper does: a portalled layer is a child of <body>, so
162
+ anything inside it written as `color: inherit` would resolve against the host
163
+ page rather than against the grid it belongs to. The dialog and the row
164
+ editor set their own colour, but the box AROUND them is the safety net. */
165
+ .kanuni-datagrid-detached {
166
+ color: var(--dg-text);
167
+ }
168
+
169
+ /* The grid's own box. */
170
+ .kanuni-datagrid-root {
171
+ box-sizing: border-box;
172
+ font-family: inherit;
173
+ -webkit-font-smoothing: antialiased;
174
+ -moz-osx-font-smoothing: grayscale;
175
+ position: relative;
176
+ display: flex;
177
+ flex-direction: column;
178
+ overflow: hidden;
179
+ border: 1px solid var(--dg-border);
180
+ border-radius: var(--dg-radius);
181
+ background: var(--dg-bg);
182
+ color: var(--dg-text);
183
+ font-size: var(--dg-font-size);
184
+ line-height: 1.4;
185
+ }
186
+
187
+ .kanuni-datagrid-root *,
188
+ .kanuni-datagrid-root *::before,
189
+ .kanuni-datagrid-root *::after {
190
+ box-sizing: border-box;
191
+ }
192
+
193
+ /* Dark theme: opt in with .kanuni-datagrid-root.kanuni-datagrid-dark */
194
+ .kanuni-datagrid-root.kanuni-datagrid-dark,
195
+ .kanuni-datagrid-enterprise.kanuni-datagrid-dark,
196
+ .kanuni-datagrid-detached.kanuni-datagrid-dark {
197
+ --dg-bg: #0b1120;
198
+ --dg-surface: #0f172a;
199
+ --dg-surface-header: #16233c;
200
+ --dg-surface-hover: #1e293b;
201
+ --dg-surface-stripe: #101a2e;
202
+ --dg-border: #24344d;
203
+ --dg-border-strong: #334155;
204
+ --dg-text: #e2e8f0;
205
+ --dg-text-muted: #94a3b8;
206
+ --dg-primary: #38bdf8;
207
+ --dg-primary-soft: #0c4a6e;
208
+ --dg-focus-ring: #7dd3fc;
209
+ /* Lifted for a dark surface: crimson on near-black reads as brown. */
210
+ --dg-note-marker: #f87171;
211
+ --dg-shadow-pinned: 2px 0 4px -2px rgba(0, 0, 0, 0.55);
212
+ --dg-shadow-pinned-right: -2px 0 4px -2px rgba(0, 0, 0, 0.55);
213
+
214
+ /* The semantic set, re-pointed for a dark surface. `--dg-on-primary` goes
215
+ DARK here because the dark accent is a light sky-400. */
216
+ --dg-on-primary: #082f49;
217
+ /* Dark: a muted amber fill with light text, so the mark reads as emphasis
218
+ instead of glowing off a dark surface the way amber-200 would. */
219
+ --dg-highlight-bg: #78350f; /* amber-900 */
220
+ --dg-highlight-text: #fef3c7; /* amber-100 — 9.6:1 on the above */
221
+ --dg-danger: #f87171;
222
+ --dg-danger-text: #fca5a5;
223
+ /* The dark danger is a LIGHT red, so what sits on it has to be dark. White
224
+ on #f87171 measures 2.4:1 — the delete button's own label. */
225
+ --dg-on-danger: #450a0a;
226
+ --dg-warning: #fbbf24;
227
+ --dg-overlay: rgba(2, 6, 23, 0.7);
228
+ --dg-scrollbar-thumb: #475569;
229
+ --dg-shadow-popup: 0 10px 25px -5px rgba(0, 0, 0, 0.6);
230
+ --dg-shadow-popup-sm: 0 8px 24px rgba(0, 0, 0, 0.5);
231
+ --dg-shadow-menu: 0 12px 28px rgba(0, 0, 0, 0.55);
232
+ --dg-shadow-card: 0 12px 32px rgba(0, 0, 0, 0.55);
233
+ --dg-shadow-panel: 0 8px 20px rgba(0, 0, 0, 0.45);
234
+ --dg-shadow-dialog: 0 24px 48px rgba(0, 0, 0, 0.65);
235
+ }
236
+
237
+ /* ── Single scroll container (§7.4: NO dual-table architecture) ────────
238
+ The header and the body live in ONE scroller, so horizontal alignment
239
+ cannot drift: there is no second element to keep in sync. */
240
+ .kanuni-datagrid-scroller {
241
+ position: relative;
242
+ flex: 1 1 auto;
243
+ overflow: auto;
244
+ overscroll-behavior: contain;
245
+ /* Scroll anchoring must be OFF in a virtualized scroller. Under scroll
246
+ compression the rows legitimately move relative to the canvas on every
247
+ scroll; Chrome's anchoring reads that as "content shifted under the user"
248
+ and walks scrollTop back to chase the anchor row. During a scrollbar drag
249
+ the correction is deferred and applied on mouse RELEASE — the reported
250
+ symptom (2026-08-14, 1M rows): the last rows peek out while dragging and
251
+ snap back when the button is let go, leaving the true bottom unreachable
252
+ by user scrolling while programmatic probes see nothing wrong. */
253
+ overflow-anchor: none;
254
+ background: var(--dg-surface);
255
+ }
256
+
257
+ /* Sized to the full virtual extent so the native scrollbar is accurate. */
258
+ .kanuni-datagrid-canvas {
259
+ position: relative;
260
+ }
261
+
262
+ /* ── Header ───────────────────────────────────────────────── */
263
+ /* ── Header bands (multi-level headers) ──────────────────────
264
+ Siblings of the header row, each sticky at its own offset so the whole
265
+ stack stays put while the rows scroll under it. */
266
+ .kanuni-datagrid-band-row {
267
+ position: sticky;
268
+ z-index: 4;
269
+ display: flex;
270
+ background: var(--dg-header-bg);
271
+ /* Deliberately a literal, not `--dg-header-cell-border`: that token is the
272
+ COLUMN separator, which some themes drop entirely. The line under a band
273
+ row is structural — without it a two-level header reads as one tall box. */
274
+ border-bottom: 1px solid var(--dg-border);
275
+ }
276
+
277
+ .kanuni-datagrid-band-cell {
278
+ box-sizing: border-box;
279
+ display: flex;
280
+ align-items: center;
281
+ justify-content: center;
282
+ min-width: 0;
283
+ padding: 0 8px;
284
+ background: var(--dg-header-bg);
285
+ color: var(--dg-header-text);
286
+ font-size: calc(var(--dg-font-size) - 1px);
287
+ font-weight: var(--dg-header-weight);
288
+ }
289
+
290
+ /* A real band is drawn as a bounded box, so its extent is readable: the right
291
+ edge on every one, and the left edge only where the thing before it is NOT
292
+ another band. Bands that touch therefore share one line instead of drawing
293
+ two beside each other. */
294
+ .kanuni-datagrid-band-cell:not(.kanuni-datagrid-band-cell--spacer) {
295
+ border-inline-end: var(--dg-band-cell-border);
296
+ }
297
+
298
+ .kanuni-datagrid-band-cell--spacer + .kanuni-datagrid-band-cell:not(.kanuni-datagrid-band-cell--spacer),
299
+ .kanuni-datagrid-band-cell:first-child:not(.kanuni-datagrid-band-cell--spacer) {
300
+ border-inline-start: var(--dg-band-cell-border);
301
+ }
302
+
303
+ /* No band at this level: the column's own header reaches up through here, so
304
+ the box must be INVISIBLE rather than an empty cell with borders. */
305
+ .kanuni-datagrid-band-cell--spacer {
306
+ border-inline-end: none;
307
+ border-inline-start: none;
308
+ }
309
+
310
+ .kanuni-datagrid-band-cell--pinned {
311
+ position: sticky;
312
+ z-index: 5;
313
+ }
314
+
315
+ .kanuni-datagrid-band-label {
316
+ overflow: hidden;
317
+ text-overflow: ellipsis;
318
+ white-space: nowrap;
319
+ }
320
+
321
+ .kanuni-datagrid-header {
322
+ position: sticky;
323
+ top: 0;
324
+ z-index: 3;
325
+ display: flex;
326
+ height: var(--dg-header-height);
327
+ background: var(--dg-header-bg);
328
+ border-bottom: var(--dg-header-border);
329
+ }
330
+
331
+ .kanuni-datagrid-header-cell {
332
+ /* Anchors the resize handle. Pinned headers override this with `sticky`,
333
+ which is also a positioned ancestor, so the handle works there too. */
334
+ position: relative;
335
+ display: flex;
336
+ align-items: center;
337
+ gap: 4px;
338
+ flex: none;
339
+ height: 100%;
340
+ padding: 0 var(--dg-cell-padding-x);
341
+ background: var(--dg-header-bg);
342
+ border-inline-end: var(--dg-header-cell-border);
343
+ font-size: var(--dg-header-font-size);
344
+ font-weight: var(--dg-header-weight);
345
+ letter-spacing: var(--dg-header-tracking);
346
+ text-transform: var(--dg-header-transform);
347
+ color: var(--dg-header-text);
348
+ white-space: nowrap;
349
+ overflow: hidden;
350
+ user-select: none;
351
+ }
352
+
353
+ .kanuni-datagrid-header-cell--sortable {
354
+ cursor: pointer;
355
+ }
356
+
357
+ .kanuni-datagrid-header-cell--sortable:hover {
358
+ background: var(--dg-header-hover);
359
+ }
360
+
361
+ .kanuni-datagrid-header-label {
362
+ overflow: hidden;
363
+ text-overflow: ellipsis;
364
+ white-space: nowrap;
365
+ }
366
+
367
+ .kanuni-datagrid-sort-indicator {
368
+ flex: none;
369
+ font-size: 10px;
370
+ line-height: 1;
371
+ color: var(--dg-primary);
372
+ }
373
+
374
+ .kanuni-datagrid-sort-order {
375
+ flex: none;
376
+ font-size: 9px;
377
+ color: var(--dg-header-text-muted);
378
+ }
379
+
380
+ /* ── Rows and cells ───────────────────────────────────────── */
381
+ .kanuni-datagrid-viewport {
382
+ position: absolute;
383
+ top: 0;
384
+ left: 0;
385
+ width: 100%;
386
+ will-change: transform;
387
+ }
388
+
389
+ .kanuni-datagrid-row {
390
+ display: flex;
391
+ height: var(--dg-row-height);
392
+ background: var(--dg-surface);
393
+ border-bottom: var(--dg-row-border);
394
+ /* Dragging a CELL RANGE must not also drag a text selection. The browser
395
+ starts one on any mousedown-and-move over text, so the user got two
396
+ overlapping highlights at once — the grid's range in pale blue and the
397
+ native selection in solid blue — with no way to tell which one the totals
398
+ strip was summing. The header and footer already do this; the data rows
399
+ were the gap. Copying is not lost: the range owns Ctrl+C. */
400
+ user-select: none;
401
+ }
402
+
403
+ /* ── Auto-height rows ──────────────────────────────────────
404
+ A grid with an `autoHeight` column lets its rows grow. The row's height is
405
+ NOT written inline here: it is measured after layout and handed to the window
406
+ engine, which owns the scroll canvas and every row offset. Setting it inline
407
+ as well would make the measurement read back whatever was set — the row would
408
+ agree with itself and never grow.
409
+
410
+ `min-height` keeps a one-line row exactly the height it was, so turning the
411
+ feature on does not resize the rows that did not need it. */
412
+ .kanuni-datagrid-root[data-auto-height] .kanuni-datagrid-row {
413
+ height: auto;
414
+ min-height: var(--dg-row-height);
415
+ }
416
+
417
+ /* Every OTHER cell of an autoHeight grid must stretch to its row.
418
+ The base rule's `height: 100%` cannot resolve against the row's
419
+ `height: auto`, so it computed as content height and every single-line cell
420
+ collapsed to its text — 18px inside a 28px row, worse inside a wrapped one.
421
+ Two user-visible symptoms (report, 2026-08-14): the focus rectangle hugged
422
+ the TEXT instead of the cell, and a click in the lower band of a cell landed
423
+ on the bare row — no column resolved, so nothing got focused. Stretching is
424
+ what the base rule always meant; `align-items: center` then keeps single-line
425
+ text centred exactly as in a fixed-height grid. The wrap cell is excluded:
426
+ its own block sizes it to its text on purpose — that is how the row learns
427
+ how tall to be. */
428
+ .kanuni-datagrid-root[data-auto-height] .kanuni-datagrid-row .kanuni-datagrid-cell:not(.kanuni-datagrid-cell--wrap) {
429
+ height: auto;
430
+ align-self: stretch;
431
+ }
432
+
433
+ /* Anything the user genuinely types into, or reads as prose, gets it back. */
434
+ .kanuni-datagrid-row input,
435
+ .kanuni-datagrid-row textarea,
436
+ .kanuni-datagrid-row select,
437
+ .kanuni-datagrid-detail-cell,
438
+ .kanuni-datagrid-editform-cell {
439
+ user-select: text;
440
+ }
441
+
442
+ .kanuni-datagrid-row--odd {
443
+ background: var(--dg-surface-stripe);
444
+ }
445
+
446
+ .kanuni-datagrid-row:hover,
447
+ .kanuni-datagrid-row:hover .kanuni-datagrid-cell--pinned {
448
+ background: var(--dg-surface-hover);
449
+ }
450
+
451
+ .kanuni-datagrid-cell {
452
+ display: flex;
453
+ align-items: center;
454
+ flex: none;
455
+ height: 100%;
456
+ padding: 0 var(--dg-cell-padding-x);
457
+ border-inline-end: var(--dg-cell-border);
458
+ white-space: nowrap;
459
+ overflow: hidden;
460
+ text-overflow: ellipsis;
461
+ }
462
+
463
+ /* ── autoHeight: the columns that wrap ─────────────────────
464
+ Only the columns that asked for it. Everything else keeps its single line and
465
+ its ellipsis — wrapping a whole grid because one column needed to is how a
466
+ dense table turns into a wall of text.
467
+
468
+ The doubled class is deliberate and load-bearing. `.kanuni-datagrid-cell--wrap`
469
+ alone has the same specificity as the base rule directly above, so whichever
470
+ came LAST in the file won; written earlier in the sheet it lost silently and
471
+ the cells kept `white-space: nowrap` while everything else about the feature
472
+ worked. Two classes settle it regardless of where this block sits. */
473
+ .kanuni-datagrid-cell.kanuni-datagrid-cell--wrap {
474
+ white-space: normal;
475
+ overflow: visible;
476
+ text-overflow: clip;
477
+ /* `anywhere` and not `break-word`: an unbroken 200-character token (a URL, a
478
+ serial number) otherwise overflows its column and lands on the next one. */
479
+ overflow-wrap: anywhere;
480
+ align-items: flex-start;
481
+ /* Not `height: 100%` — the cell has to be as tall as its own text, or the row
482
+ it is supposed to stretch never learns it needs to grow. */
483
+ height: auto;
484
+ align-self: stretch;
485
+ padding-block: 4px;
486
+ /* An INTEGER line box, not a ratio. `1.35` against the 13px body font lays
487
+ out at 17.55px, so every wrapped row landed on a fractional boundary and
488
+ its 1px border rendered a pixel thick on some rows and two on others.
489
+ A token now, because a theme moves the body font under it — 18px is right
490
+ for 13px text and cramped for Material's 14px. */
491
+ line-height: var(--dg-wrap-line-height);
492
+ }
493
+
494
+ .kanuni-datagrid-cell--number {
495
+ justify-content: flex-end;
496
+ font-variant-numeric: tabular-nums;
497
+ }
498
+
499
+ .kanuni-datagrid-cell--center {
500
+ justify-content: center;
501
+ }
502
+
503
+ /* ── Pinned columns = sticky cells inside the same row (§7.4) ──────────
504
+ Compositor-friendly and layout-free; structurally immune to the
505
+ header/body desync bug class of a dual-table design. */
506
+ .kanuni-datagrid-cell--pinned,
507
+ .kanuni-datagrid-header-cell--pinned {
508
+ position: sticky;
509
+ z-index: 2;
510
+ background: inherit;
511
+ }
512
+
513
+ /* HARDENING: a pinned cell is sticky, whatever else it becomes.
514
+ State classes keep anchoring their markers with `position: relative` —
515
+ the dirty corner, the range overlay, whatever comes next — and on a pinned
516
+ cell that override turns `insetInlineStart` from a parking threshold into a
517
+ literal shift: the cell jumps sideways by its whole inset and parks over its
518
+ neighbour. It happened twice from two different classes in one day
519
+ (range-anchor on click, dirty on commit), so the guarantee lives HERE at
520
+ three-class specificity instead of as a `:not(--pinned)` chased onto every
521
+ state rule. Sticky is itself a positioned ancestor, so those markers lose
522
+ nothing. */
523
+ .kanuni-datagrid-root .kanuni-datagrid-cell.kanuni-datagrid-cell--pinned {
524
+ position: sticky;
525
+ }
526
+
527
+ /* A STICKY column sits in the scrolling order and parks itself only once it
528
+ would leave the viewport. It needs a lower z-index than a truly pinned
529
+ column: when the two meet, the pinned one is the fixed landmark and has to
530
+ win, or the sticky column would slide over the top of it. */
531
+ .kanuni-datagrid-cell--sticky {
532
+ z-index: 1;
533
+ }
534
+
535
+ .kanuni-datagrid-header-cell--pinned {
536
+ z-index: 4;
537
+ background: var(--dg-header-bg);
538
+ }
539
+
540
+ .kanuni-datagrid-cell--pinned-left:last-of-type,
541
+ .kanuni-datagrid-header-cell--pinned-left:last-of-type {
542
+ box-shadow: var(--dg-shadow-pinned);
543
+ }
544
+
545
+ .kanuni-datagrid-pin-edge-left {
546
+ box-shadow: var(--dg-shadow-pinned);
547
+ }
548
+
549
+ .kanuni-datagrid-pin-edge-right {
550
+ box-shadow: var(--dg-shadow-pinned-right);
551
+ }
552
+
553
+ /* Spacer that reserves the width of columns outside the horizontal window. */
554
+ .kanuni-datagrid-spacer {
555
+ flex: none;
556
+ height: 100%;
557
+ }
558
+
559
+ /* ── Toolbar ──────────────────────────────────────────────── */
560
+ .kanuni-datagrid-toolbar {
561
+ display: flex;
562
+ align-items: center;
563
+ gap: 6px;
564
+ flex: none;
565
+ padding: var(--dg-chrome-padding-y) 8px;
566
+ background: var(--dg-surface);
567
+ border-bottom: 1px solid var(--dg-border);
568
+ font-size: var(--dg-chrome-font-size);
569
+ }
570
+
571
+ .kanuni-datagrid-toolbar-spacer {
572
+ flex: 1 1 auto;
573
+ }
574
+
575
+ .kanuni-datagrid-toolbar-search {
576
+ width: min(260px, 40%);
577
+ height: var(--dg-control-height);
578
+ padding: 0 8px;
579
+ border: 1px solid var(--dg-border-strong);
580
+ border-radius: var(--dg-radius-control);
581
+ background: var(--dg-surface);
582
+ color: var(--dg-text);
583
+ font: inherit;
584
+ font-size: var(--dg-control-font-size);
585
+ }
586
+
587
+ .kanuni-datagrid-toolbar-search:focus {
588
+ outline: none;
589
+ border-color: var(--dg-primary);
590
+ }
591
+
592
+ .kanuni-datagrid-toolbar > button {
593
+ padding: 3px 10px;
594
+ border: 1px solid var(--dg-border-strong);
595
+ border-radius: var(--dg-radius-control);
596
+ background: var(--dg-surface);
597
+ color: inherit;
598
+ font: inherit;
599
+ font-size: var(--dg-control-font-size);
600
+ cursor: pointer;
601
+ }
602
+
603
+ .kanuni-datagrid-toolbar > button:hover:not(:disabled) {
604
+ background: var(--dg-surface-hover);
605
+ }
606
+
607
+ .kanuni-datagrid-toolbar > button:disabled {
608
+ opacity: 0.6;
609
+ cursor: default;
610
+ }
611
+
612
+ .kanuni-datagrid-toolbar-error {
613
+ color: var(--dg-danger-text);
614
+ font-size: 11px;
615
+ }
616
+
617
+ /*
618
+ * The same message, but floating where a context menu just was — the cell
619
+ * menu's Export entries have nowhere else to report a missing optional peer.
620
+ * Sized and boxed like a menu so it reads as coming from one; it is a BUTTON
621
+ * because clicking it dismisses it, and a click target has to be one.
622
+ */
623
+ .kanuni-datagrid-menu-error {
624
+ position: fixed;
625
+ /* Same layer as the context menu it replaces — see the rule further down. */
626
+ z-index: 1200;
627
+ max-width: 320px;
628
+ padding: 8px 10px;
629
+ text-align: left;
630
+ background: var(--dg-surface);
631
+ border: 1px solid var(--dg-danger-text);
632
+ border-radius: var(--dg-radius);
633
+ box-shadow: var(--dg-shadow-menu);
634
+ font-size: var(--dg-font-size);
635
+ cursor: pointer;
636
+ }
637
+
638
+ .kanuni-datagrid-column-chooser .kanuni-datagrid-filter-item {
639
+ cursor: pointer;
640
+ }
641
+
642
+ /* ── Pager ────────────────────────────────────────────────── */
643
+ .kanuni-datagrid-pager {
644
+ display: flex;
645
+ align-items: center;
646
+ flex-wrap: wrap;
647
+ gap: 12px;
648
+ flex: none;
649
+ padding: 5px 8px;
650
+ background: var(--dg-surface);
651
+ border-top: 1px solid var(--dg-border);
652
+ font-size: var(--dg-chrome-font-size);
653
+ }
654
+
655
+ .kanuni-datagrid-pager-size {
656
+ display: inline-flex;
657
+ align-items: center;
658
+ gap: 6px;
659
+ color: var(--dg-text-muted);
660
+ }
661
+
662
+ .kanuni-datagrid-pager-size select {
663
+ padding: 2px 4px;
664
+ border: 1px solid var(--dg-border-strong);
665
+ border-radius: var(--dg-radius-control);
666
+ background: var(--dg-surface);
667
+ color: var(--dg-text);
668
+ font: inherit;
669
+ font-size: var(--dg-control-font-size);
670
+ }
671
+
672
+ .kanuni-datagrid-pager-range {
673
+ color: var(--dg-text-muted);
674
+ font-variant-numeric: tabular-nums;
675
+ }
676
+
677
+ .kanuni-datagrid-pager-buttons {
678
+ display: inline-flex;
679
+ align-items: center;
680
+ gap: 4px;
681
+ margin-left: auto;
682
+ }
683
+
684
+ .kanuni-datagrid-pager-buttons button {
685
+ min-width: 26px;
686
+ height: calc(var(--dg-control-height) - 2px);
687
+ border: 1px solid var(--dg-border-strong);
688
+ border-radius: var(--dg-radius-control);
689
+ background: var(--dg-surface);
690
+ color: inherit;
691
+ font: inherit;
692
+ cursor: pointer;
693
+ }
694
+
695
+ .kanuni-datagrid-pager-buttons button:hover:not(:disabled) {
696
+ background: var(--dg-surface-hover);
697
+ }
698
+
699
+ .kanuni-datagrid-pager-buttons button:disabled {
700
+ opacity: 0.45;
701
+ cursor: default;
702
+ }
703
+
704
+ .kanuni-datagrid-pager-current {
705
+ padding: 0 6px;
706
+ font-variant-numeric: tabular-nums;
707
+ }
708
+
709
+ /* ── Group panel ──────────────────────────────────────────── */
710
+ .kanuni-datagrid-group-panel {
711
+ display: flex;
712
+ align-items: flex-start;
713
+ flex-wrap: wrap;
714
+ gap: 8px;
715
+ flex: none;
716
+ padding: var(--dg-chrome-padding-y) 8px;
717
+ background: var(--dg-surface-header);
718
+ border-bottom: 1px solid var(--dg-border);
719
+ font-size: var(--dg-chrome-font-size);
720
+ }
721
+
722
+ /* ── Cascading levels ──────────────────────────────────────
723
+ One row per level, each indented past the one above and joined to it by an
724
+ elbow. Grouping is a hierarchy; a flat row of chips said nothing about which
725
+ column was the OUTER level, which is the only thing the order encodes. */
726
+ .kanuni-datagrid-group-levels {
727
+ display: flex;
728
+ flex-direction: column;
729
+ gap: 2px;
730
+ min-width: 0;
731
+ }
732
+
733
+ .kanuni-datagrid-group-level {
734
+ position: relative;
735
+ display: flex;
736
+ align-items: center;
737
+ /* --dg-group-level is set per chip from its index. */
738
+ padding-inline-start: calc(var(--dg-group-level, 0) * 18px);
739
+ }
740
+
741
+ /* The elbow: down from the parent chip, then right into this one. */
742
+ .kanuni-datagrid-group-level:not(:first-child)::before {
743
+ content: '';
744
+ position: absolute;
745
+ inset-inline-start: calc((var(--dg-group-level, 1) - 1) * 18px + 9px);
746
+ top: -3px;
747
+ bottom: 50%;
748
+ width: 9px;
749
+ border-inline-start: 1px solid var(--dg-border-strong);
750
+ border-block-end: 1px solid var(--dg-border-strong);
751
+ border-end-start-radius: 4px;
752
+ pointer-events: none;
753
+ }
754
+
755
+ /* Drop indicator while a level is dragged over another. */
756
+ .kanuni-datagrid-group-level--drop-before::after,
757
+ .kanuni-datagrid-group-level--drop-after::after {
758
+ content: '';
759
+ position: absolute;
760
+ inset-inline: calc(var(--dg-group-level, 0) * 18px) 0;
761
+ height: 2px;
762
+ border-radius: 1px;
763
+ background: var(--dg-primary);
764
+ pointer-events: none;
765
+ }
766
+
767
+ .kanuni-datagrid-group-level--drop-before::after {
768
+ top: -2px;
769
+ }
770
+
771
+ .kanuni-datagrid-group-level--drop-after::after {
772
+ bottom: -2px;
773
+ }
774
+
775
+ .kanuni-datagrid-group-panel-actions {
776
+ display: flex;
777
+ align-items: center;
778
+ gap: 8px;
779
+ margin-inline-start: auto;
780
+ }
781
+
782
+ .kanuni-datagrid-group-panel--over {
783
+ background: var(--dg-primary-soft);
784
+ outline: 2px dashed var(--dg-primary);
785
+ outline-offset: -3px;
786
+ }
787
+
788
+ .kanuni-datagrid-group-panel-hint {
789
+ color: var(--dg-text-muted);
790
+ }
791
+
792
+ .kanuni-datagrid-group-chip {
793
+ display: inline-flex;
794
+ align-items: center;
795
+ gap: 6px;
796
+ padding: 2px 6px 2px 10px;
797
+ border-radius: 999px;
798
+ background: var(--dg-primary);
799
+ color: var(--dg-on-primary);
800
+ font-weight: 600;
801
+ white-space: nowrap;
802
+ }
803
+
804
+ .kanuni-datagrid-group-chip[draggable='true'] {
805
+ cursor: grab;
806
+ }
807
+
808
+ .kanuni-datagrid-group-chip:focus-visible {
809
+ outline: 2px solid var(--dg-focus-ring);
810
+ outline-offset: 2px;
811
+ }
812
+
813
+ .kanuni-datagrid-group-chip button {
814
+ border: none;
815
+ background: transparent;
816
+ color: inherit;
817
+ font-size: 14px;
818
+ line-height: 1;
819
+ cursor: pointer;
820
+ }
821
+
822
+ .kanuni-datagrid-group-panel-actions > button {
823
+ padding: 2px 8px;
824
+ border: 1px solid var(--dg-border-strong);
825
+ border-radius: var(--dg-radius-control);
826
+ background: var(--dg-surface);
827
+ color: inherit;
828
+ font: inherit;
829
+ font-size: 12px;
830
+ cursor: pointer;
831
+ }
832
+
833
+ .kanuni-datagrid-group-select select {
834
+ padding: 2px 6px;
835
+ border: 1px solid var(--dg-border-strong);
836
+ border-radius: var(--dg-radius-control);
837
+ background: var(--dg-surface);
838
+ color: inherit;
839
+ font: inherit;
840
+ font-size: 12px;
841
+ }
842
+
843
+ /* ── Group rows ───────────────────────────────────────────── */
844
+ .kanuni-datagrid-group-row {
845
+ background: var(--dg-group-bg);
846
+ cursor: pointer;
847
+ font-weight: 600;
848
+ }
849
+
850
+ .kanuni-datagrid-group-row:hover {
851
+ background: var(--dg-surface-hover);
852
+ }
853
+
854
+ .kanuni-datagrid-group-cell {
855
+ /* Sticky so the label stays readable while scrolling horizontally. */
856
+ position: sticky;
857
+ inset-inline-start: 0;
858
+ display: flex;
859
+ align-items: center;
860
+ gap: 6px;
861
+ height: 100%;
862
+ padding: 0 var(--dg-cell-padding-x);
863
+ white-space: nowrap;
864
+ }
865
+
866
+ .kanuni-datagrid-group-twisty {
867
+ display: inline-block;
868
+ width: 12px;
869
+ color: var(--dg-text-muted);
870
+ }
871
+
872
+ .kanuni-datagrid-group-caption {
873
+ color: var(--dg-text-muted);
874
+ font-weight: 400;
875
+ }
876
+
877
+ .kanuni-datagrid-group-count {
878
+ color: var(--dg-text-muted);
879
+ font-weight: 400;
880
+ }
881
+
882
+ /* Nested levels step back toward the body colour so depth reads at a glance
883
+ without needing to count the indent. */
884
+ .kanuni-datagrid-group-row--level-1 {
885
+ background: color-mix(in srgb, var(--dg-surface-header) 60%, var(--dg-surface));
886
+ }
887
+
888
+ .kanuni-datagrid-group-row--level-2 {
889
+ background: color-mix(in srgb, var(--dg-surface-header) 35%, var(--dg-surface));
890
+ }
891
+
892
+ .kanuni-datagrid-group-row--level-3 {
893
+ background: color-mix(in srgb, var(--dg-surface-header) 18%, var(--dg-surface));
894
+ }
895
+
896
+ /* Aggregates trail the label; they must not push the count off-screen, so the
897
+ whole strip stays on one line and simply clips at the viewport edge. */
898
+ .kanuni-datagrid-group-aggregate {
899
+ margin-left: 12px;
900
+ font-weight: 600;
901
+ font-variant-numeric: tabular-nums;
902
+ }
903
+
904
+ .kanuni-datagrid-group-aggregate-label {
905
+ color: var(--dg-text-muted);
906
+ font-weight: 400;
907
+ }
908
+
909
+ .kanuni-datagrid-group-chip-level {
910
+ display: inline-flex;
911
+ align-items: center;
912
+ justify-content: center;
913
+ min-width: 15px;
914
+ height: 15px;
915
+ border-radius: 999px;
916
+ /* A wash of the chip's own FOREGROUND, not a fixed white: on a gold chip the
917
+ text is near-black, and a white pip on it disappeared. */
918
+ background: color-mix(in srgb, var(--dg-on-primary) 25%, transparent);
919
+ font-size: 10px;
920
+ font-weight: 700;
921
+ }
922
+
923
+ .kanuni-datagrid-group-chip button[disabled] {
924
+ opacity: 0.4;
925
+ cursor: default;
926
+ }
927
+
928
+ .kanuni-datagrid-group-row--focused {
929
+ outline: 2px solid var(--dg-focus-ring);
930
+ outline-offset: -2px;
931
+ }
932
+
933
+ /* ── Master-detail ────────────────────────────────────────── */
934
+ .kanuni-datagrid-detail-toggle-cell {
935
+ padding: 0;
936
+ }
937
+
938
+ .kanuni-datagrid-detail-toggle {
939
+ width: 100%;
940
+ height: 100%;
941
+ border: none;
942
+ background: transparent;
943
+ color: var(--dg-text-muted);
944
+ font-size: 11px;
945
+ line-height: 1;
946
+ cursor: pointer;
947
+ }
948
+
949
+ .kanuni-datagrid-detail-toggle:hover {
950
+ color: var(--dg-text);
951
+ }
952
+
953
+ /* The panel is sized in whole rows by the component (span x rowHeight); this
954
+ rule only styles it. Overriding the height here would desync it from the
955
+ filler slots the virtualizer reserved. */
956
+ .kanuni-datagrid-detail-row {
957
+ background: var(--dg-surface);
958
+ border-bottom: 1px solid var(--dg-border);
959
+ }
960
+
961
+ .kanuni-datagrid-detail-cell {
962
+ position: sticky;
963
+ inset-inline-start: 0;
964
+ box-sizing: border-box;
965
+ width: 100%;
966
+ height: 100%;
967
+ padding: 8px 12px;
968
+ overflow: auto;
969
+ border-inline-start: 3px solid var(--dg-primary);
970
+ }
971
+
972
+ /* ── In-grid edit form (mode: 'form') ─────────────────────── */
973
+ /* Sticky to the scroller's inline start so the form stays put while the
974
+ columns scroll under it — a form that slides off to the left with column 40
975
+ is a form the user has to chase. Width is the VIEWPORT's, not the canvas's,
976
+ for the same reason. */
977
+ .kanuni-datagrid-editform-row {
978
+ background: var(--dg-surface);
979
+ border-bottom: 1px solid var(--dg-border);
980
+ }
981
+
982
+ .kanuni-datagrid-editform-cell {
983
+ position: sticky;
984
+ inset-inline-start: 0;
985
+ box-sizing: border-box;
986
+ /* The VIEWPORT's width, not the row's — see `--dg-viewport-width`. Falls
987
+ back to the row when nothing has measured yet (SSR, first paint). */
988
+ width: var(--dg-viewport-width, 100%);
989
+ height: 100%;
990
+ padding: 0;
991
+ border-inline-start: 3px solid var(--dg-primary);
992
+ }
993
+
994
+ /* ── Summary bar ──────────────────────────────────────────── */
995
+ .kanuni-datagrid-summary-bar {
996
+ display: flex;
997
+ align-items: center;
998
+ flex-wrap: wrap;
999
+ gap: 16px;
1000
+ flex: none;
1001
+ padding: 5px 8px;
1002
+ background: var(--dg-footer-bg);
1003
+ border-top: var(--dg-header-border);
1004
+ font-size: var(--dg-chrome-font-size);
1005
+ }
1006
+
1007
+ .kanuni-datagrid-summary-item {
1008
+ display: inline-flex;
1009
+ align-items: baseline;
1010
+ gap: 4px;
1011
+ }
1012
+
1013
+ .kanuni-datagrid-summary-label {
1014
+ color: var(--dg-text-muted);
1015
+ }
1016
+
1017
+ /* ── Selection ────────────────────────────────────────────── */
1018
+ .kanuni-datagrid-row--selected,
1019
+ .kanuni-datagrid-row--selected .kanuni-datagrid-cell--pinned {
1020
+ background: var(--dg-selected-bg);
1021
+ }
1022
+
1023
+ .kanuni-datagrid-row--selected:hover,
1024
+ .kanuni-datagrid-row--selected:hover .kanuni-datagrid-cell--pinned {
1025
+ background: var(--dg-selected-bg);
1026
+ filter: brightness(0.97);
1027
+ }
1028
+
1029
+ .kanuni-datagrid-select-cell {
1030
+ justify-content: center;
1031
+ padding: 0;
1032
+ }
1033
+
1034
+ .kanuni-datagrid-select-cell input {
1035
+ accent-color: var(--dg-primary);
1036
+ cursor: pointer;
1037
+ }
1038
+
1039
+ /* ── Row numbers (gap analysis P3.5) ───────────────────────── */
1040
+ /* Muted and tabular on purpose. The number is an address, not data: at full
1041
+ text weight it competes with the first real column, and proportional digits
1042
+ make a column of numbers ragged down its right edge. */
1043
+ .kanuni-datagrid-rownum-cell {
1044
+ color: var(--dg-text-muted);
1045
+ font-variant-numeric: tabular-nums;
1046
+ background: var(--dg-header-bg);
1047
+ user-select: none;
1048
+ }
1049
+
1050
+ .kanuni-datagrid-row--selected .kanuni-datagrid-rownum-cell {
1051
+ background: var(--dg-selected-bg);
1052
+ }
1053
+
1054
+ /* ── Keyboard focus ───────────────────────────────────────── */
1055
+ /* Focus is virtual (aria-activedescendant), so it must be drawn explicitly —
1056
+ there is no browser focus ring on the cell itself. `inset` keeps the outline
1057
+ from being clipped by the neighbouring cell. */
1058
+ .kanuni-datagrid-cell--focused {
1059
+ outline: 2px solid var(--dg-focus-ring);
1060
+ outline-offset: -2px;
1061
+ }
1062
+
1063
+ .kanuni-datagrid-scroller:focus {
1064
+ outline: none;
1065
+ }
1066
+
1067
+ .kanuni-datagrid-scroller:focus-visible {
1068
+ outline: 2px solid var(--dg-focus-ring);
1069
+ outline-offset: -2px;
1070
+ }
1071
+
1072
+ /* Visually hidden but still read aloud. */
1073
+ .kanuni-datagrid-sr-only {
1074
+ position: absolute;
1075
+ width: 1px;
1076
+ height: 1px;
1077
+ margin: -1px;
1078
+ padding: 0;
1079
+ overflow: hidden;
1080
+ clip: rect(0, 0, 0, 0);
1081
+ white-space: nowrap;
1082
+ border: 0;
1083
+ }
1084
+
1085
+ /* ── Header filter button ──────────────────────────────────
1086
+ Hidden until the header is hovered or focused (`filterIconMode: 'hover'`,
1087
+ the default). `visibility` rather than `display` so the icon does not push
1088
+ the label around when it appears — the space is reserved either way. */
1089
+ .kanuni-datagrid-filter-button {
1090
+ display: inline-flex;
1091
+ align-items: center;
1092
+ justify-content: center;
1093
+ flex: none;
1094
+ margin-inline-start: auto;
1095
+ padding: 1px 3px;
1096
+ border: none;
1097
+ border-radius: 3px;
1098
+ background: transparent;
1099
+ /* Lives INSIDE the header cell, so it follows the header's foreground: on an
1100
+ ink header the body's muted slate is barely visible. */
1101
+ color: var(--dg-header-text-muted);
1102
+ line-height: 0;
1103
+ cursor: pointer;
1104
+ visibility: hidden;
1105
+ }
1106
+
1107
+ .kanuni-datagrid-header-cell:hover > .kanuni-datagrid-filter-button,
1108
+ .kanuni-datagrid-header-cell:focus-within > .kanuni-datagrid-filter-button,
1109
+ .kanuni-datagrid-filter-button--pinned,
1110
+ /* An ACTIVE filter is the only sign that rows are being withheld, so it stays
1111
+ visible in both modes — hiding it would make the grid look simply empty. */
1112
+ .kanuni-datagrid-filter-button--active {
1113
+ visibility: visible;
1114
+ }
1115
+
1116
+ .kanuni-datagrid-filter-button:hover {
1117
+ background: var(--dg-primary-soft);
1118
+ color: var(--dg-text);
1119
+ /* `--dg-primary-soft` is a LIGHT tint in every light theme, so the label on
1120
+ it is the body text, not the header's — this is the one spot in the header
1121
+ that deliberately does not follow `--dg-header-text`. */
1122
+ }
1123
+
1124
+ .kanuni-datagrid-filter-button--active {
1125
+ color: var(--dg-primary);
1126
+ background: var(--dg-primary-soft);
1127
+ }
1128
+
1129
+ /* ── Column reorder (drag & drop) ──────────────────────────
1130
+ The indicator is an inset shadow, not a border: a border would change the
1131
+ cell's box and shift every column to its right by a pixel mid-drag. */
1132
+ .kanuni-datagrid-header-cell--drop-before {
1133
+ box-shadow: inset 3px 0 0 -1px var(--dg-primary);
1134
+ }
1135
+
1136
+ .kanuni-datagrid-header-cell--drop-after {
1137
+ box-shadow: inset -3px 0 0 -1px var(--dg-primary);
1138
+ }
1139
+
1140
+ [dir='rtl'] .kanuni-datagrid-header-cell--drop-before {
1141
+ box-shadow: inset -3px 0 0 -1px var(--dg-primary);
1142
+ }
1143
+
1144
+ [dir='rtl'] .kanuni-datagrid-header-cell--drop-after {
1145
+ box-shadow: inset 3px 0 0 -1px var(--dg-primary);
1146
+ }
1147
+
1148
+ /* ── Column resize handle ─────────────────────────────────── */
1149
+ .kanuni-datagrid-resize-handle {
1150
+ position: absolute;
1151
+ inset-block: 0;
1152
+ inset-inline-end: -3px;
1153
+ width: 7px;
1154
+ z-index: 5;
1155
+ cursor: col-resize;
1156
+ /* Invisible by default; the line under the cursor is what marks the seam. */
1157
+ background: transparent;
1158
+ touch-action: none;
1159
+ }
1160
+
1161
+ .kanuni-datagrid-resize-handle:hover::after,
1162
+ .kanuni-datagrid-header-cell--resizing .kanuni-datagrid-resize-handle::after {
1163
+ content: '';
1164
+ position: absolute;
1165
+ inset-block: 4px;
1166
+ inset-inline-start: 3px;
1167
+ width: 2px;
1168
+ border-radius: 1px;
1169
+ background: var(--dg-primary);
1170
+ }
1171
+
1172
+ /* ── Loading overlay (busy + percentage) ──────────────────── */
1173
+ .kanuni-datagrid-loading-overlay {
1174
+ position: absolute;
1175
+ inset: 0;
1176
+ z-index: 20;
1177
+ display: flex;
1178
+ align-items: center;
1179
+ justify-content: center;
1180
+ background: color-mix(in srgb, var(--dg-bg) 62%, transparent);
1181
+ /* Pointer-transparent: a recompute triggered by typing must not swallow the
1182
+ next keystroke aimed at the filter box. */
1183
+ pointer-events: none;
1184
+ }
1185
+
1186
+ /* Exporting is the one phase that takes the grid away, so it looks like it.
1187
+ The sheet is nearly opaque instead of a hint, it swallows clicks instead of
1188
+ letting them fall through to a grid that cannot answer them, and the cursor
1189
+ says wait. See the comment on `data-blocking` in GridLoadingOverlay.tsx. */
1190
+ .kanuni-datagrid-loading-overlay[data-blocking='true'] {
1191
+ background: color-mix(in srgb, var(--dg-bg) 92%, transparent);
1192
+ pointer-events: auto;
1193
+ cursor: progress;
1194
+ }
1195
+
1196
+ .kanuni-datagrid-loading-card {
1197
+ display: flex;
1198
+ flex-direction: column;
1199
+ align-items: center;
1200
+ gap: 10px;
1201
+ padding: 18px 24px;
1202
+ border: 1px solid var(--dg-border);
1203
+ border-radius: calc(var(--dg-radius) + 4px);
1204
+ background: var(--dg-surface);
1205
+ box-shadow: var(--dg-shadow-card);
1206
+ color: var(--dg-text);
1207
+ font-size: var(--dg-chrome-font-size);
1208
+ }
1209
+
1210
+ .kanuni-datagrid-loading-spinner {
1211
+ width: 26px;
1212
+ height: 26px;
1213
+ border: 2px solid transparent;
1214
+ border-bottom-color: var(--dg-primary);
1215
+ border-radius: 50%;
1216
+ animation: kanuni-datagrid-spin 1s linear infinite;
1217
+ }
1218
+
1219
+ .kanuni-datagrid-loading-label {
1220
+ font-weight: 500;
1221
+ color: var(--dg-text-muted);
1222
+ }
1223
+
1224
+ .kanuni-datagrid-loading-progress {
1225
+ display: flex;
1226
+ flex-direction: column;
1227
+ align-items: center;
1228
+ gap: 6px;
1229
+ width: 220px;
1230
+ }
1231
+
1232
+ .kanuni-datagrid-loading-track {
1233
+ width: 100%;
1234
+ height: 6px;
1235
+ border-radius: 999px;
1236
+ overflow: hidden;
1237
+ background: color-mix(in srgb, var(--dg-border) 70%, transparent);
1238
+ }
1239
+
1240
+ .kanuni-datagrid-loading-fill {
1241
+ height: 100%;
1242
+ border-radius: 999px;
1243
+ background: var(--dg-primary);
1244
+ transition: width 160ms ease-out;
1245
+ }
1246
+
1247
+ .kanuni-datagrid-loading-percent {
1248
+ font-size: 11px;
1249
+ font-weight: 600;
1250
+ font-variant-numeric: tabular-nums;
1251
+ }
1252
+
1253
+ /* ── Filter panel (cxGrid) ────────────────────────────────────
1254
+ Under the footer, outside the scroller: the conditions describe the whole
1255
+ result, so they must not scroll away with the rows. */
1256
+ .kanuni-datagrid-filter-panel {
1257
+ display: flex;
1258
+ align-items: center;
1259
+ gap: 6px;
1260
+ flex: none;
1261
+ padding: 4px 8px;
1262
+ background: var(--dg-surface-header);
1263
+ border-top: 1px solid var(--dg-border);
1264
+ font-size: var(--dg-chrome-font-size);
1265
+ color: var(--dg-text);
1266
+ }
1267
+
1268
+ /* Suspended: the conditions are still there, just not being applied. Dimmed
1269
+ rather than hidden — there has to be something to switch back on. */
1270
+ .kanuni-datagrid-filter-panel--suspended .kanuni-datagrid-filter-panel-text {
1271
+ opacity: 0.5;
1272
+ text-decoration: line-through;
1273
+ }
1274
+
1275
+ .kanuni-datagrid-filter-panel-clear,
1276
+ .kanuni-datagrid-filter-panel-builder {
1277
+ display: inline-flex;
1278
+ align-items: center;
1279
+ justify-content: center;
1280
+ flex: none;
1281
+ width: 18px;
1282
+ height: 18px;
1283
+ padding: 0;
1284
+ border: 1px solid transparent;
1285
+ border-radius: var(--dg-radius-control);
1286
+ background: transparent;
1287
+ color: var(--dg-text-muted);
1288
+ font: inherit;
1289
+ line-height: 1;
1290
+ cursor: pointer;
1291
+ }
1292
+
1293
+ .kanuni-datagrid-filter-panel-clear:hover:not(:disabled),
1294
+ .kanuni-datagrid-filter-panel-builder:hover:not(:disabled) {
1295
+ background: var(--dg-surface-hover);
1296
+ border-color: var(--dg-border-strong);
1297
+ color: var(--dg-text);
1298
+ }
1299
+
1300
+ .kanuni-datagrid-filter-panel-clear:disabled,
1301
+ .kanuni-datagrid-filter-panel-builder:disabled {
1302
+ opacity: 0.4;
1303
+ cursor: default;
1304
+ }
1305
+
1306
+ .kanuni-datagrid-filter-panel-toggle {
1307
+ flex: none;
1308
+ margin: 0;
1309
+ accent-color: var(--dg-primary);
1310
+ cursor: pointer;
1311
+ }
1312
+
1313
+ .kanuni-datagrid-filter-panel-text {
1314
+ flex: 1 1 auto;
1315
+ min-width: 0;
1316
+ overflow: hidden;
1317
+ text-overflow: ellipsis;
1318
+ white-space: nowrap;
1319
+ }
1320
+
1321
+ .kanuni-datagrid-filter-panel-builder {
1322
+ margin-inline-start: auto;
1323
+ }
1324
+
1325
+ /* Shown only for phases that can actually stop — see CANCELLABLE in the overlay. */
1326
+ .kanuni-datagrid-loading-cancel {
1327
+ margin-top: 8px;
1328
+ padding: 3px 12px;
1329
+ border: 1px solid var(--dg-border);
1330
+ border-radius: var(--dg-radius);
1331
+ background: var(--dg-surface);
1332
+ color: inherit;
1333
+ font: inherit;
1334
+ font-size: 11px;
1335
+ cursor: pointer;
1336
+ }
1337
+
1338
+ .kanuni-datagrid-loading-cancel:hover {
1339
+ background: var(--dg-surface-hover);
1340
+ }
1341
+
1342
+ @media (prefers-reduced-motion: reduce) {
1343
+ .kanuni-datagrid-loading-spinner {
1344
+ animation-duration: 3s;
1345
+ }
1346
+
1347
+ .kanuni-datagrid-loading-fill {
1348
+ transition: none;
1349
+ }
1350
+ }
1351
+
1352
+ /* ── Filter row (under the header, same sticky geometry) ──── */
1353
+ .kanuni-datagrid-filter-row {
1354
+ position: sticky;
1355
+ /* Clears the BANDS as well as the header. `--dg-band-offset` is published by
1356
+ the component (0 when there are no bands); the header height stays a
1357
+ variable here so density keeps working. Sticking at the header height alone
1358
+ parked this row inside the header on any banded grid, and the two drew over
1359
+ each other as soon as the user scrolled. */
1360
+ top: calc(var(--dg-band-offset, 0px) + var(--dg-header-height));
1361
+ z-index: 3;
1362
+ display: flex;
1363
+ height: var(--dg-header-height);
1364
+ background: var(--dg-surface-header);
1365
+ border-bottom: var(--dg-header-border);
1366
+ }
1367
+
1368
+ .kanuni-datagrid-filter-cell {
1369
+ display: flex;
1370
+ align-items: center;
1371
+ flex: none;
1372
+ height: 100%;
1373
+ padding: 0 4px;
1374
+ background: var(--dg-surface-header);
1375
+ border-inline-end: var(--dg-header-cell-border);
1376
+ overflow: hidden;
1377
+ }
1378
+
1379
+ .kanuni-datagrid-filter-cell--pinned {
1380
+ position: sticky;
1381
+ z-index: 4;
1382
+ background: var(--dg-surface-header);
1383
+ }
1384
+
1385
+ .kanuni-datagrid-filter-input {
1386
+ width: 100%;
1387
+ min-width: 0;
1388
+ height: var(--dg-filter-input-height);
1389
+ padding: 0 6px;
1390
+ border: 1px solid var(--dg-border-strong);
1391
+ border-radius: var(--dg-radius-control);
1392
+ background: var(--dg-surface);
1393
+ color: var(--dg-text);
1394
+ font: inherit;
1395
+ font-size: var(--dg-control-font-size);
1396
+ }
1397
+
1398
+ .kanuni-datagrid-filter-input:focus {
1399
+ outline: none;
1400
+ border-color: var(--dg-primary);
1401
+ }
1402
+
1403
+ /* ── Header filter popup (portaled — see GridPortal) ──────── */
1404
+ .kanuni-datagrid-portal {
1405
+ /* The portal host must not paint or capture anything itself. */
1406
+ position: static;
1407
+ border: none;
1408
+ background: transparent;
1409
+ overflow: visible;
1410
+ }
1411
+
1412
+ /* ── Band fold toggle (P2.6) ──────────────────────────────── */
1413
+ .kanuni-datagrid-band-toggle {
1414
+ display: inline-flex;
1415
+ align-items: center;
1416
+ justify-content: center;
1417
+ flex: none;
1418
+ width: 16px;
1419
+ height: 16px;
1420
+ margin-inline-end: 4px;
1421
+ padding: 0;
1422
+ border: none;
1423
+ border-radius: 3px;
1424
+ background: transparent;
1425
+ color: var(--dg-text-muted);
1426
+ font-size: 10px;
1427
+ line-height: 1;
1428
+ cursor: pointer;
1429
+ }
1430
+
1431
+ .kanuni-datagrid-band-toggle:hover {
1432
+ background: var(--dg-primary-soft);
1433
+ color: var(--dg-text);
1434
+ }
1435
+
1436
+ .kanuni-datagrid-band-toggle:focus-visible {
1437
+ outline: 2px solid var(--dg-focus-ring);
1438
+ outline-offset: -1px;
1439
+ }
1440
+
1441
+ /* ── Search highlight (P2.4) ──────────────────────────────── */
1442
+ /* A <mark>, so the match is in the accessibility tree as emphasis rather than
1443
+ as a colour only. The browser default is a hard yellow that fights every
1444
+ theme; these follow the accent instead. */
1445
+ .kanuni-datagrid-highlight {
1446
+ padding: 0;
1447
+ border-radius: 2px;
1448
+ background: var(--dg-highlight-bg);
1449
+ color: var(--dg-highlight-text);
1450
+ font-weight: 600;
1451
+ }
1452
+
1453
+ /* ── Cell / header tooltip (P2.1) ─────────────────────────── */
1454
+ .kanuni-datagrid-tooltip {
1455
+ position: fixed;
1456
+ /* Above every popup: a tooltip describes whatever is under the pointer, and
1457
+ the pointer can be over a popup. */
1458
+ z-index: 1100;
1459
+ /* The tooltip must never be the hover target — it appears UNDER the pointer's
1460
+ path, and taking the pointer would fire mouseout on the cell that asked for
1461
+ it, hiding the tooltip, which un-hovers it, which shows it again. */
1462
+ pointer-events: none;
1463
+ max-width: 420px;
1464
+ padding: 4px 8px;
1465
+ border: 1px solid var(--dg-border-strong);
1466
+ border-radius: var(--dg-radius);
1467
+ background: var(--dg-surface);
1468
+ color: var(--dg-text);
1469
+ box-shadow: var(--dg-shadow-popup);
1470
+ font-size: var(--dg-font-size);
1471
+ line-height: 1.45;
1472
+ /* Long values wrap instead of running off-screen; a pasted paragraph in a
1473
+ cell is exactly the case the tooltip exists for. */
1474
+ white-space: pre-wrap;
1475
+ overflow-wrap: anywhere;
1476
+ }
1477
+
1478
+ .kanuni-datagrid-filter-popup {
1479
+ position: fixed;
1480
+ z-index: 1000;
1481
+ display: flex;
1482
+ flex-direction: column;
1483
+ gap: 6px;
1484
+ width: 260px;
1485
+ max-height: 340px;
1486
+ padding: 8px;
1487
+ border: 1px solid var(--dg-border-strong);
1488
+ border-radius: var(--dg-radius);
1489
+ /* Explicit background: a transparent popup was the PivotGrid drill-down bug. */
1490
+ background: var(--dg-surface);
1491
+ color: var(--dg-text);
1492
+ box-shadow: var(--dg-shadow-popup);
1493
+ font-size: var(--dg-font-size);
1494
+ }
1495
+
1496
+ .kanuni-datagrid-filter-search {
1497
+ width: 100%;
1498
+ height: var(--dg-control-height);
1499
+ padding: 0 8px;
1500
+ border: 1px solid var(--dg-border-strong);
1501
+ border-radius: var(--dg-radius-control);
1502
+ background: var(--dg-surface);
1503
+ color: var(--dg-text);
1504
+ font: inherit;
1505
+ font-size: var(--dg-control-font-size);
1506
+ }
1507
+
1508
+ /* "There are more values than these." Sits directly under the search box
1509
+ because the search box is the answer to it — on a capped list, search scans
1510
+ the whole dataset rather than the values already on screen. */
1511
+ .kanuni-datagrid-filter-truncated {
1512
+ margin: 6px 0 0;
1513
+ font-size: 11px;
1514
+ line-height: 1.4;
1515
+ color: var(--dg-text-muted);
1516
+ }
1517
+
1518
+ .kanuni-datagrid-filter-actions {
1519
+ display: flex;
1520
+ gap: 6px;
1521
+ }
1522
+
1523
+ .kanuni-datagrid-filter-actions button,
1524
+ .kanuni-datagrid-filter-footer button {
1525
+ padding: 3px 8px;
1526
+ border: 1px solid var(--dg-border-strong);
1527
+ border-radius: var(--dg-radius-control);
1528
+ background: var(--dg-surface);
1529
+ color: var(--dg-text);
1530
+ font: inherit;
1531
+ font-size: var(--dg-control-font-size);
1532
+ cursor: pointer;
1533
+ }
1534
+
1535
+ .kanuni-datagrid-filter-actions button:hover,
1536
+ .kanuni-datagrid-filter-footer button:hover {
1537
+ background: var(--dg-surface-hover);
1538
+ }
1539
+
1540
+ .kanuni-datagrid-filter-primary {
1541
+ background: var(--dg-primary) !important;
1542
+ border-color: var(--dg-primary) !important;
1543
+ color: var(--dg-on-primary) !important;
1544
+ }
1545
+
1546
+ .kanuni-datagrid-filter-list {
1547
+ flex: 1 1 auto;
1548
+ overflow-y: auto;
1549
+ /* Windowed list — scroll anchoring off for the same reason as the main
1550
+ scroller: rows repositioning during scroll read as content shifts and the
1551
+ browser fights the gesture. See `.kanuni-datagrid-scroller`. */
1552
+ overflow-anchor: none;
1553
+ /* Stop the wheel from CHAINING into the grid once the list hits its end.
1554
+ Without this, wheeling to the bottom of the list keeps going and scrolls
1555
+ the rows underneath — which moves the anchor and closes the popup, in the
1556
+ middle of a gesture the user meant for the list. */
1557
+ overscroll-behavior: contain;
1558
+ border: 1px solid var(--dg-border);
1559
+ border-radius: var(--dg-radius-control);
1560
+ }
1561
+
1562
+ .kanuni-datagrid-filter-item {
1563
+ display: flex;
1564
+ align-items: center;
1565
+ gap: 6px;
1566
+ padding: 3px 6px;
1567
+ cursor: pointer;
1568
+ white-space: nowrap;
1569
+ overflow: hidden;
1570
+ text-overflow: ellipsis;
1571
+ /* FIXED, and the virtualizer depends on it: the list renders only the rows in
1572
+ view and positions them by `index x FILTER_ITEM_HEIGHT`. A label that
1573
+ wrapped to two lines would put every row below it out of place. Single-line
1574
+ is already enforced by `white-space: nowrap` above; this makes the height
1575
+ the popup computes with a fact rather than an assumption.
1576
+ Keep in sync with FILTER_ITEM_HEIGHT in GridHeaderFilterPopup.tsx. */
1577
+ height: 25px;
1578
+ box-sizing: border-box;
1579
+ flex: none;
1580
+ }
1581
+
1582
+ .kanuni-datagrid-filter-item:hover,
1583
+ .kanuni-datagrid-filter-item--active {
1584
+ background: var(--dg-surface-hover);
1585
+ }
1586
+
1587
+ .kanuni-datagrid-filter-item input {
1588
+ flex: none;
1589
+ accent-color: var(--dg-primary);
1590
+ }
1591
+
1592
+ .kanuni-datagrid-filter-empty {
1593
+ padding: 10px;
1594
+ color: var(--dg-text-muted);
1595
+ text-align: center;
1596
+ }
1597
+
1598
+ .kanuni-datagrid-filter-footer {
1599
+ display: flex;
1600
+ justify-content: flex-end;
1601
+ gap: 6px;
1602
+ }
1603
+
1604
+ /* ── Empty state ──────────────────────────────────────────── */
1605
+ .kanuni-datagrid-empty {
1606
+ display: flex;
1607
+ align-items: center;
1608
+ justify-content: center;
1609
+ padding: 32px 16px;
1610
+ color: var(--dg-text-muted);
1611
+ font-size: var(--dg-font-size);
1612
+ }
1613
+
1614
+ /* ── Accessibility ────────────────────────────────────────── */
1615
+ .kanuni-datagrid-root :focus-visible {
1616
+ outline: 2px solid var(--dg-focus-ring);
1617
+ outline-offset: -2px;
1618
+ }
1619
+
1620
+ @media (prefers-reduced-motion: reduce) {
1621
+ .kanuni-datagrid-viewport {
1622
+ will-change: auto;
1623
+ }
1624
+ }
1625
+
1626
+ /* ── Remote loading rows (P10) ────────────────────────────── */
1627
+ /* A row that exists on the server but has not arrived. It keeps its slot so
1628
+ the scrollbar and every row index stay exact while the block is in flight. */
1629
+ .kanuni-datagrid-row--loading {
1630
+ align-items: center;
1631
+ }
1632
+
1633
+ .kanuni-datagrid-loading-cell {
1634
+ position: sticky;
1635
+ inset-inline-start: 0;
1636
+ flex: 1;
1637
+ border-inline-end: none;
1638
+ }
1639
+
1640
+ .kanuni-datagrid-loading-bar {
1641
+ display: block;
1642
+ width: 40%;
1643
+ max-width: 240px;
1644
+ height: 8px;
1645
+ border-radius: 4px;
1646
+ background: var(--dg-surface-hover);
1647
+ animation: kanuni-datagrid-pulse 1.2s ease-in-out infinite;
1648
+ }
1649
+
1650
+ @keyframes kanuni-datagrid-pulse {
1651
+ 0%,
1652
+ 100% {
1653
+ opacity: 0.45;
1654
+ }
1655
+ 50% {
1656
+ opacity: 1;
1657
+ }
1658
+ }
1659
+
1660
+ /* Respect a reduced-motion preference: the pulse is decoration, and a user who
1661
+ asked for stillness should get a static placeholder. */
1662
+ @media (prefers-reduced-motion: reduce) {
1663
+ .kanuni-datagrid-loading-bar {
1664
+ animation: none;
1665
+ }
1666
+ }
1667
+
1668
+
1669
+ /* ── RTL ──────────────────────────────────────────────────────
1670
+ Insets are logical (inset-inline-*), so sticky columns flip on their own.
1671
+ What does NOT flip automatically is a directional SHADOW: the edge shadow
1672
+ that separates a left-pinned block from the scrolling body has to fall on
1673
+ the other side when the writing direction reverses. */
1674
+ .kanuni-datagrid-root[dir='rtl'] .kanuni-datagrid-pin-edge-left,
1675
+ [dir='rtl'] .kanuni-datagrid-root .kanuni-datagrid-pin-edge-left {
1676
+ box-shadow: var(--dg-shadow-pinned-right);
1677
+ }
1678
+
1679
+ .kanuni-datagrid-root[dir='rtl'] .kanuni-datagrid-pin-edge-right,
1680
+ [dir='rtl'] .kanuni-datagrid-root .kanuni-datagrid-pin-edge-right {
1681
+ box-shadow: var(--dg-shadow-pinned);
1682
+ }
1683
+
1684
+ /* The sort caret and the resize handle are positioned, not flowed. */
1685
+ .kanuni-datagrid-root[dir='rtl'] .kanuni-datagrid-resize-handle,
1686
+ [dir='rtl'] .kanuni-datagrid-root .kanuni-datagrid-resize-handle {
1687
+ right: auto;
1688
+ left: 0;
1689
+ }
1690
+
1691
+ /* ── Density presets ──────────────────────────────────────────
1692
+ The ROW HEIGHT is not set here: it is a core value (the virtualizer runs on
1693
+ it), so the component resolves the preset into `rowHeight` and core feeds it
1694
+ back as --dg-row-height. What lives here is everything CSS owns — header
1695
+ height, padding and type scale — so the two can never disagree about the
1696
+ row geometry. */
1697
+ .kanuni-datagrid-root.kanuni-datagrid-density-compact {
1698
+ --dg-header-height: 28px;
1699
+ --dg-cell-padding-x: 6px;
1700
+ --dg-font-size: 12px;
1701
+ --dg-header-font-size: 11px;
1702
+ }
1703
+
1704
+ .kanuni-datagrid-root.kanuni-datagrid-density-comfortable {
1705
+ --dg-header-height: 40px;
1706
+ --dg-cell-padding-x: 12px;
1707
+ --dg-font-size: 14px;
1708
+ --dg-header-font-size: 13px;
1709
+ }
1710
+
1711
+ /* ── Row context menu ─────────────────────────────────────── */
1712
+ .kanuni-datagrid-context-menu {
1713
+ position: fixed;
1714
+ z-index: 1200;
1715
+ min-width: 180px;
1716
+ max-width: 320px;
1717
+ /*
1718
+ * A menu longer than the screen scrolls instead of running off it: the
1719
+ * clamp in `GridContextMenu` can only move a menu that FITS, and an app row
1720
+ * menu with a dozen entries plus the built-ins does not.
1721
+ *
1722
+ * This is safe only because the SUBMENU is portaled as a sibling. While it
1723
+ * was a child, any overflow value other than `visible` clipped it away
1724
+ * entirely — which is why this rule was removed once before.
1725
+ */
1726
+ max-height: calc(100vh - 16px);
1727
+ overflow-y: auto;
1728
+ padding: 4px;
1729
+ border: 1px solid var(--dg-border-strong);
1730
+ border-radius: var(--dg-radius);
1731
+ background: var(--dg-surface);
1732
+ color: var(--dg-text);
1733
+ box-shadow: var(--dg-shadow-menu);
1734
+ font-size: var(--dg-font-size);
1735
+ }
1736
+
1737
+ .kanuni-datagrid-context-menu:focus {
1738
+ outline: none;
1739
+ }
1740
+
1741
+ .kanuni-datagrid-menu-row {
1742
+ position: relative;
1743
+ }
1744
+
1745
+ .kanuni-datagrid-menu-item {
1746
+ display: flex;
1747
+ align-items: center;
1748
+ gap: 8px;
1749
+ width: 100%;
1750
+ padding: 5px 8px;
1751
+ border: none;
1752
+ border-radius: var(--dg-radius-control);
1753
+ background: transparent;
1754
+ color: inherit;
1755
+ font: inherit;
1756
+ text-align: start;
1757
+ cursor: pointer;
1758
+ }
1759
+
1760
+ /* Hover and keyboard highlight share one class so the two navigation modes
1761
+ look identical — a menu where the arrow keys highlight differently from the
1762
+ mouse reads as two different menus. */
1763
+ .kanuni-datagrid-menu-item:hover:not(:disabled),
1764
+ .kanuni-datagrid-menu-item--active:not(:disabled) {
1765
+ background: var(--dg-surface-hover);
1766
+ }
1767
+
1768
+ .kanuni-datagrid-menu-item:disabled {
1769
+ opacity: 0.45;
1770
+ cursor: default;
1771
+ }
1772
+
1773
+ .kanuni-datagrid-menu-icon {
1774
+ display: inline-flex;
1775
+ align-items: center;
1776
+ flex: none;
1777
+ width: 16px;
1778
+ }
1779
+
1780
+ .kanuni-datagrid-menu-label {
1781
+ flex: 1 1 auto;
1782
+ overflow: hidden;
1783
+ text-overflow: ellipsis;
1784
+ white-space: nowrap;
1785
+ }
1786
+
1787
+ .kanuni-datagrid-menu-shortcut {
1788
+ flex: none;
1789
+ color: var(--dg-text-muted);
1790
+ font-size: 11px;
1791
+ }
1792
+
1793
+ .kanuni-datagrid-menu-caret {
1794
+ flex: none;
1795
+ color: var(--dg-text-muted);
1796
+ }
1797
+
1798
+ .kanuni-datagrid-menu-separator {
1799
+ height: 1px;
1800
+ margin: 4px 6px;
1801
+ background: var(--dg-border);
1802
+ }
1803
+
1804
+ /* Submenus open toward the inline end, so they flip in RTL for free. */
1805
+ /*
1806
+ * Positioned in VIEWPORT coordinates by `GridContextMenu`, because it is a
1807
+ * sibling of the menu rather than a child — see the comment there. It inherits
1808
+ * the menu's box from the shared class; only the positioning differs.
1809
+ */
1810
+ .kanuni-datagrid-submenu {
1811
+ position: fixed;
1812
+ }
1813
+
1814
+ /* ── Toolbar icon button + export menu ────────────────────── */
1815
+ .kanuni-datagrid-toolbar-icon-button {
1816
+ display: inline-flex;
1817
+ align-items: center;
1818
+ justify-content: center;
1819
+ width: 28px;
1820
+ height: 24px;
1821
+ padding: 0;
1822
+ }
1823
+
1824
+ /* Spinner shown in place of the icon while an export runs, so the button does
1825
+ not change size and the toolbar does not reflow mid-export. */
1826
+ .kanuni-datagrid-toolbar-busy {
1827
+ width: 12px;
1828
+ height: 12px;
1829
+ border: 2px solid var(--dg-border-strong);
1830
+ border-top-color: var(--dg-primary);
1831
+ border-radius: 50%;
1832
+ animation: kanuni-datagrid-spin 0.7s linear infinite;
1833
+ }
1834
+
1835
+ @keyframes kanuni-datagrid-spin {
1836
+ to {
1837
+ transform: rotate(360deg);
1838
+ }
1839
+ }
1840
+
1841
+ @media (prefers-reduced-motion: reduce) {
1842
+ .kanuni-datagrid-toolbar-busy {
1843
+ animation: none;
1844
+ }
1845
+ }
1846
+
1847
+ /* The export menu reuses the popup shell but is a plain list of actions. */
1848
+ .kanuni-datagrid-export-menu {
1849
+ width: 190px;
1850
+ gap: 2px;
1851
+ padding: 4px;
1852
+ }
1853
+
1854
+ .kanuni-datagrid-chooser-empty {
1855
+ padding: 8px 4px;
1856
+ color: var(--dg-text-muted);
1857
+ font-style: italic;
1858
+ }
1859
+
1860
+ /* ── Column-aligned footer row (punch #18) ─────────────────
1861
+ Sticky to the BOTTOM of the same scroller the header sticks to the top of, so
1862
+ it scrolls horizontally with the columns and stays put vertically. */
1863
+ /* ── Pinned top/bottom rows (P2.3) ────────────────────────── */
1864
+ /* Sticky in the same stack as the header and the footer. `--dg-pinned-gap`
1865
+ counts the header-height rows between this block and its edge (header +
1866
+ filter row above, footer below), so the offset follows the THEME's header
1867
+ height instead of a number measured in JS at the previous theme's size. */
1868
+ .kanuni-datagrid-pinned-rows {
1869
+ position: sticky;
1870
+ /* Above the data rows, below the header — a pinned row must not cover the
1871
+ header when the grid is scrolled to the very top. */
1872
+ z-index: 2;
1873
+ display: flex;
1874
+ flex-direction: column;
1875
+ background: var(--dg-surface-header);
1876
+ }
1877
+
1878
+ .kanuni-datagrid-pinned-rows--top {
1879
+ top: calc(var(--dg-band-offset, 0px) + var(--dg-header-height) * var(--dg-pinned-gap, 1));
1880
+ border-bottom: var(--dg-header-border);
1881
+ }
1882
+
1883
+ .kanuni-datagrid-pinned-rows--bottom {
1884
+ bottom: calc(var(--dg-header-height) * var(--dg-pinned-gap, 0));
1885
+ border-top: var(--dg-header-border);
1886
+ }
1887
+
1888
+ /* The rows inside keep the grid's cell geometry but not its striping — these
1889
+ are not part of the alternating rhythm of the data. */
1890
+ .kanuni-datagrid-pinned-rows .kanuni-datagrid-row,
1891
+ .kanuni-datagrid-pinned-rows .kanuni-datagrid-cell {
1892
+ background: var(--dg-surface-header);
1893
+ }
1894
+
1895
+ .kanuni-datagrid-footer-row {
1896
+ position: sticky;
1897
+ bottom: 0;
1898
+ z-index: 3;
1899
+ display: flex;
1900
+ height: var(--dg-header-height);
1901
+ background: var(--dg-footer-bg);
1902
+ border-top: var(--dg-header-border);
1903
+ }
1904
+
1905
+ /* Totals at the TOP (P2.8) — same row, other edge. The border swaps sides so
1906
+ the rule always separates the totals from the DATA, not from the header. */
1907
+ .kanuni-datagrid-footer-row--top {
1908
+ top: calc(var(--dg-band-offset, 0px) + var(--dg-header-height) * var(--dg-pinned-gap, 1));
1909
+ bottom: auto;
1910
+ border-top: none;
1911
+ border-bottom: var(--dg-header-border);
1912
+ }
1913
+
1914
+ .kanuni-datagrid-footer-cell {
1915
+ display: flex;
1916
+ align-items: center;
1917
+ gap: 5px;
1918
+ flex: none;
1919
+ height: 100%;
1920
+ padding: 0 var(--dg-cell-padding-x);
1921
+ background: var(--dg-footer-bg);
1922
+ border-inline-end: var(--dg-header-cell-border);
1923
+ font-size: var(--dg-header-font-size);
1924
+ white-space: nowrap;
1925
+ overflow: hidden;
1926
+ user-select: none;
1927
+ }
1928
+
1929
+ .kanuni-datagrid-footer-cell--number {
1930
+ justify-content: flex-end;
1931
+ font-variant-numeric: tabular-nums;
1932
+ }
1933
+
1934
+ .kanuni-datagrid-footer-cell--pinned {
1935
+ position: sticky;
1936
+ /* Above the unpinned footer cells, matching the header's pinned layer. */
1937
+ z-index: 4;
1938
+ }
1939
+
1940
+ .kanuni-datagrid-footer-cell:hover {
1941
+ background: var(--dg-surface-hover);
1942
+ cursor: context-menu;
1943
+ }
1944
+
1945
+ .kanuni-datagrid-footer-value {
1946
+ overflow: hidden;
1947
+ text-overflow: ellipsis;
1948
+ font-weight: 600;
1949
+ color: var(--dg-text);
1950
+ }
1951
+
1952
+ /* The affordance in an empty cell: barely there until hovered, so an untouched
1953
+ footer reads as blank rather than as a row of dots. */
1954
+ .kanuni-datagrid-footer-hint {
1955
+ margin-inline-start: auto;
1956
+ color: var(--dg-border-strong);
1957
+ opacity: 0;
1958
+ }
1959
+
1960
+ .kanuni-datagrid-footer-cell:hover .kanuni-datagrid-footer-hint {
1961
+ opacity: 1;
1962
+ }
1963
+
1964
+ /* ── Focused row (gap G6) ──────────────────────────────────
1965
+ Deliberately NOT the selection colour: focus is where the keyboard is,
1966
+ selection is what an action applies to, and a user who cannot tell them
1967
+ apart cannot tell what "delete selected" is about to do. An inset bar on the
1968
+ leading edge reads as "you are here" without competing with the row tint. */
1969
+ /* Mixed into the row's OWN colour, never into `transparent`.
1970
+ Pinned cells take `background: inherit`, so a see-through row tint made them
1971
+ see-through too — and a sticky cell you can see through shows the columns
1972
+ sliding along underneath it, which reads as two rows of text printed on top
1973
+ of each other. Both variants below are opaque, and keep the stripe. */
1974
+ .kanuni-datagrid-row--focused {
1975
+ background: color-mix(in srgb, var(--dg-primary) 7%, var(--dg-surface));
1976
+ box-shadow: inset 3px 0 0 0 var(--dg-primary);
1977
+ }
1978
+
1979
+ .kanuni-datagrid-row--odd.kanuni-datagrid-row--focused {
1980
+ background: color-mix(in srgb, var(--dg-primary) 7%, var(--dg-surface-stripe));
1981
+ }
1982
+
1983
+ /* Selected AND focused: the selection tint wins, the bar still marks the row. */
1984
+ .kanuni-datagrid-row--focused.kanuni-datagrid-row--selected {
1985
+ box-shadow: inset 3px 0 0 0 var(--dg-primary);
1986
+ }
1987
+
1988
+ [dir='rtl'] .kanuni-datagrid-row--focused,
1989
+ [dir='rtl'] .kanuni-datagrid-row--focused.kanuni-datagrid-row--selected {
1990
+ box-shadow: inset -3px 0 0 0 var(--dg-primary);
1991
+ }
1992
+
1993
+ /* ── Row drag & drop (G7) ─────────────────────────────────── */
1994
+ /*
1995
+ * A sticky GRIP overlaid at the row's inline start, not a lead column: a third
1996
+ * lead column would have to be threaded through `leadColumnWidth`, every
1997
+ * sticky offset and every aria-colindex across six components, and those
1998
+ * drifting apart is exactly what desynchronizes header and body.
1999
+ */
2000
+ .kanuni-datagrid-row-grip {
2001
+ position: sticky;
2002
+ inset-inline-start: 0;
2003
+ z-index: 5;
2004
+ display: flex;
2005
+ align-items: center;
2006
+ justify-content: center;
2007
+ flex: none;
2008
+ width: 0;
2009
+ overflow: visible;
2010
+ color: var(--dg-text-muted, #94a3b8);
2011
+ cursor: grab;
2012
+ opacity: 0;
2013
+ transition: opacity 120ms ease;
2014
+ }
2015
+
2016
+ /* Takes no horizontal space (width: 0) so column geometry is untouched; the
2017
+ glyph is painted into the row's own leading padding. */
2018
+ .kanuni-datagrid-row-grip > svg {
2019
+ position: absolute;
2020
+ /* Past the row-number column when one is showing (audit finding #4): the
2021
+ glyph used to sit at 1px unconditionally, which put it on top of the
2022
+ digits once P3.5 parked a number column at the inline start — visible at
2023
+ six-digit row counts, where the right-aligned number reaches the cell's
2024
+ left edge. The adapter sets the variable; without it nothing changes. */
2025
+ inset-inline-start: calc(var(--dg-grip-inset, 0px) + 1px);
2026
+ }
2027
+
2028
+ .kanuni-datagrid-row:hover .kanuni-datagrid-row-grip {
2029
+ opacity: 1;
2030
+ }
2031
+
2032
+ .kanuni-datagrid-row-grip:active {
2033
+ cursor: grabbing;
2034
+ }
2035
+
2036
+ /* The drop line. An INSET box-shadow rather than a border: a border would add
2037
+ a pixel to the row and shift every row below it by one as the pointer moves,
2038
+ which reads as the whole grid twitching. */
2039
+ .kanuni-datagrid-row--drop-before {
2040
+ box-shadow: inset 0 2px 0 0 var(--dg-primary);
2041
+ }
2042
+
2043
+ .kanuni-datagrid-row--drop-after {
2044
+ box-shadow: inset 0 -2px 0 0 var(--dg-primary);
2045
+ }
2046
+
2047
+ /* ── Tree data (P1.4) ──────────────────────────────────────
2048
+ Indentation is inline on the cell (depth is data); everything visual lives
2049
+ here so all six themes and dark mode restyle the toggle for free. */
2050
+ .kanuni-datagrid-tree-cell {
2051
+ display: inline-flex;
2052
+ align-items: center;
2053
+ gap: 4px;
2054
+ min-width: 0;
2055
+ }
2056
+
2057
+ .kanuni-datagrid-tree-toggle {
2058
+ flex: none;
2059
+ width: 16px;
2060
+ height: 16px;
2061
+ padding: 0;
2062
+ border: 0;
2063
+ background: transparent;
2064
+ color: var(--dg-text-muted, #64748b);
2065
+ font-size: 10px;
2066
+ line-height: 16px;
2067
+ cursor: pointer;
2068
+ border-radius: var(--dg-radius-control, 4px);
2069
+ }
2070
+
2071
+ .kanuni-datagrid-tree-toggle:hover {
2072
+ color: var(--dg-text, #0f172a);
2073
+ background: var(--dg-surface-hover, #f1f5f9);
2074
+ }
2075
+
2076
+ .kanuni-datagrid-tree-toggle--leaf {
2077
+ cursor: default;
2078
+ pointer-events: none;
2079
+ }
2080
+
2081
+ /* Shown only because a descendant matched the filter — visibly the path,
2082
+ not a match. */
2083
+ .kanuni-datagrid-tree-cell--ancestor {
2084
+ color: var(--dg-text-muted, #64748b);
2085
+ }
2086
+ /* ── Cell flash (P1.7) ─────────────────────────────────────
2087
+ The "this number just changed" pulse. Driven by a class the adapter adds
2088
+ and removes; the colour is a token so all six themes and dark mode inherit
2089
+ it, and the animation is what fades — no JS frame loop. */
2090
+ .kanuni-datagrid-cell--flash {
2091
+ animation: kanuni-datagrid-flash var(--dg-flash-duration, 700ms) ease-out;
2092
+ }
2093
+
2094
+ @keyframes kanuni-datagrid-flash {
2095
+ from {
2096
+ background-color: var(--dg-flash-bg, rgba(3, 105, 161, 0.35));
2097
+ }
2098
+ to {
2099
+ background-color: transparent;
2100
+ }
2101
+ }
2102
+
2103
+ /* A flashing grid is exactly the kind of motion that triggers vestibular
2104
+ symptoms. Reduced-motion keeps the SIGNAL — the cell still marks itself —
2105
+ and drops the animation. */
2106
+ @media (prefers-reduced-motion: reduce) {
2107
+ .kanuni-datagrid-cell--flash {
2108
+ animation: none;
2109
+ background-color: var(--dg-flash-bg, rgba(3, 105, 161, 0.35));
2110
+ }
2111
+ }
2112
+ /* ── Print layout (gap analysis P3.4) ──────────────────────────
2113
+ Everything that makes the grid fast on screen is what ruins it on paper. The
2114
+ scroller is a clipped, fixed-height box holding one screenful; the print
2115
+ engine gets exactly that and paginates twenty rows out of a hundred thousand.
2116
+
2117
+ This class undoes the five things that clip the content, in the order they
2118
+ clip it: the root's own box, the scroll container, the canvas that carries
2119
+ the virtual height, the absolutely-positioned viewport, and the block
2120
+ transform that parks the rendered window at its scroll offset.
2121
+
2122
+ The class is set by the ADAPTER, not by a bare `@media print`, because the
2123
+ rows themselves have to be re-rendered — CSS cannot un-virtualize a list that
2124
+ does not exist in the DOM. */
2125
+ .kanuni-datagrid-print {
2126
+ height: auto !important;
2127
+ max-height: none !important;
2128
+ overflow: visible !important;
2129
+ /* Apps mount the grid as a flex child that fills the page — the shape a
2130
+ screen layout wants. Flex sizing beats `height: auto`, so the box kept the
2131
+ viewport's height and the rows below the fold printed on top of whatever
2132
+ came next. `flex: none` hands the sizing back to the content.
2133
+ A host that constrains the grid from further out still has to release it;
2134
+ the grid can only unclip itself. */
2135
+ flex: none !important;
2136
+ }
2137
+
2138
+ .kanuni-datagrid-print .kanuni-datagrid-scroller {
2139
+ height: auto !important;
2140
+ max-height: none !important;
2141
+ overflow: visible !important;
2142
+ }
2143
+
2144
+ .kanuni-datagrid-print .kanuni-datagrid-canvas,
2145
+ .kanuni-datagrid-print .kanuni-datagrid-rows {
2146
+ /* The virtual extent, in a document that is no longer virtual.
2147
+ `.kanuni-datagrid-rows` is the one that matters and the one that was
2148
+ missed first: it carries the scroll canvas's height inline, and with the
2149
+ viewport put back in flow inside it, that inline height decided how much
2150
+ of the grid reached the page — five hundred rows printed as two hundred
2151
+ pixels of paper. The canvas is here for the same reason one level up. */
2152
+ height: auto !important;
2153
+ }
2154
+
2155
+ .kanuni-datagrid-print .kanuni-datagrid-viewport {
2156
+ position: static;
2157
+ /* The block offset is a scroll position. On paper there is no scrolling, and
2158
+ leaving it in pushed the first row down by however far the user happened to
2159
+ have scrolled. */
2160
+ transform: none !important;
2161
+ will-change: auto;
2162
+ }
2163
+
2164
+ /* Sticky is meaningless without a scrolling ancestor, but it still creates the
2165
+ stacking and offset behaviour — a pinned column would print on top of the
2166
+ cell it sits beside. In flow, each cell keeps its own width and order. */
2167
+ .kanuni-datagrid-print .kanuni-datagrid-header,
2168
+ .kanuni-datagrid-print .kanuni-datagrid-band-row,
2169
+ .kanuni-datagrid-print .kanuni-datagrid-filter-row,
2170
+ .kanuni-datagrid-print .kanuni-datagrid-pinned-rows,
2171
+ .kanuni-datagrid-print .kanuni-datagrid-footer-row {
2172
+ position: static !important;
2173
+ }
2174
+
2175
+ /* Rules that only make sense once the ink is on paper. */
2176
+ @media print {
2177
+ /* Release the page around the grid.
2178
+ A grid is normally mounted as a flex child filling a viewport-height shell,
2179
+ and every ancestor in that chain holds it at one screen — the grid can
2180
+ unclip itself and still print one page. The adapter marks the ROOT ELEMENT
2181
+ while print layout is on, because CSS cannot reach an ancestor from a class
2182
+ on the grid, and only for the duration.
2183
+
2184
+ The grid's OWN wrappers are named; the host's are not, so an app with its
2185
+ own fixed-height shell needs the same two lines on it. */
2186
+ .kanuni-datagrid-printing,
2187
+ .kanuni-datagrid-printing body,
2188
+ .kanuni-datagrid-printing .kanuni-datagrid-enterprise,
2189
+ .kanuni-datagrid-printing .kanuni-datagrid-main {
2190
+ height: auto !important;
2191
+ max-height: none !important;
2192
+ overflow: visible !important;
2193
+ flex: none !important;
2194
+ }
2195
+
2196
+ /* A row split across a page break is unreadable — the top half of the text on
2197
+ one sheet and the bottom half on the next. */
2198
+ .kanuni-datagrid-print .kanuni-datagrid-row,
2199
+ .kanuni-datagrid-print .kanuni-datagrid-group-row {
2200
+ break-inside: avoid;
2201
+ page-break-inside: avoid;
2202
+ }
2203
+
2204
+ /* Chrome and Safari drop background colours by default, which turns the
2205
+ header, the group banners and the row striping into undifferentiated white.
2206
+ The structure IS the meaning here, so ask for them back. */
2207
+ .kanuni-datagrid-print {
2208
+ -webkit-print-color-adjust: exact;
2209
+ print-color-adjust: exact;
2210
+ }
2211
+
2212
+ /* Controls that print as furniture: nothing on paper can be clicked, and each
2213
+ one costs a column of width that the data needs. */
2214
+ .kanuni-datagrid-print .kanuni-datagrid-toolbar,
2215
+ .kanuni-datagrid-print .kanuni-datagrid-filter-panel,
2216
+ .kanuni-datagrid-print .kanuni-datagrid-pager,
2217
+ .kanuni-datagrid-print .kanuni-datagrid-status-bar,
2218
+ .kanuni-datagrid-print .kanuni-datagrid-sidebar,
2219
+ .kanuni-datagrid-print .kanuni-datagrid-resize-handle,
2220
+ .kanuni-datagrid-print .kanuni-datagrid-filter-button {
2221
+ display: none !important;
2222
+ }
2223
+ }
2224
+
2225
+ /* ── Cell notes (gap analysis P3.3) ────────────────────────────
2226
+ A note is something a PERSON attached to this cell, and the whole value of
2227
+ the marker is that you can find those cells without hovering every one of
2228
+ them. Excel's corner triangle, for the same reason and in the same corner. */
2229
+ .kanuni-datagrid-cell--noted {
2230
+ position: relative;
2231
+ }
2232
+
2233
+ .kanuni-datagrid-cell--noted::after {
2234
+ content: '';
2235
+ position: absolute;
2236
+ top: 0;
2237
+ /* Logical, not `right`: in an RTL grid the marker belongs in the corner the
2238
+ reader's eye ends at, which is the other one. */
2239
+ inset-inline-end: 0;
2240
+ width: 0;
2241
+ height: 0;
2242
+ /* A triangle from borders — no glyph, no font dependency, no extra element in
2243
+ a cell the row memo is built to keep cheap. */
2244
+ border-top: 6px solid var(--dg-note-marker);
2245
+ border-inline-start: 6px solid transparent;
2246
+ pointer-events: none;
2247
+ }
2248
+
2249
+ .kanuni-datagrid-note-editor {
2250
+ display: flex;
2251
+ flex-direction: column;
2252
+ gap: 8px;
2253
+ padding: 8px;
2254
+ min-width: 260px;
2255
+ }
2256
+
2257
+ .kanuni-datagrid-note-input {
2258
+ font: inherit;
2259
+ color: var(--dg-text);
2260
+ background: var(--dg-surface);
2261
+ border: 1px solid var(--dg-border);
2262
+ border-radius: var(--dg-radius);
2263
+ padding: 6px 8px;
2264
+ resize: vertical;
2265
+ min-height: 64px;
2266
+ }
2267
+
2268
+ .kanuni-datagrid-note-input:focus-visible {
2269
+ outline: 2px solid var(--dg-focus-ring);
2270
+ outline-offset: -1px;
2271
+ }
2272
+
2273
+ .kanuni-datagrid-note-actions {
2274
+ display: flex;
2275
+ justify-content: flex-end;
2276
+ gap: 6px;
2277
+ }
2278
+
2279
+ /* ── Cell spans (gap analysis P3.1) ────────────────────────────
2280
+ The block is an absolutely-positioned child of the cell, not a taller cell.
2281
+ A taller cell would change the height of its ROW, and a grid with an
2282
+ `autoHeight` column measures rows and feeds the result back to the engine —
2283
+ block inflates row, row moves offsets, offsets resize block, React ends it
2284
+ with "Maximum update depth exceeded". Out of the flow, the cell stays the
2285
+ height it always was. */
2286
+ .kanuni-datagrid-cell--span {
2287
+ position: relative;
2288
+ /* The box reaches past the cell's own bottom edge on purpose. */
2289
+ overflow: visible;
2290
+ }
2291
+
2292
+ .kanuni-datagrid-span-box {
2293
+ position: absolute;
2294
+ top: 0;
2295
+ inset-inline: 0;
2296
+ display: flex;
2297
+ /* Top-aligned, not centred: a two-row block and a fifty-row block should put
2298
+ the value in the same place, and a reader looks for it beside the first row
2299
+ of the group. */
2300
+ align-items: flex-start;
2301
+ /* Horizontal padding matches an ordinary cell so the value lines up with the
2302
+ column above and below. The top padding centres it within the block's FIRST
2303
+ row — the grid's line-height is 1.4, so half the leftover row height puts
2304
+ it exactly where an unmerged cell would sit. */
2305
+ padding: calc((var(--dg-row-height) - 1.4em) / 2) var(--dg-cell-padding-x);
2306
+ /* Its own background: the row underneath ends where the row does, and the
2307
+ rest of the block would show whatever is behind it. */
2308
+ background: var(--dg-surface);
2309
+ /* Above the STATIC center cells of later rows (which would otherwise paint
2310
+ their backgrounds over everything below the block's first row) — but below
2311
+ the pinned columns' 2. The audit caught 3 here: a center column's block
2312
+ painted OVER the left-pinned column during horizontal scroll, because the
2313
+ box competes in the viewport's stacking context while a box inside a
2314
+ pinned cell is scoped by its parent's own context. */
2315
+ z-index: 1;
2316
+ border-bottom: var(--dg-row-border);
2317
+ overflow: hidden;
2318
+ white-space: nowrap;
2319
+ text-overflow: ellipsis;
2320
+ }
2321
+
2322
+ .kanuni-datagrid-cell--span-covered {
2323
+ /* No content, no rules. The block above owns this space. */
2324
+ border-bottom: none;
2325
+ background: transparent;
2326
+ }
2327
+
2328
+ /* The FOCUSED covered cell lifts over the block and shows its own content —
2329
+ this is what makes an editor opened on it visible (audit finding #3), and
2330
+ arrowing through a merge gives per-row feedback instead of a void. */
2331
+ .kanuni-datagrid-cell--span-covered.kanuni-datagrid-cell--focused {
2332
+ position: relative;
2333
+ z-index: 2;
2334
+ background: var(--dg-surface);
2335
+ border-bottom: var(--dg-row-border);
2336
+ }
2337
+
2338
+ /* A merged block sits over rows that stripe, get selected and get hovered.
2339
+ Their treatment must not show THROUGH the block — only beside it. */
2340
+ .kanuni-datagrid-row--odd .kanuni-datagrid-span-box,
2341
+ .kanuni-datagrid-row--selected .kanuni-datagrid-span-box {
2342
+ background: var(--dg-surface);
2343
+ }
2344
+
2345
+ /* ── Filter popup resize handle (user request, 2026-08-14) ─────
2346
+ One corner, chosen by where there is room to grow: bottom-right normally,
2347
+ bottom-left when the popup is clamped against the viewport's right edge.
2348
+ Drawn as the familiar diagonal ridges so it reads as "drag me". */
2349
+ .kanuni-datagrid-popup-resize {
2350
+ position: absolute;
2351
+ bottom: 0;
2352
+ width: 14px;
2353
+ height: 14px;
2354
+ z-index: 2;
2355
+ background:
2356
+ repeating-linear-gradient(
2357
+ -45deg,
2358
+ transparent 0 3px,
2359
+ var(--dg-border-strong) 3px 4px
2360
+ );
2361
+ opacity: 0.7;
2362
+ }
2363
+
2364
+ .kanuni-datagrid-popup-resize--right {
2365
+ right: 0;
2366
+ cursor: nwse-resize;
2367
+ clip-path: polygon(100% 0, 100% 100%, 0 100%);
2368
+ }
2369
+
2370
+ .kanuni-datagrid-popup-resize--left {
2371
+ left: 0;
2372
+ cursor: nesw-resize;
2373
+ clip-path: polygon(0 0, 100% 100%, 0 100%);
2374
+ background:
2375
+ repeating-linear-gradient(
2376
+ 45deg,
2377
+ transparent 0 3px,
2378
+ var(--dg-border-strong) 3px 4px
2379
+ );
2380
+ }
2352
2381
 
2353
2382
  /* ─────────────────────────────────────────────────────────────
2354
2383
  KANUNILABS DataGrid — packaged themes