fsanyoto commited on
Commit
2358748
Β·
verified Β·
1 Parent(s): 5799f9f

Deploy AIOS web (React glide grid + FastAPI slice)

Browse files
RELEASES.json CHANGED
@@ -1,6 +1,12 @@
1
  {
2
- "current": "v48 (3c69fd9)",
3
  "releases": [
 
 
 
 
 
 
4
  {
5
  "version": "v48",
6
  "sha": "3c69fd9",
 
1
  {
2
+ "current": "v49 (9a14c01)",
3
  "releases": [
4
+ {
5
+ "version": "v49",
6
+ "sha": "9a14c01",
7
+ "date": "2026-08-23",
8
+ "subject": "release v49"
9
+ },
10
  {
11
  "version": "v48",
12
  "sha": "3c69fd9",
VERSION CHANGED
@@ -1 +1 @@
1
- v48 (3c69fd9)
 
1
+ v49 (9a14c01)
web/src/customer-grid/RecordDetail.css CHANGED
@@ -1,1021 +1,1027 @@
1
- .cg-record-backdrop {
2
- position: fixed;
3
- inset: 0;
4
- z-index: var(--cg-z-drawer);
5
- display: grid;
6
- place-items: center;
7
- padding: 24px;
8
- background: rgba(25, 34, 46, 0.34);
9
- backdrop-filter: blur(1px);
10
- }
11
-
12
- .cg-record-modal {
13
- width: min(1120px, calc(100vw - 48px));
14
- height: min(760px, calc(100vh - 48px));
15
- min-height: 420px;
16
- display: flex;
17
- flex-direction: column;
18
- overflow: hidden;
19
- border: 1px solid color-mix(in srgb, var(--lp-line) 82%, var(--lp-muted));
20
- border-radius: 12px;
21
- background: var(--lp-surface);
22
- box-shadow:
23
- 0 30px 80px rgba(20, 30, 43, 0.24),
24
- 0 4px 18px rgba(20, 30, 43, 0.12);
25
- color: var(--lp-ink);
26
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
27
- font-size: var(--lp-fs-xs);
28
- animation: cg-record-open 150ms cubic-bezier(0.2, 0, 0, 1);
29
- }
30
-
31
- .cg-record-modal:focus {
32
- outline: none;
33
- }
34
-
35
- /* Universal linked-record viewer: the target is the real Grid surface, not a JSON-shaped
36
- approximation. It is deliberately wider than the record detail so filtering and multi-column
37
- sorting have the same working room they do on the database page. */
38
- .cg-link-grid-modal {
39
- width: min(1320px, calc(100vw - 48px));
40
- height: min(840px, calc(100vh - 48px));
41
- min-height: 460px;
42
- display: flex;
43
- flex-direction: column;
44
- overflow: hidden;
45
- border: 1px solid color-mix(in srgb, var(--lp-line) 82%, var(--lp-muted));
46
- border-radius: 12px;
47
- background: var(--lp-surface);
48
- box-shadow:
49
- 0 30px 80px rgba(20, 30, 43, 0.24),
50
- 0 4px 18px rgba(20, 30, 43, 0.12);
51
- color: var(--lp-ink);
52
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
53
- font-size: var(--lp-fs-xs);
54
- animation: cg-record-open 150ms cubic-bezier(0.2, 0, 0, 1);
55
- }
56
-
57
- .cg-link-grid-modal:focus { outline: none; }
58
-
59
- .cg-link-grid-head {
60
- flex: 0 0 54px;
61
- min-height: 54px;
62
- display: flex;
63
- align-items: center;
64
- justify-content: space-between;
65
- gap: 16px;
66
- padding: 0 14px 0 18px;
67
- border-bottom: 1px solid var(--lp-line);
68
- background: var(--lp-surface);
69
- }
70
-
71
- .cg-link-grid-title {
72
- font-size: var(--lp-fs-sm);
73
- font-weight: 650;
74
- line-height: 1.2;
75
- }
76
-
77
- .cg-link-grid-sub {
78
- margin-top: 2px;
79
- color: var(--lp-muted);
80
- font-size: var(--lp-fs-3xs);
81
- }
82
-
83
- .cg-link-grid-body {
84
- flex: 1 1 auto;
85
- min-width: 0;
86
- min-height: 0;
87
- background: var(--lp-surface);
88
- }
89
-
90
- .cg-link-grid-foot {
91
- flex: 0 0 52px;
92
- display: flex;
93
- align-items: center;
94
- justify-content: flex-end;
95
- gap: 8px;
96
- padding: 8px 14px;
97
- border-top: 1px solid var(--lp-line);
98
- background: var(--lp-surface);
99
- }
100
-
101
- @keyframes cg-record-open {
102
- from {
103
- opacity: 0;
104
- transform: translateY(8px) scale(0.992);
105
- }
106
- }
107
-
108
- .cg-record-modal > .cg-detail-header {
109
- height: 46px;
110
- padding: 0 12px;
111
- background: var(--lp-surface);
112
- }
113
-
114
- .cg-record-layout {
115
- flex: 1 1 auto;
116
- min-height: 0;
117
- display: grid;
118
- grid-template-columns: minmax(0, 1.95fr) minmax(300px, 0.95fr);
119
- }
120
-
121
- .cg-record-modal--single {
122
- width: min(880px, calc(100vw - 48px));
123
- }
124
-
125
- .cg-record-modal--single .cg-record-layout {
126
- grid-template-columns: minmax(0, 1fr);
127
- }
128
-
129
- .cg-record-main {
130
- min-width: 0;
131
- min-height: 0;
132
- display: flex;
133
- flex-direction: column;
134
- }
135
-
136
- .cg-record-main .cg-detail-titlewrap {
137
- padding: 22px 28px 18px;
138
- }
139
-
140
- .cg-record-main .cg-detail-title {
141
- font-size: var(--lp-fs-lg);
142
- line-height: 1.25;
143
- }
144
-
145
- .cg-record-main .cg-detail-body {
146
- min-height: 0;
147
- padding: 4px 12px 28px;
148
- scrollbar-gutter: stable;
149
- }
150
-
151
- .cg-record-main .cg-detail-section {
152
- padding: 16px;
153
- }
154
-
155
- /* ==========================================================================
156
- Wave 2026-08-02 item 2 β€” the record fields: no yellow, one subtle box.
157
- ==========================================================================
158
- Owner: "kill yellow, subtle outline for all field types."
159
-
160
- Before this, exactly one kind of field had a treatment β€” the editable
161
- overlay notes, in a gold frame on a gold wash (index.css:2027) β€” and every
162
- other field was bare text under a hairline. So the loudest thing in the
163
- panel was also the least important, and the panel had two layouts: label
164
- ABOVE a full-width textarea for notes, label BESIDE the value for the rest.
165
-
166
- Now every field is one row on one grid: label left, a value SLOT right.
167
- The slot carries a 1px --lp-line hairline and --lp-r-md corners, and the
168
- fill is what says whether it is yours:
169
-
170
- --lp-surface-2 (warm grey) read-only β€” an Odoo attribute, a picked
171
- choice, a star rating you set from the table
172
- --lp-surface (white) editable β€” type here
173
-
174
- That replaces the signal the yellow wash used to carry (this stratum is
175
- your data, not the ERP's) with one that is also an affordance: the bright
176
- slots are the ones that respond. Interaction, not decoration, marks them.
177
-
178
- EVERY rule here is scoped `.cg-record-main …` so it (a) outranks the
179
- index.css base block, which is FROZEN for the other sessions this wave,
180
- and (b) stops at the record modal β€” the same `.cg-detail-*` classes dress
181
- surfaces this file does not own. Specificity, never source order: the
182
- :focus and ::placeholder overrides below exist because their index.css
183
- counterparts tie at (0,2,0) and (0,1,1), and a tie is decided by bundle
184
- order β€” which is not a thing to bet a gold glow on. */
185
-
186
- .cg-record-main .cg-detail-field {
187
- min-height: 0;
188
- align-items: flex-start;
189
- gap: 12px;
190
- padding: 3px 0;
191
- /* The hairline separator is REPLACED by the box, not stacked under it β€”
192
- a ruled row around a bordered slot reads as a rendering fault. */
193
- border-bottom: none;
194
- }
195
-
196
- .cg-record-main .cg-detail-label {
197
- flex: 0 0 160px;
198
- /* Optical, not arithmetic: 7px lands the 12px label on the same line as the
199
- 13px value inside its 1px border + 5px padding. */
200
- padding-top: 7px;
201
- color: var(--lp-muted);
202
- font-size: var(--lp-fs-2xs);
203
- line-height: 1.45;
204
- }
205
-
206
- .cg-record-main .cg-detail-value {
207
- min-height: 30px;
208
- padding: 5px 9px;
209
- border: 1px solid var(--lp-line);
210
- border-radius: var(--lp-r-md);
211
- background: var(--lp-surface-2);
212
- line-height: 1.45;
213
- }
214
-
215
- /* ⭐ Wave-23 C8 (owner item 4 / ruling R9) β€” a MACHINE-OWNED field's value slot.
216
-
217
- `--lp-machine-tint` is EXACTLY what the canvas wash (`theme.MACHINE_CELL`,
218
- `washOf(LP_MACHINE_DEEP, 0.12)`) composites to over white, so the drawer slot
219
- and the grid cell are one grey rather than two that were each picked to look
220
- right on their own β€” the sibling-panel rule (DESIGN.md Β§2) applied across two
221
- rendering technologies instead of two panes.
222
-
223
- THE VALUE KEEPS ITS INK, deliberately, on both surfaces. The wash is the whole
224
- signal; dimming the number as well would say the figure matters less, and an
225
- automation-written follower count is the entire reason its column exists.
226
-
227
- Background only β€” no border change, no second marker. The column menu already
228
- says "Filled by automation" once, and DESIGN.md Β§4 is that the app does not
229
- narrate itself twice. */
230
- .cg-record-main .cg-detail-field.is-machine .cg-detail-value {
231
- background: var(--lp-machine-tint);
232
- }
233
-
234
- /* A rating's marks are 18px inline SVG; on the text baseline they hang their
235
- descender space below the line box and grow the slot past every neighbour. */
236
- .cg-record-main .cg-detail-value .cg-stars {
237
- vertical-align: middle;
238
- }
239
-
240
- /* An editable field is the same ROW as every other one now β€” index.css stacks
241
- it (flex-direction: column, and a label with `flex: none`), which is what
242
- made the notes the odd block out. */
243
- .cg-record-main .cg-detail-field--edit {
244
- flex-direction: row;
245
- align-items: flex-start;
246
- gap: 12px;
247
- }
248
-
249
- .cg-record-main .cg-detail-field--edit .cg-detail-label {
250
- flex: 0 0 160px;
251
- }
252
-
253
- /* The value column of an editable row: the box, then its action link. */
254
- .cg-detail-editwrap {
255
- flex: 1 1 auto;
256
- min-width: 0;
257
- display: flex;
258
- flex-direction: column;
259
- }
260
-
261
- .cg-record-main .cg-detail-textarea {
262
- /* The owner's "big static box" was min-height: 76px. One row is the resting
263
- size; AutoTextarea grows it from there, so an empty note is as quiet as an
264
- empty value and a long one is readable without a scrollbar. */
265
- min-height: 30px;
266
- max-height: 260px;
267
- padding: 5px 9px;
268
- overflow-y: auto;
269
- /* The height is measured, so a drag handle would fight the measurement: the
270
- next keystroke re-fits and snaps back whatever was dragged. */
271
- resize: none;
272
- border: 1px solid var(--lp-line);
273
- background: var(--lp-surface);
274
- line-height: 1.45;
275
- }
276
-
277
- .cg-record-main .cg-detail-textarea:hover:not(:focus) {
278
- border-color: color-mix(in srgb, var(--lp-line) 45%, var(--lp-muted));
279
- }
280
-
281
- .cg-record-main .cg-detail-textarea:focus {
282
- /* Identical to the comment composer below β€” same modal, same gesture, so the
283
- same mark. `box-shadow: none` is load-bearing: without it index.css:2047's
284
- 3px gold glow survives an override that only changes the border. */
285
- outline: 2px solid var(--lp-blue-deep);
286
- outline-offset: -1px;
287
- border-color: transparent;
288
- box-shadow: none;
289
- }
290
-
291
- .cg-record-main .cg-detail-textarea::placeholder {
292
- /* --lp-muted is the palette's body-safe grey (4.83:1). The stock #a2a2ac was
293
- picked against the gold wash and reads as 2.7:1 on white β€” and "Add
294
- notes…" is an instruction, not decoration. */
295
- color: var(--lp-muted);
296
- opacity: 1;
297
- }
298
-
299
- /* A checkbox IS its own control; boxing it would frame a 15px square in a
300
- 30px slot and say nothing. It only needs to sit on the label's line. */
301
- .cg-record-main .cg-detail-checkbox {
302
- margin-top: 7px;
303
- }
304
-
305
- /* ==========================================================================
306
- Wave 14 item 9 β€” an editor for EVERY user-created field type.
307
- ==========================================================================
308
- Until now one type was editable here (notes) and the other eight read. The
309
- affordance law item 2 established is what makes this scale without a legend:
310
-
311
- --lp-surface-2 (warm grey) read-only
312
- --lp-surface (white) yours to change
313
-
314
- so `--live` is the ONLY thing that says "this responds", across nine row
315
- shapes. It is deliberately the same white the textarea already used, not a
316
- new colour: adding a third fill would turn a two-state signal into a puzzle.
317
-
318
- ONE BOX PER ROW. The controls are borderless and transparent INSIDE the
319
- slot rather than bordered themselves β€” a bordered input inside a bordered
320
- slot draws two rectangles 1px apart, which reads as a rendering fault, and
321
- it would also break the single label-left/value-right grid every other row
322
- sits on. The focus ring therefore lands on the SLOT (`:focus-within`) and
323
- the control's own outline is suppressed; the ring is the same 2px inset
324
- blue as the textarea and the comment composer, because it is the same
325
- gesture in the same modal.
326
-
327
- ⚠ `font: inherit` is NOT used on these controls. The `font` shorthand
328
- RESETS `font-variant-numeric`, which `.cg-detail-value` sets to tabular-nums
329
- (index.css:2013) β€” so the shorthand would silently un-align every number in
330
- the panel. The longhands are spelled out instead. */
331
- .cg-record-main .cg-detail-value--live {
332
- background: var(--lp-surface);
333
- }
334
-
335
- .cg-record-main .cg-detail-value--live:hover:not(:focus-within) {
336
- border-color: color-mix(in srgb, var(--lp-line) 45%, var(--lp-muted));
337
- }
338
-
339
- .cg-record-main .cg-detail-value--live:focus-within {
340
- outline: 2px solid var(--lp-blue-deep);
341
- outline-offset: -1px;
342
- border-color: transparent;
343
- }
344
-
345
- /* The slot as a ROW: an avatar, a currency mark or a hint sits beside its
346
- control instead of above it. */
347
- .cg-record-main .cg-detail-pickslot {
348
- display: flex;
349
- align-items: center;
350
- gap: 7px;
351
- }
352
-
353
- .cg-record-main .cg-detail-select,
354
- .cg-record-main .cg-detail-input {
355
- flex: 1 1 auto;
356
- min-width: 0;
357
- width: 100%;
358
- padding: 0;
359
- border: 0;
360
- background: transparent;
361
- color: var(--lp-ink);
362
- font-family: inherit;
363
- font-size: inherit;
364
- font-weight: inherit;
365
- line-height: 1.45;
366
- }
367
-
368
- /* The ring is on the slot; a second one inside it would draw a rectangle
369
- around a rectangle. */
370
- .cg-record-main .cg-detail-select:focus,
371
- .cg-record-main .cg-detail-input:focus {
372
- outline: none;
373
- }
374
-
375
- .cg-record-main .cg-detail-select {
376
- /* The native arrow is kept β€” it is the only thing that says "pick" once the
377
- border has been handed to the slot. */
378
- cursor: pointer;
379
- margin-left: -2px;
380
- }
381
-
382
- .cg-record-main .cg-detail-input--num {
383
- text-align: right;
384
- }
385
-
386
- /* Spinners removed, and not for looks: this panel SCROLLS, and a wheel event
387
- over a focused number input changes the value in Chrome. A currency figure
388
- silently edited by scrolling past it is data loss with no gesture behind it.
389
- The right-aligned tabular figure and the unit mark carry the type. */
390
- .cg-record-main .cg-detail-input--num::-webkit-outer-spin-button,
391
- .cg-record-main .cg-detail-input--num::-webkit-inner-spin-button {
392
- appearance: none;
393
- margin: 0;
394
- }
395
-
396
- .cg-record-main .cg-detail-input--num {
397
- -moz-appearance: textfield;
398
- appearance: textfield;
399
- }
400
-
401
- .cg-record-main .cg-detail-unit,
402
- .cg-record-main .cg-detail-hint {
403
- flex: 0 0 auto;
404
- color: var(--lp-muted);
405
- font-size: var(--lp-fs-2xs);
406
- }
407
-
408
- .cg-record-main .cg-detail-hint {
409
- font-style: italic;
410
- }
411
-
412
- .cg-record-main .cg-detail-nochoice {
413
- color: var(--lp-muted);
414
- font-size: var(--lp-fs-2xs);
415
- }
416
-
417
- /* Multiselect: a chip row, not a `<select multiple>`. The native control needs
418
- ctrl-click for a second value and paints a four-row scrollbox for a
419
- three-choice field; a chip says its own state and takes one click. */
420
- .cg-record-main .cg-detail-chips {
421
- display: flex;
422
- flex-wrap: wrap;
423
- gap: 5px;
424
- }
425
-
426
- .cg-record-main .cg-detail-chip {
427
- height: 22px;
428
- padding: 0 9px;
429
- border: 1px solid var(--lp-line);
430
- border-radius: var(--lp-r-pill);
431
- background: var(--lp-surface-2);
432
- color: var(--lp-muted);
433
- font-family: inherit;
434
- font-size: var(--lp-fs-2xs);
435
- line-height: 1;
436
- white-space: nowrap;
437
- cursor: pointer;
438
- }
439
-
440
- .cg-record-main .cg-detail-chip:hover {
441
- border-color: var(--lp-blue-deep);
442
- }
443
-
444
- /* Tint fills, `-deep` on the text β€” the brand rule. The base pastel is not a
445
- readable ink at 12px. */
446
- .cg-record-main .cg-detail-chip.is-on {
447
- border-color: var(--lp-blue-deep);
448
- background: var(--lp-blue-tint);
449
- color: var(--lp-blue-deep);
450
- font-weight: 600;
451
- }
452
-
453
- .cg-record-main .cg-detail-chip:focus-visible {
454
- outline: 2px solid var(--lp-blue-deep);
455
- outline-offset: 1px;
456
- }
457
-
458
- /* ⭐ Wave-19 R7 β€” the IMAGE slot: the picture, then its reference and controls.
459
- The thumbnail is `object-fit: contain` for the reason the grid cell is: a
460
- product photo cropped to a square loses the thing being sold. Letterboxing
461
- inside a bounded frame is the honest fit for a catalogue picture. */
462
- .cg-record-main .cg-detail-imageslot {
463
- display: flex;
464
- align-items: flex-start;
465
- gap: 10px;
466
- }
467
-
468
- .cg-record-main .cg-detail-image,
469
- .cg-record-main .cg-detail-imageempty {
470
- flex: 0 0 auto;
471
- width: 72px;
472
- height: 72px;
473
- border: 1px solid var(--lp-line);
474
- border-radius: var(--lp-r-sm);
475
- background: var(--lp-surface-2);
476
- object-fit: contain;
477
- }
478
-
479
- .cg-record-main .cg-detail-imagemeta {
480
- display: flex;
481
- flex-direction: column;
482
- align-items: flex-start;
483
- gap: 5px;
484
- min-width: 0;
485
- }
486
-
487
- /* The reference itself, in the mono face: it is an identifier (a SKU code, a
488
- slug, a minted id), and identifiers are read character by character. The
489
- stack is spelled out rather than tokenised because there is no `--lp-mono`
490
- token β€” the three existing monospace sites in index.css all spell it too, and
491
- C6 says NO new tokens this wave. */
492
- .cg-record-main .cg-detail-imageref {
493
- max-width: 100%;
494
- overflow: hidden;
495
- text-overflow: ellipsis;
496
- white-space: nowrap;
497
- color: var(--lp-muted);
498
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
499
- font-size: var(--lp-fs-2xs);
500
- }
501
-
502
- .cg-record-main .cg-detail-imageacts {
503
- display: inline-flex;
504
- gap: 6px;
505
- }
506
-
507
- /* The `<label>` and the `<button>` are one control visually β€” the file input
508
- inside the label is hidden, which is the only way to style a file picker. */
509
- .cg-record-main .cg-detail-imagebtn {
510
- display: inline-flex;
511
- align-items: center;
512
- height: 22px;
513
- padding: 0 9px;
514
- border: 1px solid var(--lp-line);
515
- border-radius: var(--lp-r-pill);
516
- background: var(--lp-surface-2);
517
- color: var(--lp-muted);
518
- font-family: inherit;
519
- font-size: var(--lp-fs-2xs);
520
- line-height: 1;
521
- white-space: nowrap;
522
- cursor: pointer;
523
- }
524
-
525
- .cg-record-main .cg-detail-imagebtn:hover {
526
- border-color: var(--lp-blue-deep);
527
- color: var(--lp-blue-deep);
528
- }
529
-
530
- .cg-record-main .cg-detail-imagebtn input[type="file"] {
531
- display: none;
532
- }
533
-
534
- .cg-record-main .cg-detail-imageerr {
535
- color: var(--lp-red-deep);
536
- font-size: var(--lp-fs-2xs);
537
- }
538
-
539
- /* ⭐ Wave-23 C7 (owner item 5) β€” a `json` field's row: the same compact preview the grid cell
540
- paints, plus the door to the viewer. Reuses `.cg-detail-imagebtn` rather than minting a
541
- second quiet-button shape (DESIGN.md Β§4) β€” the two rows are doing the same job, showing a
542
- value too big for a slot and offering the surface that owns it. */
543
- .cg-record-main .cg-detail-jsonslot {
544
- display: flex;
545
- align-items: center;
546
- justify-content: space-between;
547
- gap: 10px;
548
- }
549
-
550
- /* ⚠ `min-width: 0` is what lets the ellipsis happen at all: a flex item's default
551
- `min-width: auto` refuses to shrink below its content, so the preview would push the Open
552
- button out of the panel instead of truncating. Monospace because the preview is a fragment
553
- of a document β€” `{handle: "royalimports"}` reads as code, and setting it in Inter beside the
554
- viewer's monospace tree would make one value look like two different things. */
555
- .cg-record-main .cg-detail-jsonpreview {
556
- flex: 1 1 auto;
557
- min-width: 0;
558
- overflow: hidden;
559
- text-overflow: ellipsis;
560
- white-space: nowrap;
561
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
562
- font-size: var(--lp-fs-2xs);
563
- color: var(--lp-ink);
564
- }
565
-
566
- /* Rating: the stars ARE the picker. Clicking the lit star clears the value β€”
567
- without it a rating could be changed here but never removed. */
568
- .cg-record-main .cg-detail-stars {
569
- display: inline-flex;
570
- align-items: center;
571
- gap: 1px;
572
- }
573
-
574
- .cg-record-main .cg-detail-star {
575
- display: grid;
576
- place-items: center;
577
- padding: 0 1px;
578
- border: 0;
579
- background: transparent;
580
- color: inherit;
581
- cursor: pointer;
582
- line-height: 0;
583
- }
584
-
585
- .cg-record-main .cg-detail-star:focus-visible {
586
- outline: 2px solid var(--lp-blue-deep);
587
- outline-offset: 1px;
588
- border-radius: var(--lp-r-sm);
589
- }
590
-
591
- /* ==========================================================================
592
- Wave 14 item 11 (C-AVATAR) β€” the person, as a face.
593
- ==========================================================================
594
- R6: cells show the disc alone; here it sits BESIDE the name, because two
595
- initials at 20px identify nobody by themselves. The initials pastel is
596
- `pickTint`'s hash β€” the same one the option pills use β€” so one person is one
597
- colour on every surface, and it is the DESIGNED state rather than the error
598
- state: every deployment renders it until a photo is uploaded. */
599
- .cg-record-main .cg-detail-avatar {
600
- flex: 0 0 auto;
601
- width: 20px;
602
- height: 20px;
603
- display: inline-grid;
604
- place-items: center;
605
- border-radius: 50%;
606
- object-fit: cover;
607
- font-size: var(--lp-fs-4xs);
608
- font-weight: 700;
609
- letter-spacing: 0.01em;
610
- line-height: 1;
611
- }
612
-
613
- /* ==========================================================================
614
- Wave 2026-08-02 item 5 (C-LAYOUT) β€” the six-dot reorder handle.
615
- ==========================================================================
616
- Hover-revealed, Airtable's own behaviour and the reason it is right here:
617
- this panel carries ~26 fields, and 26 permanently-visible grips would put a
618
- dotted column down the left edge louder than any value in the record. The
619
- handle is an affordance for a gesture, not information.
620
-
621
- It keeps its 16px gutter at rest (`opacity`, never `display`), so revealing
622
- it cannot reflow the row under the pointer that is reaching for it β€” and it
623
- stays in the tab order at all times, which is what makes the ArrowUp/Down
624
- fallback reachable without a mouse.
625
-
626
- ⚠ Known gap, booked rather than papered over: HTML5 drag-and-drop does not
627
- fire for touch. On a touch device there is no hover, so the grip stays
628
- invisible and the gesture is simply absent β€” which is the honest
629
- degradation. A pointer-events reorder is the fix if it is ever asked for. */
630
- .cg-detail-grip {
631
- flex: 0 0 16px;
632
- height: 22px;
633
- margin-top: 5px;
634
- display: grid;
635
- place-items: center;
636
- padding: 0;
637
- border: 0;
638
- border-radius: var(--lp-r-sm);
639
- background: transparent;
640
- color: var(--lp-muted);
641
- cursor: grab;
642
- opacity: 0;
643
- transition: opacity 90ms ease;
644
- }
645
-
646
- .cg-detail-field:hover .cg-detail-grip,
647
- .cg-detail-grip:focus-visible {
648
- opacity: 1;
649
- }
650
-
651
- .cg-detail-grip:focus-visible {
652
- outline: 2px solid var(--lp-blue-deep);
653
- outline-offset: -1px;
654
- }
655
-
656
- .cg-detail-grip:active {
657
- cursor: grabbing;
658
- }
659
-
660
- /* The row under the pointer. It is where the dragged field LANDS (it takes
661
- this slot), so the whole row is marked rather than an insertion line
662
- between two β€” an edge cannot say which side you are on. */
663
- .cg-detail-field.is-dropzone {
664
- border-radius: var(--lp-r-md);
665
- background: var(--lp-blue-tint);
666
- box-shadow: inset 0 0 0 1px var(--lp-blue-deep);
667
- }
668
-
669
- .cg-detail-field.is-dragging {
670
- opacity: 0.4;
671
- }
672
-
673
- /* ==========================================================================
674
- Wave 2026-08-02 item 13 (C-EMBED) β€” Fields | Insights.
675
- ==========================================================================
676
- An underline tab row, not a segmented pill: these are two READINGS of the
677
- same record, and a pill group reads as a mode switch that changes what the
678
- panel is for. Sits under the title, above the scrolling body, so switching
679
- never moves the customer's name. */
680
- .cg-record-tabs {
681
- flex: 0 0 auto;
682
- display: flex;
683
- gap: 2px;
684
- padding: 0 28px;
685
- border-bottom: 1px solid var(--lp-line);
686
- }
687
-
688
- .cg-record-tab {
689
- padding: 7px 10px 8px;
690
- border: 0;
691
- /* The 1px underline lives on the button so the active mark sits ON the row's
692
- hairline rather than 1px above or below it. */
693
- border-bottom: 2px solid transparent;
694
- margin-bottom: -1px;
695
- background: transparent;
696
- color: var(--lp-muted);
697
- font: inherit;
698
- font-size: var(--lp-fs-2xs);
699
- font-weight: 600;
700
- cursor: pointer;
701
- }
702
-
703
- .cg-record-tab:hover {
704
- color: var(--lp-ink);
705
- }
706
-
707
- .cg-record-tab.is-on {
708
- border-bottom-color: var(--lp-blue-solid);
709
- color: var(--lp-ink);
710
- }
711
-
712
- .cg-record-tab:focus-visible {
713
- outline: 2px solid var(--lp-blue-deep);
714
- outline-offset: -2px;
715
- }
716
-
717
- .cg-record-insights {
718
- padding: 12px 16px 24px;
719
- }
720
-
721
- @media (max-width: 760px) {
722
- .cg-record-tabs {
723
- padding-inline: 18px;
724
- }
725
-
726
- .cg-record-insights {
727
- padding-inline: 4px;
728
- }
729
- }
730
-
731
- .cg-record-comments {
732
- min-width: 0;
733
- min-height: 0;
734
- display: flex;
735
- flex-direction: column;
736
- border-left: 1px solid var(--lp-line);
737
- background: var(--lp-wash);
738
- }
739
-
740
- .cg-record-comments-head {
741
- flex: 0 0 52px;
742
- display: flex;
743
- align-items: center;
744
- gap: 8px;
745
- padding: 0 18px;
746
- border-bottom: 1px solid var(--lp-line);
747
- background: var(--lp-surface);
748
- }
749
-
750
- .cg-record-comments-head h2 {
751
- margin: 0;
752
- font-size: var(--lp-fs-sm);
753
- font-weight: 650;
754
- }
755
-
756
- .cg-record-comments-head span {
757
- min-width: 20px;
758
- padding: 1px 6px;
759
- border-radius: var(--lp-r-pill);
760
- background: var(--lp-surface-2);
761
- color: var(--lp-muted);
762
- font-size: var(--lp-fs-3xs);
763
- text-align: center;
764
- }
765
-
766
- .cg-record-comment-feed {
767
- flex: 1 1 auto;
768
- min-height: 0;
769
- overflow-y: auto;
770
- padding: 12px 16px;
771
- }
772
-
773
- .cg-record-comment-state {
774
- padding: 26px 10px;
775
- color: var(--lp-muted);
776
- font-size: var(--lp-fs-2xs);
777
- line-height: 1.5;
778
- text-align: center;
779
- }
780
-
781
- .cg-record-comment {
782
- display: grid;
783
- grid-template-columns: 30px minmax(0, 1fr);
784
- gap: 9px;
785
- padding: 10px 0 13px;
786
- }
787
-
788
- .cg-record-comment + .cg-record-comment {
789
- border-top: 1px solid var(--lp-line);
790
- }
791
-
792
- .cg-record-comment-avatar {
793
- width: 30px;
794
- height: 30px;
795
- display: grid;
796
- place-items: center;
797
- border-radius: 50%;
798
- background: var(--lp-blue-tint);
799
- color: var(--lp-blue-solid);
800
- font-size: var(--lp-fs-4xs);
801
- font-weight: 700;
802
- letter-spacing: 0.01em;
803
- }
804
-
805
- .cg-record-comment-content {
806
- min-width: 0;
807
- }
808
-
809
- .cg-record-comment-meta {
810
- display: flex;
811
- align-items: baseline;
812
- gap: 6px;
813
- }
814
-
815
- .cg-record-comment-meta strong {
816
- min-width: 0;
817
- overflow: hidden;
818
- color: var(--lp-ink);
819
- font-size: var(--lp-fs-2xs);
820
- font-weight: 650;
821
- text-overflow: ellipsis;
822
- white-space: nowrap;
823
- }
824
-
825
- .cg-record-comment-meta time {
826
- flex: 0 0 auto;
827
- color: var(--lp-muted);
828
- font-size: var(--lp-fs-4xs);
829
- }
830
-
831
- .cg-record-comment-delete {
832
- flex: 0 0 auto;
833
- width: 20px;
834
- height: 20px;
835
- margin-left: auto;
836
- border: 0;
837
- border-radius: var(--lp-r-sm);
838
- background: transparent;
839
- color: var(--lp-muted);
840
- cursor: pointer;
841
- }
842
-
843
- .cg-record-comment-delete:hover {
844
- background: var(--lp-red-tint);
845
- color: var(--lp-red-deep);
846
- }
847
-
848
- .cg-record-comment-content p {
849
- margin: 4px 0 0;
850
- color: var(--lp-ink);
851
- font-size: var(--lp-fs-2xs);
852
- line-height: 1.48;
853
- overflow-wrap: anywhere;
854
- white-space: pre-wrap;
855
- }
856
-
857
- .cg-record-comment-error {
858
- margin: 8px 0;
859
- padding: 8px 10px;
860
- border: 1px solid var(--lp-red);
861
- border-radius: var(--lp-r-md);
862
- background: var(--lp-red-tint);
863
- color: var(--lp-red-deep);
864
- font-size: var(--lp-fs-3xs);
865
- line-height: 1.4;
866
- }
867
-
868
- .cg-record-comment-compose {
869
- flex: 0 0 auto;
870
- padding: 12px 14px 14px;
871
- border-top: 1px solid var(--lp-line);
872
- background: var(--lp-surface);
873
- }
874
-
875
- .cg-record-comment-compose label {
876
- display: block;
877
- margin-bottom: 6px;
878
- color: var(--lp-ink);
879
- font-size: var(--lp-fs-2xs);
880
- font-weight: 600;
881
- }
882
-
883
- .cg-record-comment-compose textarea {
884
- width: 100%;
885
- min-height: 78px;
886
- max-height: 180px;
887
- resize: vertical;
888
- padding: 9px 10px;
889
- border: 1px solid var(--lp-line);
890
- border-radius: var(--lp-r-md);
891
- background: var(--lp-surface);
892
- color: var(--lp-ink);
893
- font: inherit;
894
- font-size: var(--lp-fs-2xs);
895
- line-height: 1.45;
896
- }
897
-
898
- .cg-record-comment-compose textarea:focus {
899
- outline: 2px solid var(--lp-blue-deep);
900
- outline-offset: -1px;
901
- border-color: transparent;
902
- }
903
-
904
- .cg-record-comment-compose-foot {
905
- display: flex;
906
- align-items: center;
907
- justify-content: space-between;
908
- gap: 8px;
909
- margin-top: 7px;
910
- }
911
-
912
- .cg-record-comment-compose-foot span {
913
- color: var(--lp-muted);
914
- font-size: var(--lp-fs-4xs);
915
- }
916
-
917
- .cg-record-comment-compose-foot button {
918
- min-width: 76px;
919
- height: 28px;
920
- border: 1px solid var(--lp-blue-solid);
921
- border-radius: var(--lp-r-md);
922
- background: var(--lp-blue-solid);
923
- color: white;
924
- font: inherit;
925
- font-size: var(--lp-fs-2xs);
926
- font-weight: 600;
927
- cursor: pointer;
928
- }
929
-
930
- .cg-record-comment-compose-foot button:disabled {
931
- opacity: 0.48;
932
- cursor: default;
933
- }
934
-
935
- @media (max-width: 760px) {
936
- .cg-record-backdrop {
937
- padding: 0;
938
- }
939
-
940
- .cg-record-modal,
941
- .cg-record-modal--single {
942
- width: 100vw;
943
- height: 100dvh;
944
- min-height: 0;
945
- border: 0;
946
- border-radius: 0;
947
- }
948
-
949
- .cg-record-layout {
950
- grid-template-columns: minmax(0, 1fr);
951
- grid-template-rows: minmax(220px, 1.2fr) minmax(250px, 0.8fr);
952
- }
953
-
954
- .cg-record-comments {
955
- border-top: 1px solid var(--lp-line);
956
- border-left: 0;
957
- }
958
-
959
- .cg-record-main .cg-detail-titlewrap {
960
- padding: 16px 18px 14px;
961
- }
962
-
963
- .cg-record-main .cg-detail-body {
964
- padding-inline: 2px;
965
- }
966
-
967
- /* Both selectors, because the editable row's label is set at (0,3,0) above:
968
- narrowing only the read-only one would step the two label columns apart at
969
- exactly the width where the misalignment is most visible. */
970
- .cg-record-main .cg-detail-label,
971
- .cg-record-main .cg-detail-field--edit .cg-detail-label {
972
- flex-basis: 120px;
973
- }
974
- }
975
-
976
- /* ==========================================================================
977
- Wave 14 R7 β€” one line, no wraps: the record modal's button rows.
978
- ==========================================================================
979
- Audited every row of controls this session renders. Two were real risks and
980
- the rest were already sound, which is worth recording so the next sweep does
981
- not re-derive it:
982
-
983
- Β· the header (β€Ή β€Ί Β· "Record 3 of 1,234" Β· Γ—) β€” the buttons are fixed
984
- 28px squares, but the POSITION is variable text in a 44/46px fixed-height
985
- row. At a narrow width it wrapped to two lines and pushed itself out of
986
- the header it lives in.
987
- Β· the comment composer's foot β€” a character counter opposite a Post
988
- button under `space-between`; the counter wrapped before the button
989
- shrank, which moved the button off its baseline.
990
- Β· Documents rows were ALREADY correct (index.css: name ellipsises,
991
- meta and delete are `flex: 0 0 auto`) β€” left alone.
992
- Β· the tab row, the chip rows and the star row are new this wave and were
993
- built nowrap; the declarations below are what make that a rule rather
994
- than a coincidence of the current labels.
995
-
996
- `nowrap` is the assertion. A label that outgrows its space now overflows
997
- VISIBLY instead of quietly reflowing β€” the failure you can see beats the one
998
- you cannot. */
999
- .cg-record-modal .cg-detail-pos,
1000
- .cg-record-tab,
1001
- .cg-record-main .cg-detail-chip,
1002
- .cg-record-comment-compose-foot button {
1003
- white-space: nowrap;
1004
- }
1005
-
1006
- .cg-record-comment-compose-foot span {
1007
- min-width: 0;
1008
- overflow: hidden;
1009
- text-overflow: ellipsis;
1010
- white-space: nowrap;
1011
- }
1012
-
1013
- .cg-record-comment-compose-foot button {
1014
- flex: 0 0 auto;
1015
- }
1016
-
1017
- @media (prefers-reduced-motion: reduce) {
1018
- .cg-record-modal {
1019
- animation: none;
1020
- }
1021
- }
 
 
 
 
 
 
 
1
+ .cg-record-backdrop {
2
+ position: fixed;
3
+ inset: 0;
4
+ /* ⭐⭐ OWNER ITEM 11 (2026-08-23) β€” A RECORD IS ALWAYS ON TOP. Owner: *"the Detail records
5
+ when clicked should ALWAYS be at the top of every other navigation pop-up."* At
6
+ `--cg-z-drawer` (70) it lost to the Map's route planner (95), so clicking a pin opened the
7
+ record BEHIND the panel the click came from. `--cg-z-record` (100) is above every shell and
8
+ surface layer; the portal'd anchored-overlay step sits above it on purpose, so a menu opened
9
+ INSIDE this dialog still paints over it. */
10
+ z-index: var(--cg-z-record);
11
+ display: grid;
12
+ place-items: center;
13
+ padding: 24px;
14
+ background: rgba(25, 34, 46, 0.34);
15
+ backdrop-filter: blur(1px);
16
+ }
17
+
18
+ .cg-record-modal {
19
+ width: min(1120px, calc(100vw - 48px));
20
+ height: min(760px, calc(100vh - 48px));
21
+ min-height: 420px;
22
+ display: flex;
23
+ flex-direction: column;
24
+ overflow: hidden;
25
+ border: 1px solid color-mix(in srgb, var(--lp-line) 82%, var(--lp-muted));
26
+ border-radius: 12px;
27
+ background: var(--lp-surface);
28
+ box-shadow:
29
+ 0 30px 80px rgba(20, 30, 43, 0.24),
30
+ 0 4px 18px rgba(20, 30, 43, 0.12);
31
+ color: var(--lp-ink);
32
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
33
+ font-size: var(--lp-fs-xs);
34
+ animation: cg-record-open 150ms cubic-bezier(0.2, 0, 0, 1);
35
+ }
36
+
37
+ .cg-record-modal:focus {
38
+ outline: none;
39
+ }
40
+
41
+ /* Universal linked-record viewer: the target is the real Grid surface, not a JSON-shaped
42
+ approximation. It is deliberately wider than the record detail so filtering and multi-column
43
+ sorting have the same working room they do on the database page. */
44
+ .cg-link-grid-modal {
45
+ width: min(1320px, calc(100vw - 48px));
46
+ height: min(840px, calc(100vh - 48px));
47
+ min-height: 460px;
48
+ display: flex;
49
+ flex-direction: column;
50
+ overflow: hidden;
51
+ border: 1px solid color-mix(in srgb, var(--lp-line) 82%, var(--lp-muted));
52
+ border-radius: 12px;
53
+ background: var(--lp-surface);
54
+ box-shadow:
55
+ 0 30px 80px rgba(20, 30, 43, 0.24),
56
+ 0 4px 18px rgba(20, 30, 43, 0.12);
57
+ color: var(--lp-ink);
58
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
59
+ font-size: var(--lp-fs-xs);
60
+ animation: cg-record-open 150ms cubic-bezier(0.2, 0, 0, 1);
61
+ }
62
+
63
+ .cg-link-grid-modal:focus { outline: none; }
64
+
65
+ .cg-link-grid-head {
66
+ flex: 0 0 54px;
67
+ min-height: 54px;
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: space-between;
71
+ gap: 16px;
72
+ padding: 0 14px 0 18px;
73
+ border-bottom: 1px solid var(--lp-line);
74
+ background: var(--lp-surface);
75
+ }
76
+
77
+ .cg-link-grid-title {
78
+ font-size: var(--lp-fs-sm);
79
+ font-weight: 650;
80
+ line-height: 1.2;
81
+ }
82
+
83
+ .cg-link-grid-sub {
84
+ margin-top: 2px;
85
+ color: var(--lp-muted);
86
+ font-size: var(--lp-fs-3xs);
87
+ }
88
+
89
+ .cg-link-grid-body {
90
+ flex: 1 1 auto;
91
+ min-width: 0;
92
+ min-height: 0;
93
+ background: var(--lp-surface);
94
+ }
95
+
96
+ .cg-link-grid-foot {
97
+ flex: 0 0 52px;
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: flex-end;
101
+ gap: 8px;
102
+ padding: 8px 14px;
103
+ border-top: 1px solid var(--lp-line);
104
+ background: var(--lp-surface);
105
+ }
106
+
107
+ @keyframes cg-record-open {
108
+ from {
109
+ opacity: 0;
110
+ transform: translateY(8px) scale(0.992);
111
+ }
112
+ }
113
+
114
+ .cg-record-modal > .cg-detail-header {
115
+ height: 46px;
116
+ padding: 0 12px;
117
+ background: var(--lp-surface);
118
+ }
119
+
120
+ .cg-record-layout {
121
+ flex: 1 1 auto;
122
+ min-height: 0;
123
+ display: grid;
124
+ grid-template-columns: minmax(0, 1.95fr) minmax(300px, 0.95fr);
125
+ }
126
+
127
+ .cg-record-modal--single {
128
+ width: min(880px, calc(100vw - 48px));
129
+ }
130
+
131
+ .cg-record-modal--single .cg-record-layout {
132
+ grid-template-columns: minmax(0, 1fr);
133
+ }
134
+
135
+ .cg-record-main {
136
+ min-width: 0;
137
+ min-height: 0;
138
+ display: flex;
139
+ flex-direction: column;
140
+ }
141
+
142
+ .cg-record-main .cg-detail-titlewrap {
143
+ padding: 22px 28px 18px;
144
+ }
145
+
146
+ .cg-record-main .cg-detail-title {
147
+ font-size: var(--lp-fs-lg);
148
+ line-height: 1.25;
149
+ }
150
+
151
+ .cg-record-main .cg-detail-body {
152
+ min-height: 0;
153
+ padding: 4px 12px 28px;
154
+ scrollbar-gutter: stable;
155
+ }
156
+
157
+ .cg-record-main .cg-detail-section {
158
+ padding: 16px;
159
+ }
160
+
161
+ /* ==========================================================================
162
+ Wave 2026-08-02 item 2 β€” the record fields: no yellow, one subtle box.
163
+ ==========================================================================
164
+ Owner: "kill yellow, subtle outline for all field types."
165
+
166
+ Before this, exactly one kind of field had a treatment β€” the editable
167
+ overlay notes, in a gold frame on a gold wash (index.css:2027) β€” and every
168
+ other field was bare text under a hairline. So the loudest thing in the
169
+ panel was also the least important, and the panel had two layouts: label
170
+ ABOVE a full-width textarea for notes, label BESIDE the value for the rest.
171
+
172
+ Now every field is one row on one grid: label left, a value SLOT right.
173
+ The slot carries a 1px --lp-line hairline and --lp-r-md corners, and the
174
+ fill is what says whether it is yours:
175
+
176
+ --lp-surface-2 (warm grey) read-only β€” an Odoo attribute, a picked
177
+ choice, a star rating you set from the table
178
+ --lp-surface (white) editable β€” type here
179
+
180
+ That replaces the signal the yellow wash used to carry (this stratum is
181
+ your data, not the ERP's) with one that is also an affordance: the bright
182
+ slots are the ones that respond. Interaction, not decoration, marks them.
183
+
184
+ EVERY rule here is scoped `.cg-record-main …` so it (a) outranks the
185
+ index.css base block, which is FROZEN for the other sessions this wave,
186
+ and (b) stops at the record modal β€” the same `.cg-detail-*` classes dress
187
+ surfaces this file does not own. Specificity, never source order: the
188
+ :focus and ::placeholder overrides below exist because their index.css
189
+ counterparts tie at (0,2,0) and (0,1,1), and a tie is decided by bundle
190
+ order β€” which is not a thing to bet a gold glow on. */
191
+
192
+ .cg-record-main .cg-detail-field {
193
+ min-height: 0;
194
+ align-items: flex-start;
195
+ gap: 12px;
196
+ padding: 3px 0;
197
+ /* The hairline separator is REPLACED by the box, not stacked under it β€”
198
+ a ruled row around a bordered slot reads as a rendering fault. */
199
+ border-bottom: none;
200
+ }
201
+
202
+ .cg-record-main .cg-detail-label {
203
+ flex: 0 0 160px;
204
+ /* Optical, not arithmetic: 7px lands the 12px label on the same line as the
205
+ 13px value inside its 1px border + 5px padding. */
206
+ padding-top: 7px;
207
+ color: var(--lp-muted);
208
+ font-size: var(--lp-fs-2xs);
209
+ line-height: 1.45;
210
+ }
211
+
212
+ .cg-record-main .cg-detail-value {
213
+ min-height: 30px;
214
+ padding: 5px 9px;
215
+ border: 1px solid var(--lp-line);
216
+ border-radius: var(--lp-r-md);
217
+ background: var(--lp-surface-2);
218
+ line-height: 1.45;
219
+ }
220
+
221
+ /* ⭐ Wave-23 C8 (owner item 4 / ruling R9) β€” a MACHINE-OWNED field's value slot.
222
+
223
+ `--lp-machine-tint` is EXACTLY what the canvas wash (`theme.MACHINE_CELL`,
224
+ `washOf(LP_MACHINE_DEEP, 0.12)`) composites to over white, so the drawer slot
225
+ and the grid cell are one grey rather than two that were each picked to look
226
+ right on their own β€” the sibling-panel rule (DESIGN.md Β§2) applied across two
227
+ rendering technologies instead of two panes.
228
+
229
+ THE VALUE KEEPS ITS INK, deliberately, on both surfaces. The wash is the whole
230
+ signal; dimming the number as well would say the figure matters less, and an
231
+ automation-written follower count is the entire reason its column exists.
232
+
233
+ Background only β€” no border change, no second marker. The column menu already
234
+ says "Filled by automation" once, and DESIGN.md Β§4 is that the app does not
235
+ narrate itself twice. */
236
+ .cg-record-main .cg-detail-field.is-machine .cg-detail-value {
237
+ background: var(--lp-machine-tint);
238
+ }
239
+
240
+ /* A rating's marks are 18px inline SVG; on the text baseline they hang their
241
+ descender space below the line box and grow the slot past every neighbour. */
242
+ .cg-record-main .cg-detail-value .cg-stars {
243
+ vertical-align: middle;
244
+ }
245
+
246
+ /* An editable field is the same ROW as every other one now β€” index.css stacks
247
+ it (flex-direction: column, and a label with `flex: none`), which is what
248
+ made the notes the odd block out. */
249
+ .cg-record-main .cg-detail-field--edit {
250
+ flex-direction: row;
251
+ align-items: flex-start;
252
+ gap: 12px;
253
+ }
254
+
255
+ .cg-record-main .cg-detail-field--edit .cg-detail-label {
256
+ flex: 0 0 160px;
257
+ }
258
+
259
+ /* The value column of an editable row: the box, then its action link. */
260
+ .cg-detail-editwrap {
261
+ flex: 1 1 auto;
262
+ min-width: 0;
263
+ display: flex;
264
+ flex-direction: column;
265
+ }
266
+
267
+ .cg-record-main .cg-detail-textarea {
268
+ /* The owner's "big static box" was min-height: 76px. One row is the resting
269
+ size; AutoTextarea grows it from there, so an empty note is as quiet as an
270
+ empty value and a long one is readable without a scrollbar. */
271
+ min-height: 30px;
272
+ max-height: 260px;
273
+ padding: 5px 9px;
274
+ overflow-y: auto;
275
+ /* The height is measured, so a drag handle would fight the measurement: the
276
+ next keystroke re-fits and snaps back whatever was dragged. */
277
+ resize: none;
278
+ border: 1px solid var(--lp-line);
279
+ background: var(--lp-surface);
280
+ line-height: 1.45;
281
+ }
282
+
283
+ .cg-record-main .cg-detail-textarea:hover:not(:focus) {
284
+ border-color: color-mix(in srgb, var(--lp-line) 45%, var(--lp-muted));
285
+ }
286
+
287
+ .cg-record-main .cg-detail-textarea:focus {
288
+ /* Identical to the comment composer below β€” same modal, same gesture, so the
289
+ same mark. `box-shadow: none` is load-bearing: without it index.css:2047's
290
+ 3px gold glow survives an override that only changes the border. */
291
+ outline: 2px solid var(--lp-blue-deep);
292
+ outline-offset: -1px;
293
+ border-color: transparent;
294
+ box-shadow: none;
295
+ }
296
+
297
+ .cg-record-main .cg-detail-textarea::placeholder {
298
+ /* --lp-muted is the palette's body-safe grey (4.83:1). The stock #a2a2ac was
299
+ picked against the gold wash and reads as 2.7:1 on white β€” and "Add
300
+ notes…" is an instruction, not decoration. */
301
+ color: var(--lp-muted);
302
+ opacity: 1;
303
+ }
304
+
305
+ /* A checkbox IS its own control; boxing it would frame a 15px square in a
306
+ 30px slot and say nothing. It only needs to sit on the label's line. */
307
+ .cg-record-main .cg-detail-checkbox {
308
+ margin-top: 7px;
309
+ }
310
+
311
+ /* ==========================================================================
312
+ Wave 14 item 9 β€” an editor for EVERY user-created field type.
313
+ ==========================================================================
314
+ Until now one type was editable here (notes) and the other eight read. The
315
+ affordance law item 2 established is what makes this scale without a legend:
316
+
317
+ --lp-surface-2 (warm grey) read-only
318
+ --lp-surface (white) yours to change
319
+
320
+ so `--live` is the ONLY thing that says "this responds", across nine row
321
+ shapes. It is deliberately the same white the textarea already used, not a
322
+ new colour: adding a third fill would turn a two-state signal into a puzzle.
323
+
324
+ ONE BOX PER ROW. The controls are borderless and transparent INSIDE the
325
+ slot rather than bordered themselves β€” a bordered input inside a bordered
326
+ slot draws two rectangles 1px apart, which reads as a rendering fault, and
327
+ it would also break the single label-left/value-right grid every other row
328
+ sits on. The focus ring therefore lands on the SLOT (`:focus-within`) and
329
+ the control's own outline is suppressed; the ring is the same 2px inset
330
+ blue as the textarea and the comment composer, because it is the same
331
+ gesture in the same modal.
332
+
333
+ ⚠ `font: inherit` is NOT used on these controls. The `font` shorthand
334
+ RESETS `font-variant-numeric`, which `.cg-detail-value` sets to tabular-nums
335
+ (index.css:2013) β€” so the shorthand would silently un-align every number in
336
+ the panel. The longhands are spelled out instead. */
337
+ .cg-record-main .cg-detail-value--live {
338
+ background: var(--lp-surface);
339
+ }
340
+
341
+ .cg-record-main .cg-detail-value--live:hover:not(:focus-within) {
342
+ border-color: color-mix(in srgb, var(--lp-line) 45%, var(--lp-muted));
343
+ }
344
+
345
+ .cg-record-main .cg-detail-value--live:focus-within {
346
+ outline: 2px solid var(--lp-blue-deep);
347
+ outline-offset: -1px;
348
+ border-color: transparent;
349
+ }
350
+
351
+ /* The slot as a ROW: an avatar, a currency mark or a hint sits beside its
352
+ control instead of above it. */
353
+ .cg-record-main .cg-detail-pickslot {
354
+ display: flex;
355
+ align-items: center;
356
+ gap: 7px;
357
+ }
358
+
359
+ .cg-record-main .cg-detail-select,
360
+ .cg-record-main .cg-detail-input {
361
+ flex: 1 1 auto;
362
+ min-width: 0;
363
+ width: 100%;
364
+ padding: 0;
365
+ border: 0;
366
+ background: transparent;
367
+ color: var(--lp-ink);
368
+ font-family: inherit;
369
+ font-size: inherit;
370
+ font-weight: inherit;
371
+ line-height: 1.45;
372
+ }
373
+
374
+ /* The ring is on the slot; a second one inside it would draw a rectangle
375
+ around a rectangle. */
376
+ .cg-record-main .cg-detail-select:focus,
377
+ .cg-record-main .cg-detail-input:focus {
378
+ outline: none;
379
+ }
380
+
381
+ .cg-record-main .cg-detail-select {
382
+ /* The native arrow is kept β€” it is the only thing that says "pick" once the
383
+ border has been handed to the slot. */
384
+ cursor: pointer;
385
+ margin-left: -2px;
386
+ }
387
+
388
+ .cg-record-main .cg-detail-input--num {
389
+ text-align: right;
390
+ }
391
+
392
+ /* Spinners removed, and not for looks: this panel SCROLLS, and a wheel event
393
+ over a focused number input changes the value in Chrome. A currency figure
394
+ silently edited by scrolling past it is data loss with no gesture behind it.
395
+ The right-aligned tabular figure and the unit mark carry the type. */
396
+ .cg-record-main .cg-detail-input--num::-webkit-outer-spin-button,
397
+ .cg-record-main .cg-detail-input--num::-webkit-inner-spin-button {
398
+ appearance: none;
399
+ margin: 0;
400
+ }
401
+
402
+ .cg-record-main .cg-detail-input--num {
403
+ -moz-appearance: textfield;
404
+ appearance: textfield;
405
+ }
406
+
407
+ .cg-record-main .cg-detail-unit,
408
+ .cg-record-main .cg-detail-hint {
409
+ flex: 0 0 auto;
410
+ color: var(--lp-muted);
411
+ font-size: var(--lp-fs-2xs);
412
+ }
413
+
414
+ .cg-record-main .cg-detail-hint {
415
+ font-style: italic;
416
+ }
417
+
418
+ .cg-record-main .cg-detail-nochoice {
419
+ color: var(--lp-muted);
420
+ font-size: var(--lp-fs-2xs);
421
+ }
422
+
423
+ /* Multiselect: a chip row, not a `<select multiple>`. The native control needs
424
+ ctrl-click for a second value and paints a four-row scrollbox for a
425
+ three-choice field; a chip says its own state and takes one click. */
426
+ .cg-record-main .cg-detail-chips {
427
+ display: flex;
428
+ flex-wrap: wrap;
429
+ gap: 5px;
430
+ }
431
+
432
+ .cg-record-main .cg-detail-chip {
433
+ height: 22px;
434
+ padding: 0 9px;
435
+ border: 1px solid var(--lp-line);
436
+ border-radius: var(--lp-r-pill);
437
+ background: var(--lp-surface-2);
438
+ color: var(--lp-muted);
439
+ font-family: inherit;
440
+ font-size: var(--lp-fs-2xs);
441
+ line-height: 1;
442
+ white-space: nowrap;
443
+ cursor: pointer;
444
+ }
445
+
446
+ .cg-record-main .cg-detail-chip:hover {
447
+ border-color: var(--lp-blue-deep);
448
+ }
449
+
450
+ /* Tint fills, `-deep` on the text β€” the brand rule. The base pastel is not a
451
+ readable ink at 12px. */
452
+ .cg-record-main .cg-detail-chip.is-on {
453
+ border-color: var(--lp-blue-deep);
454
+ background: var(--lp-blue-tint);
455
+ color: var(--lp-blue-deep);
456
+ font-weight: 600;
457
+ }
458
+
459
+ .cg-record-main .cg-detail-chip:focus-visible {
460
+ outline: 2px solid var(--lp-blue-deep);
461
+ outline-offset: 1px;
462
+ }
463
+
464
+ /* ⭐ Wave-19 R7 β€” the IMAGE slot: the picture, then its reference and controls.
465
+ The thumbnail is `object-fit: contain` for the reason the grid cell is: a
466
+ product photo cropped to a square loses the thing being sold. Letterboxing
467
+ inside a bounded frame is the honest fit for a catalogue picture. */
468
+ .cg-record-main .cg-detail-imageslot {
469
+ display: flex;
470
+ align-items: flex-start;
471
+ gap: 10px;
472
+ }
473
+
474
+ .cg-record-main .cg-detail-image,
475
+ .cg-record-main .cg-detail-imageempty {
476
+ flex: 0 0 auto;
477
+ width: 72px;
478
+ height: 72px;
479
+ border: 1px solid var(--lp-line);
480
+ border-radius: var(--lp-r-sm);
481
+ background: var(--lp-surface-2);
482
+ object-fit: contain;
483
+ }
484
+
485
+ .cg-record-main .cg-detail-imagemeta {
486
+ display: flex;
487
+ flex-direction: column;
488
+ align-items: flex-start;
489
+ gap: 5px;
490
+ min-width: 0;
491
+ }
492
+
493
+ /* The reference itself, in the mono face: it is an identifier (a SKU code, a
494
+ slug, a minted id), and identifiers are read character by character. The
495
+ stack is spelled out rather than tokenised because there is no `--lp-mono`
496
+ token β€” the three existing monospace sites in index.css all spell it too, and
497
+ C6 says NO new tokens this wave. */
498
+ .cg-record-main .cg-detail-imageref {
499
+ max-width: 100%;
500
+ overflow: hidden;
501
+ text-overflow: ellipsis;
502
+ white-space: nowrap;
503
+ color: var(--lp-muted);
504
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
505
+ font-size: var(--lp-fs-2xs);
506
+ }
507
+
508
+ .cg-record-main .cg-detail-imageacts {
509
+ display: inline-flex;
510
+ gap: 6px;
511
+ }
512
+
513
+ /* The `<label>` and the `<button>` are one control visually β€” the file input
514
+ inside the label is hidden, which is the only way to style a file picker. */
515
+ .cg-record-main .cg-detail-imagebtn {
516
+ display: inline-flex;
517
+ align-items: center;
518
+ height: 22px;
519
+ padding: 0 9px;
520
+ border: 1px solid var(--lp-line);
521
+ border-radius: var(--lp-r-pill);
522
+ background: var(--lp-surface-2);
523
+ color: var(--lp-muted);
524
+ font-family: inherit;
525
+ font-size: var(--lp-fs-2xs);
526
+ line-height: 1;
527
+ white-space: nowrap;
528
+ cursor: pointer;
529
+ }
530
+
531
+ .cg-record-main .cg-detail-imagebtn:hover {
532
+ border-color: var(--lp-blue-deep);
533
+ color: var(--lp-blue-deep);
534
+ }
535
+
536
+ .cg-record-main .cg-detail-imagebtn input[type="file"] {
537
+ display: none;
538
+ }
539
+
540
+ .cg-record-main .cg-detail-imageerr {
541
+ color: var(--lp-red-deep);
542
+ font-size: var(--lp-fs-2xs);
543
+ }
544
+
545
+ /* ⭐ Wave-23 C7 (owner item 5) β€” a `json` field's row: the same compact preview the grid cell
546
+ paints, plus the door to the viewer. Reuses `.cg-detail-imagebtn` rather than minting a
547
+ second quiet-button shape (DESIGN.md Β§4) β€” the two rows are doing the same job, showing a
548
+ value too big for a slot and offering the surface that owns it. */
549
+ .cg-record-main .cg-detail-jsonslot {
550
+ display: flex;
551
+ align-items: center;
552
+ justify-content: space-between;
553
+ gap: 10px;
554
+ }
555
+
556
+ /* ⚠ `min-width: 0` is what lets the ellipsis happen at all: a flex item's default
557
+ `min-width: auto` refuses to shrink below its content, so the preview would push the Open
558
+ button out of the panel instead of truncating. Monospace because the preview is a fragment
559
+ of a document β€” `{handle: "royalimports"}` reads as code, and setting it in Inter beside the
560
+ viewer's monospace tree would make one value look like two different things. */
561
+ .cg-record-main .cg-detail-jsonpreview {
562
+ flex: 1 1 auto;
563
+ min-width: 0;
564
+ overflow: hidden;
565
+ text-overflow: ellipsis;
566
+ white-space: nowrap;
567
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
568
+ font-size: var(--lp-fs-2xs);
569
+ color: var(--lp-ink);
570
+ }
571
+
572
+ /* Rating: the stars ARE the picker. Clicking the lit star clears the value β€”
573
+ without it a rating could be changed here but never removed. */
574
+ .cg-record-main .cg-detail-stars {
575
+ display: inline-flex;
576
+ align-items: center;
577
+ gap: 1px;
578
+ }
579
+
580
+ .cg-record-main .cg-detail-star {
581
+ display: grid;
582
+ place-items: center;
583
+ padding: 0 1px;
584
+ border: 0;
585
+ background: transparent;
586
+ color: inherit;
587
+ cursor: pointer;
588
+ line-height: 0;
589
+ }
590
+
591
+ .cg-record-main .cg-detail-star:focus-visible {
592
+ outline: 2px solid var(--lp-blue-deep);
593
+ outline-offset: 1px;
594
+ border-radius: var(--lp-r-sm);
595
+ }
596
+
597
+ /* ==========================================================================
598
+ Wave 14 item 11 (C-AVATAR) β€” the person, as a face.
599
+ ==========================================================================
600
+ R6: cells show the disc alone; here it sits BESIDE the name, because two
601
+ initials at 20px identify nobody by themselves. The initials pastel is
602
+ `pickTint`'s hash β€” the same one the option pills use β€” so one person is one
603
+ colour on every surface, and it is the DESIGNED state rather than the error
604
+ state: every deployment renders it until a photo is uploaded. */
605
+ .cg-record-main .cg-detail-avatar {
606
+ flex: 0 0 auto;
607
+ width: 20px;
608
+ height: 20px;
609
+ display: inline-grid;
610
+ place-items: center;
611
+ border-radius: 50%;
612
+ object-fit: cover;
613
+ font-size: var(--lp-fs-4xs);
614
+ font-weight: 700;
615
+ letter-spacing: 0.01em;
616
+ line-height: 1;
617
+ }
618
+
619
+ /* ==========================================================================
620
+ Wave 2026-08-02 item 5 (C-LAYOUT) β€” the six-dot reorder handle.
621
+ ==========================================================================
622
+ Hover-revealed, Airtable's own behaviour and the reason it is right here:
623
+ this panel carries ~26 fields, and 26 permanently-visible grips would put a
624
+ dotted column down the left edge louder than any value in the record. The
625
+ handle is an affordance for a gesture, not information.
626
+
627
+ It keeps its 16px gutter at rest (`opacity`, never `display`), so revealing
628
+ it cannot reflow the row under the pointer that is reaching for it β€” and it
629
+ stays in the tab order at all times, which is what makes the ArrowUp/Down
630
+ fallback reachable without a mouse.
631
+
632
+ ⚠ Known gap, booked rather than papered over: HTML5 drag-and-drop does not
633
+ fire for touch. On a touch device there is no hover, so the grip stays
634
+ invisible and the gesture is simply absent β€” which is the honest
635
+ degradation. A pointer-events reorder is the fix if it is ever asked for. */
636
+ .cg-detail-grip {
637
+ flex: 0 0 16px;
638
+ height: 22px;
639
+ margin-top: 5px;
640
+ display: grid;
641
+ place-items: center;
642
+ padding: 0;
643
+ border: 0;
644
+ border-radius: var(--lp-r-sm);
645
+ background: transparent;
646
+ color: var(--lp-muted);
647
+ cursor: grab;
648
+ opacity: 0;
649
+ transition: opacity 90ms ease;
650
+ }
651
+
652
+ .cg-detail-field:hover .cg-detail-grip,
653
+ .cg-detail-grip:focus-visible {
654
+ opacity: 1;
655
+ }
656
+
657
+ .cg-detail-grip:focus-visible {
658
+ outline: 2px solid var(--lp-blue-deep);
659
+ outline-offset: -1px;
660
+ }
661
+
662
+ .cg-detail-grip:active {
663
+ cursor: grabbing;
664
+ }
665
+
666
+ /* The row under the pointer. It is where the dragged field LANDS (it takes
667
+ this slot), so the whole row is marked rather than an insertion line
668
+ between two β€” an edge cannot say which side you are on. */
669
+ .cg-detail-field.is-dropzone {
670
+ border-radius: var(--lp-r-md);
671
+ background: var(--lp-blue-tint);
672
+ box-shadow: inset 0 0 0 1px var(--lp-blue-deep);
673
+ }
674
+
675
+ .cg-detail-field.is-dragging {
676
+ opacity: 0.4;
677
+ }
678
+
679
+ /* ==========================================================================
680
+ Wave 2026-08-02 item 13 (C-EMBED) β€” Fields | Insights.
681
+ ==========================================================================
682
+ An underline tab row, not a segmented pill: these are two READINGS of the
683
+ same record, and a pill group reads as a mode switch that changes what the
684
+ panel is for. Sits under the title, above the scrolling body, so switching
685
+ never moves the customer's name. */
686
+ .cg-record-tabs {
687
+ flex: 0 0 auto;
688
+ display: flex;
689
+ gap: 2px;
690
+ padding: 0 28px;
691
+ border-bottom: 1px solid var(--lp-line);
692
+ }
693
+
694
+ .cg-record-tab {
695
+ padding: 7px 10px 8px;
696
+ border: 0;
697
+ /* The 1px underline lives on the button so the active mark sits ON the row's
698
+ hairline rather than 1px above or below it. */
699
+ border-bottom: 2px solid transparent;
700
+ margin-bottom: -1px;
701
+ background: transparent;
702
+ color: var(--lp-muted);
703
+ font: inherit;
704
+ font-size: var(--lp-fs-2xs);
705
+ font-weight: 600;
706
+ cursor: pointer;
707
+ }
708
+
709
+ .cg-record-tab:hover {
710
+ color: var(--lp-ink);
711
+ }
712
+
713
+ .cg-record-tab.is-on {
714
+ border-bottom-color: var(--lp-blue-solid);
715
+ color: var(--lp-ink);
716
+ }
717
+
718
+ .cg-record-tab:focus-visible {
719
+ outline: 2px solid var(--lp-blue-deep);
720
+ outline-offset: -2px;
721
+ }
722
+
723
+ .cg-record-insights {
724
+ padding: 12px 16px 24px;
725
+ }
726
+
727
+ @media (max-width: 760px) {
728
+ .cg-record-tabs {
729
+ padding-inline: 18px;
730
+ }
731
+
732
+ .cg-record-insights {
733
+ padding-inline: 4px;
734
+ }
735
+ }
736
+
737
+ .cg-record-comments {
738
+ min-width: 0;
739
+ min-height: 0;
740
+ display: flex;
741
+ flex-direction: column;
742
+ border-left: 1px solid var(--lp-line);
743
+ background: var(--lp-wash);
744
+ }
745
+
746
+ .cg-record-comments-head {
747
+ flex: 0 0 52px;
748
+ display: flex;
749
+ align-items: center;
750
+ gap: 8px;
751
+ padding: 0 18px;
752
+ border-bottom: 1px solid var(--lp-line);
753
+ background: var(--lp-surface);
754
+ }
755
+
756
+ .cg-record-comments-head h2 {
757
+ margin: 0;
758
+ font-size: var(--lp-fs-sm);
759
+ font-weight: 650;
760
+ }
761
+
762
+ .cg-record-comments-head span {
763
+ min-width: 20px;
764
+ padding: 1px 6px;
765
+ border-radius: var(--lp-r-pill);
766
+ background: var(--lp-surface-2);
767
+ color: var(--lp-muted);
768
+ font-size: var(--lp-fs-3xs);
769
+ text-align: center;
770
+ }
771
+
772
+ .cg-record-comment-feed {
773
+ flex: 1 1 auto;
774
+ min-height: 0;
775
+ overflow-y: auto;
776
+ padding: 12px 16px;
777
+ }
778
+
779
+ .cg-record-comment-state {
780
+ padding: 26px 10px;
781
+ color: var(--lp-muted);
782
+ font-size: var(--lp-fs-2xs);
783
+ line-height: 1.5;
784
+ text-align: center;
785
+ }
786
+
787
+ .cg-record-comment {
788
+ display: grid;
789
+ grid-template-columns: 30px minmax(0, 1fr);
790
+ gap: 9px;
791
+ padding: 10px 0 13px;
792
+ }
793
+
794
+ .cg-record-comment + .cg-record-comment {
795
+ border-top: 1px solid var(--lp-line);
796
+ }
797
+
798
+ .cg-record-comment-avatar {
799
+ width: 30px;
800
+ height: 30px;
801
+ display: grid;
802
+ place-items: center;
803
+ border-radius: 50%;
804
+ background: var(--lp-blue-tint);
805
+ color: var(--lp-blue-solid);
806
+ font-size: var(--lp-fs-4xs);
807
+ font-weight: 700;
808
+ letter-spacing: 0.01em;
809
+ }
810
+
811
+ .cg-record-comment-content {
812
+ min-width: 0;
813
+ }
814
+
815
+ .cg-record-comment-meta {
816
+ display: flex;
817
+ align-items: baseline;
818
+ gap: 6px;
819
+ }
820
+
821
+ .cg-record-comment-meta strong {
822
+ min-width: 0;
823
+ overflow: hidden;
824
+ color: var(--lp-ink);
825
+ font-size: var(--lp-fs-2xs);
826
+ font-weight: 650;
827
+ text-overflow: ellipsis;
828
+ white-space: nowrap;
829
+ }
830
+
831
+ .cg-record-comment-meta time {
832
+ flex: 0 0 auto;
833
+ color: var(--lp-muted);
834
+ font-size: var(--lp-fs-4xs);
835
+ }
836
+
837
+ .cg-record-comment-delete {
838
+ flex: 0 0 auto;
839
+ width: 20px;
840
+ height: 20px;
841
+ margin-left: auto;
842
+ border: 0;
843
+ border-radius: var(--lp-r-sm);
844
+ background: transparent;
845
+ color: var(--lp-muted);
846
+ cursor: pointer;
847
+ }
848
+
849
+ .cg-record-comment-delete:hover {
850
+ background: var(--lp-red-tint);
851
+ color: var(--lp-red-deep);
852
+ }
853
+
854
+ .cg-record-comment-content p {
855
+ margin: 4px 0 0;
856
+ color: var(--lp-ink);
857
+ font-size: var(--lp-fs-2xs);
858
+ line-height: 1.48;
859
+ overflow-wrap: anywhere;
860
+ white-space: pre-wrap;
861
+ }
862
+
863
+ .cg-record-comment-error {
864
+ margin: 8px 0;
865
+ padding: 8px 10px;
866
+ border: 1px solid var(--lp-red);
867
+ border-radius: var(--lp-r-md);
868
+ background: var(--lp-red-tint);
869
+ color: var(--lp-red-deep);
870
+ font-size: var(--lp-fs-3xs);
871
+ line-height: 1.4;
872
+ }
873
+
874
+ .cg-record-comment-compose {
875
+ flex: 0 0 auto;
876
+ padding: 12px 14px 14px;
877
+ border-top: 1px solid var(--lp-line);
878
+ background: var(--lp-surface);
879
+ }
880
+
881
+ .cg-record-comment-compose label {
882
+ display: block;
883
+ margin-bottom: 6px;
884
+ color: var(--lp-ink);
885
+ font-size: var(--lp-fs-2xs);
886
+ font-weight: 600;
887
+ }
888
+
889
+ .cg-record-comment-compose textarea {
890
+ width: 100%;
891
+ min-height: 78px;
892
+ max-height: 180px;
893
+ resize: vertical;
894
+ padding: 9px 10px;
895
+ border: 1px solid var(--lp-line);
896
+ border-radius: var(--lp-r-md);
897
+ background: var(--lp-surface);
898
+ color: var(--lp-ink);
899
+ font: inherit;
900
+ font-size: var(--lp-fs-2xs);
901
+ line-height: 1.45;
902
+ }
903
+
904
+ .cg-record-comment-compose textarea:focus {
905
+ outline: 2px solid var(--lp-blue-deep);
906
+ outline-offset: -1px;
907
+ border-color: transparent;
908
+ }
909
+
910
+ .cg-record-comment-compose-foot {
911
+ display: flex;
912
+ align-items: center;
913
+ justify-content: space-between;
914
+ gap: 8px;
915
+ margin-top: 7px;
916
+ }
917
+
918
+ .cg-record-comment-compose-foot span {
919
+ color: var(--lp-muted);
920
+ font-size: var(--lp-fs-4xs);
921
+ }
922
+
923
+ .cg-record-comment-compose-foot button {
924
+ min-width: 76px;
925
+ height: 28px;
926
+ border: 1px solid var(--lp-blue-solid);
927
+ border-radius: var(--lp-r-md);
928
+ background: var(--lp-blue-solid);
929
+ color: white;
930
+ font: inherit;
931
+ font-size: var(--lp-fs-2xs);
932
+ font-weight: 600;
933
+ cursor: pointer;
934
+ }
935
+
936
+ .cg-record-comment-compose-foot button:disabled {
937
+ opacity: 0.48;
938
+ cursor: default;
939
+ }
940
+
941
+ @media (max-width: 760px) {
942
+ .cg-record-backdrop {
943
+ padding: 0;
944
+ }
945
+
946
+ .cg-record-modal,
947
+ .cg-record-modal--single {
948
+ width: 100vw;
949
+ height: 100dvh;
950
+ min-height: 0;
951
+ border: 0;
952
+ border-radius: 0;
953
+ }
954
+
955
+ .cg-record-layout {
956
+ grid-template-columns: minmax(0, 1fr);
957
+ grid-template-rows: minmax(220px, 1.2fr) minmax(250px, 0.8fr);
958
+ }
959
+
960
+ .cg-record-comments {
961
+ border-top: 1px solid var(--lp-line);
962
+ border-left: 0;
963
+ }
964
+
965
+ .cg-record-main .cg-detail-titlewrap {
966
+ padding: 16px 18px 14px;
967
+ }
968
+
969
+ .cg-record-main .cg-detail-body {
970
+ padding-inline: 2px;
971
+ }
972
+
973
+ /* Both selectors, because the editable row's label is set at (0,3,0) above:
974
+ narrowing only the read-only one would step the two label columns apart at
975
+ exactly the width where the misalignment is most visible. */
976
+ .cg-record-main .cg-detail-label,
977
+ .cg-record-main .cg-detail-field--edit .cg-detail-label {
978
+ flex-basis: 120px;
979
+ }
980
+ }
981
+
982
+ /* ==========================================================================
983
+ Wave 14 R7 β€” one line, no wraps: the record modal's button rows.
984
+ ==========================================================================
985
+ Audited every row of controls this session renders. Two were real risks and
986
+ the rest were already sound, which is worth recording so the next sweep does
987
+ not re-derive it:
988
+
989
+ Β· the header (β€Ή β€Ί Β· "Record 3 of 1,234" Β· Γ—) β€” the buttons are fixed
990
+ 28px squares, but the POSITION is variable text in a 44/46px fixed-height
991
+ row. At a narrow width it wrapped to two lines and pushed itself out of
992
+ the header it lives in.
993
+ Β· the comment composer's foot β€” a character counter opposite a Post
994
+ button under `space-between`; the counter wrapped before the button
995
+ shrank, which moved the button off its baseline.
996
+ Β· Documents rows were ALREADY correct (index.css: name ellipsises,
997
+ meta and delete are `flex: 0 0 auto`) β€” left alone.
998
+ Β· the tab row, the chip rows and the star row are new this wave and were
999
+ built nowrap; the declarations below are what make that a rule rather
1000
+ than a coincidence of the current labels.
1001
+
1002
+ `nowrap` is the assertion. A label that outgrows its space now overflows
1003
+ VISIBLY instead of quietly reflowing β€” the failure you can see beats the one
1004
+ you cannot. */
1005
+ .cg-record-modal .cg-detail-pos,
1006
+ .cg-record-tab,
1007
+ .cg-record-main .cg-detail-chip,
1008
+ .cg-record-comment-compose-foot button {
1009
+ white-space: nowrap;
1010
+ }
1011
+
1012
+ .cg-record-comment-compose-foot span {
1013
+ min-width: 0;
1014
+ overflow: hidden;
1015
+ text-overflow: ellipsis;
1016
+ white-space: nowrap;
1017
+ }
1018
+
1019
+ .cg-record-comment-compose-foot button {
1020
+ flex: 0 0 auto;
1021
+ }
1022
+
1023
+ @media (prefers-reduced-motion: reduce) {
1024
+ .cg-record-modal {
1025
+ animation: none;
1026
+ }
1027
+ }
web/src/customer-grid/RecordDetail.tsx CHANGED
@@ -1151,27 +1151,34 @@ export default function RecordDetail({
1151
  >
1152
  β€Ί
1153
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1154
  <span className="cg-detail-pos">{positionLabel}</span>
1155
  </div>
1156
- {/* ⭐⭐ W35-T29 (R5, R6) β€” THE SAME RECORD STAR AS THE GRID'S, in the drawer.
1157
- β›” THE SAME STATE, NOT A SECOND ONE: `starred`/`onStar` come from the grid's own
1158
- `useRecordStars`, so starring here and starring in the row are one act on one list.
1159
- A drawer with its own client would answer the same question twice and disagree the
1160
- moment either wrote [[one-evaluator-per-question]].
1161
- ⚠ ABSENT, not disabled, when the caller passes no handler β€” an embedded grid has no
1162
- record stars at all, and a dead control is worse than none. */}
1163
- {onStar && (
1164
- <button
1165
- type="button"
1166
- className={"cg-detail-star" + (starred ? " is-on" : "")}
1167
- aria-pressed={starred}
1168
- aria-label={starred ? "Unstar this record" : "Star this record"}
1169
- title={starred ? "Unstar this record" : "Star this record"}
1170
- onClick={() => onStar(!starred)}
1171
- >
1172
- <RecordStarMark size={16} filled={starred} />
1173
- </button>
1174
- )}
1175
  <button
1176
  type="button"
1177
  className="cg-detail-close"
 
1151
  >
1152
  β€Ί
1153
  </button>
1154
+ {/* ⭐⭐ W35-T29 (R5, R6) β€” THE SAME RECORD STAR AS THE GRID'S, in the drawer.
1155
+ β›” THE SAME STATE, NOT A SECOND ONE: `starred`/`onStar` come from the grid's own
1156
+ `useRecordStars`, so starring here and starring in the row are one act on one list.
1157
+ A drawer with its own client would answer the same question twice and disagree the
1158
+ moment either wrote [[one-evaluator-per-question]].
1159
+ ⚠ ABSENT, not disabled, when the caller passes no handler β€” an embedded grid has
1160
+ no record stars at all, and a dead control is worse than none.
1161
+ ⭐ OWNER ITEM 12 (2026-08-23) β€” IT MOVED INTO THIS GROUP, and the header layout is
1162
+ why it had to. `.cg-detail-header` is `justify-content: space-between` with three
1163
+ children, so a middle child is centred by ARITHMETIC rather than by intent: the
1164
+ star floated alone in the middle of the bar, attached to nothing. Owner: *"the
1165
+ 'Star record' place is so out of placed there. Put it just beside the < > under
1166
+ record detail."* Inside `.cg-detail-nav` it is one of three 28px keys, and the
1167
+ position caption reads after them. */}
1168
+ {onStar && (
1169
+ <button
1170
+ type="button"
1171
+ className={"cg-detail-star" + (starred ? " is-on" : "")}
1172
+ aria-pressed={starred}
1173
+ aria-label={starred ? "Unstar this record" : "Star this record"}
1174
+ title={starred ? "Unstar this record" : "Star this record"}
1175
+ onClick={() => onStar(!starred)}
1176
+ >
1177
+ <RecordStarMark size={16} filled={starred} />
1178
+ </button>
1179
+ )}
1180
  <span className="cg-detail-pos">{positionLabel}</span>
1181
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1182
  <button
1183
  type="button"
1184
  className="cg-detail-close"
web/src/customer-grid/rowStar.css CHANGED
@@ -1,76 +1,80 @@
1
- /* ---------------------------------------------------------------------------
2
- customer-grid / rowStar.css β€” W35-T29 (rulings R5, R6), the record star.
3
-
4
- β›” ITS OWN SHEET, AND THE REASON IS OWNERSHIP RATHER THAN TIDINESS. `.cg-row-expand`, the hover
5
- control this one sits beside, lives in `index.css`, which belongs to another lane this wave. A
6
- new surface brings its own stylesheet (the wave's own rule) and a change to an existing
7
- `.cg-*` rule is that lane's ticket.
8
-
9
- ⚠ IMPORTED BY `CustomerGrid.tsx`, the module that RENDERS these classes β€” never by a lazy
10
- parent. W35-T22 in this same wave was exactly that mistake one directory over: `query.css` was
11
- imported only by a `lazy()` page while the component drawing its classes shipped in another
12
- chunk, so the rail painted unstyled for a network round trip.
13
- --------------------------------------------------------------------------- */
14
-
15
- /* The hover control at the LEFT end of the primary cell, mirroring the Expand button at its
16
- right end. Both are real DOM buttons laid over the canvas at glide's own bounds, so the rect
17
- IS the hit test and there is no second copy of the geometry to drift from the drawing. */
18
- .cg-row-star {
19
- position: fixed;
20
- z-index: 55;
21
- display: inline-flex;
22
- align-items: center;
23
- justify-content: center;
24
- padding: 0;
25
- border: 1px solid transparent;
26
- border-radius: var(--lp-r-sm);
27
- background: transparent;
28
- color: var(--lp-muted);
29
- cursor: pointer;
30
- }
31
-
32
- /* β›” A STARRED ROW'S MARK IS NOT HOVER-ONLY. The hover control is how you SET one; a row that is
33
- already starred has to say so while the pointer is elsewhere, or the grid cannot answer the
34
- question the feature exists for. D-253 is a booked defect whose whole content is "satisfied
35
- only on hover" β€” this is that lesson applied to a mark rather than to an explanation.
36
- The always-on mark is drawn by the same button with `is-on`; the caller keeps it mounted for a
37
- starred row whether or not the pointer is over it. */
38
- .cg-row-star.is-on {
39
- color: var(--lp-yellow-deep);
40
- }
41
-
42
- .cg-row-star:hover {
43
- border-color: var(--lp-line);
44
- background: var(--cg-white);
45
- color: var(--lp-yellow-deep);
46
- }
47
-
48
- .cg-row-star:focus-visible {
49
- outline: 2px solid var(--lp-blue-deep);
50
- outline-offset: 1px;
51
- }
52
-
53
- .cg-row-star:disabled {
54
- cursor: default;
55
- opacity: 0.55;
56
- }
57
-
58
- /* The drawer's own copy of the control (`RecordDetail`), which is in the flow rather than over a
59
- canvas β€” so it takes the colour rules above and none of the positioning. */
60
- .cg-detail-star {
61
- display: inline-flex;
62
- align-items: center;
63
- justify-content: center;
64
- width: 28px;
65
- height: 28px;
66
- padding: 0;
67
- border: 1px solid transparent;
68
- border-radius: var(--lp-r-sm);
69
- background: transparent;
70
- color: var(--lp-muted);
71
- cursor: pointer;
72
- }
73
-
74
- .cg-detail-star.is-on { color: var(--lp-yellow-deep); }
75
- .cg-detail-star:hover { border-color: var(--lp-line); color: var(--lp-yellow-deep); }
76
- .cg-detail-star:disabled { cursor: default; opacity: 0.55; }
 
 
 
 
 
1
+ /* ---------------------------------------------------------------------------
2
+ customer-grid / rowStar.css β€” W35-T29 (rulings R5, R6), the record star.
3
+
4
+ β›” ITS OWN SHEET, AND THE REASON IS OWNERSHIP RATHER THAN TIDINESS. `.cg-row-expand`, the hover
5
+ control this one sits beside, lives in `index.css`, which belongs to another lane this wave. A
6
+ new surface brings its own stylesheet (the wave's own rule) and a change to an existing
7
+ `.cg-*` rule is that lane's ticket.
8
+
9
+ ⚠ IMPORTED BY `CustomerGrid.tsx`, the module that RENDERS these classes β€” never by a lazy
10
+ parent. W35-T22 in this same wave was exactly that mistake one directory over: `query.css` was
11
+ imported only by a `lazy()` page while the component drawing its classes shipped in another
12
+ chunk, so the rail painted unstyled for a network round trip.
13
+ --------------------------------------------------------------------------- */
14
+
15
+ /* The hover control at the LEFT end of the primary cell, mirroring the Expand button at its
16
+ right end. Both are real DOM buttons laid over the canvas at glide's own bounds, so the rect
17
+ IS the hit test and there is no second copy of the geometry to drift from the drawing. */
18
+ .cg-row-star {
19
+ position: fixed;
20
+ z-index: 55;
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ padding: 0;
25
+ border: 1px solid transparent;
26
+ border-radius: var(--lp-r-sm);
27
+ background: transparent;
28
+ color: var(--lp-muted);
29
+ cursor: pointer;
30
+ }
31
+
32
+ /* β›” A STARRED ROW'S MARK IS NOT HOVER-ONLY. The hover control is how you SET one; a row that is
33
+ already starred has to say so while the pointer is elsewhere, or the grid cannot answer the
34
+ question the feature exists for. D-253 is a booked defect whose whole content is "satisfied
35
+ only on hover" β€” this is that lesson applied to a mark rather than to an explanation.
36
+ The always-on mark is drawn by the same button with `is-on`; the caller keeps it mounted for a
37
+ starred row whether or not the pointer is over it. */
38
+ .cg-row-star.is-on {
39
+ color: var(--lp-yellow-deep);
40
+ }
41
+
42
+ .cg-row-star:hover {
43
+ border-color: var(--lp-line);
44
+ background: var(--cg-white);
45
+ color: var(--lp-yellow-deep);
46
+ }
47
+
48
+ .cg-row-star:focus-visible {
49
+ outline: 2px solid var(--lp-blue-deep);
50
+ outline-offset: 1px;
51
+ }
52
+
53
+ .cg-row-star:disabled {
54
+ cursor: default;
55
+ opacity: 0.55;
56
+ }
57
+
58
+ /* The drawer's own copy of the control (`RecordDetail`), which is in the flow rather than over a
59
+ canvas β€” so it takes the colour rules above and none of the positioning.
60
+ ⭐ OWNER ITEM 12 (2026-08-23) β€” it now sits INSIDE `.cg-detail-nav`, beside the prev/next keys,
61
+ so it is sized and bordered like them: a group of three buttons reads as a group only if the
62
+ third one is the same shape. The border is transparent until hover, which is what still tells
63
+ an ACTION apart from the two NAVIGATION keys beside it. */
64
+ .cg-detail-star {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ width: 28px;
69
+ height: 28px;
70
+ padding: 0;
71
+ border: 1px solid transparent;
72
+ border-radius: var(--lp-r-md);
73
+ background: transparent;
74
+ color: var(--lp-muted);
75
+ cursor: pointer;
76
+ }
77
+
78
+ .cg-detail-star.is-on { color: var(--lp-yellow-deep); }
79
+ .cg-detail-star:hover { border-color: var(--lp-line); color: var(--lp-yellow-deep); }
80
+ .cg-detail-star:disabled { cursor: default; opacity: 0.55; }
web/src/index.css CHANGED
@@ -56,7 +56,22 @@
56
  --cg-z-editor: 40;
57
  --cg-z-menu: 50;
58
  --cg-z-column: 60;
 
 
 
 
 
 
59
  --cg-z-drawer: 70;
 
 
 
 
 
 
 
 
 
60
  /* β›”β›” THE PORTAL'D OVERLAY STEP, AND IT IS ABOVE THE DIALOG STEP ON PURPOSE.
61
  `AnchoredOverlay` renders through `BodyPortal` β€” a DIRECT CHILD OF <body>, so it
62
  resolves in the ROOT stacking context and its z-index competes with the app's
@@ -73,7 +88,12 @@
73
  ⭐ ABOVE 80 (`.shell-newdb`'s scrim) and 90 (`.shell-nav-tip`) too: a transient
74
  panel anchored to a control the user just clicked belongs above every surface,
75
  because it is only on screen while they are using it. */
76
- --cg-z-overlay: 95;
 
 
 
 
 
77
 
78
  /* --- Loopable C1 tokens (wave-8 I7c) ------------------------------------
79
  ADDITIVE on purpose. The `--cg-*` tokens above are NOT redefined because
@@ -4773,7 +4793,11 @@ body > .cg-overlay {
4773
  }
4774
  .cg-route-popover {
4775
  position: absolute;
4776
- z-index: var(--cg-z-overlay);
 
 
 
 
4777
  top: 48px;
4778
  left: 12px;
4779
  width: min(360px, calc(100% - 24px));
 
56
  --cg-z-editor: 40;
57
  --cg-z-menu: 50;
58
  --cg-z-column: 60;
59
+ /* ⭐ OWNER ITEM 11 (2026-08-23) β€” MAP-LOCAL FLOATING CHROME, its own step so it can be told
60
+ apart from a DIALOG. The route planner is an `absolute` panel inside the map frame; it needs
61
+ to beat the map's own contents and the selection bar (`--cg-z-menu`) and nothing else. It used
62
+ to borrow `--cg-z-overlay`, which is the PORTAL step, and that is how a map panel ended up
63
+ painting over the record detail. */
64
+ --cg-z-mapchrome: 65;
65
  --cg-z-drawer: 70;
66
+ /* ⭐⭐ OWNER ITEM 11 β€” A MODAL DIALOG PORTAL'D TO <body>: the record detail and the
67
+ linked-record grid, which share `.cg-record-backdrop`.
68
+ Owner: *"the Detail records when clicked should ALWAYS be at the top of every other
69
+ navigation pop-up."* It was `--cg-z-drawer` (70) and the Map's route planner was 95, so
70
+ opening a record from a pin painted the record UNDER the panel it was opened from β€” the
71
+ dialog you just asked for, behind the thing you asked from.
72
+ ⚠ ABOVE the shell's own layers too (`.shell-newdb`'s scrim at 80, `.shell-nav-tip` at 90),
73
+ because "every other navigation pop-up" includes the ones outside the grid. */
74
+ --cg-z-record: 100;
75
  /* β›”β›” THE PORTAL'D OVERLAY STEP, AND IT IS ABOVE THE DIALOG STEP ON PURPOSE.
76
  `AnchoredOverlay` renders through `BodyPortal` β€” a DIRECT CHILD OF <body>, so it
77
  resolves in the ROOT stacking context and its z-index competes with the app's
 
88
  ⭐ ABOVE 80 (`.shell-newdb`'s scrim) and 90 (`.shell-nav-tip`) too: a transient
89
  panel anchored to a control the user just clicked belongs above every surface,
90
  because it is only on screen while they are using it. */
91
+ /* β›” RAISED 95 -> 110 BY OWNER ITEM 11, AND THE ORDER IT KEEPS IS THE WHOLE REASON. The
92
+ record modal moved to 100 so it can beat the map's route panel; this step has to stay ABOVE
93
+ it, or a column menu opened inside the LINKED-RECORD GRID (which shares that backdrop) would
94
+ paint underneath its own dialog β€” which is precisely the failure the paragraph above
95
+ records, re-created one modal later. */
96
+ --cg-z-overlay: 110;
97
 
98
  /* --- Loopable C1 tokens (wave-8 I7c) ------------------------------------
99
  ADDITIVE on purpose. The `--cg-*` tokens above are NOT redefined because
 
4793
  }
4794
  .cg-route-popover {
4795
  position: absolute;
4796
+ /* β›” OWNER ITEM 11 β€” `--cg-z-mapchrome`, NOT `--cg-z-overlay`. This panel is `absolute` inside
4797
+ the map frame, not portal'd to <body>, so it never needed the portal step; borrowing it is
4798
+ what put a map panel over the record detail. It still outranks the map's own contents and
4799
+ the `.cg-selbar` (`--cg-z-menu`), which is everything it has to beat. */
4800
+ z-index: var(--cg-z-mapchrome);
4801
  top: 48px;
4802
  left: 12px;
4803
  width: min(360px, calc(100% - 24px));