emdash-classic-theme 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/skin.css ADDED
@@ -0,0 +1,3184 @@
1
+ /* ==========================================================================
2
+ skin.css (v4)
3
+ "WordPress" skin for the EmDash admin panel. Served from the site itself and injected
4
+ into the <head> of /_emdash/admin by the outer middleware (src/cms/painel-wp.ts).
5
+ Does not touch core: if this file disappears, the panel goes back to normal.
6
+
7
+ v4 = the owner's v3 plus the 9 fixes from the 2026-09-07 audit (29 agents,
8
+ four fronts, each finding passed through a skeptic). Each fix is commented
9
+ at the point where it applies, with the measured reason.
10
+ ========================================================================== */
11
+
12
+ /* ==========================================================================
13
+ [0] WHITE LABEL TOKENS (change only here, per client)
14
+ The html:root[data-mode] selector has specificity (0,2,1): it beats EmDash's
15
+ "classic" theme ([data-theme="classic"]:not([data-mode="dark"]) = 0,2,0),
16
+ which defines several of these same tokens.
17
+ ========================================================================== */
18
+ html:root[data-mode] {
19
+ /* FIX 1 (2026-09-07 audit): kumo's dark mode comes from light-dark() on ~130
20
+ tokens. Declaring color-scheme here disarms all of them at once, and it's
21
+ what stops the panel from mixing the skin's white surface with the dark
22
+ theme's light text (contrast measured at 1.28:1 on the success notice).
23
+ WordPress has no dark mode, so forcing light is consistent.
24
+ Accepted consequence: the panel's theme switcher becomes inert. */
25
+ color-scheme: light;
26
+
27
+ /* Status text: the skin paints the surface white, and the classic theme's
28
+ values (#00a32a, #dba617) would measure 3.35:1 and ~2:1. These pass AA on
29
+ #fff. */
30
+ --text-color-kumo-danger: #b32d2e;
31
+ --text-color-kumo-success: #007017;
32
+ --text-color-kumo-warning: #996800;
33
+ --text-color-kumo-info: #2271b1;
34
+
35
+ --wp-accent: #2271b1; /* client's accent color */
36
+ --wp-ink: #1d2327; /* toolbar and menu */
37
+ --wp-canvas: #f0f0f1; /* page background */
38
+ --wp-surface: #ffffff; /* widgets, tables, fields */
39
+ /* round 5 (2026-09-08, owner's request): ONE border color for everything, and
40
+ weaker. Section outline, widget frame, table header and field resting
41
+ border all now read the same #dcdcde. CONSCIOUS DECISION AGAINST WCAG:
42
+ #dcdcde measures 1.36:1 on white, below the 3:1 that 1.4.11 asks for on
43
+ component outlines. The owner was told the number and kept it ("the
44
+ borders are too heavy... even against contrast"). What holds
45
+ accessibility in place instead of the border: field hover darkens to
46
+ #8c8f94 (line ~722) and focus still relies on the 2px accent ring, which
47
+ is what the standard actually requires to be visible. */
48
+ --wp-line: #dcdcde; /* borders: section, widget, table, band */
49
+ --wp-field-line: #dcdcde; /* field resting border: the SAME as the section's (hover goes to #8c8f94) */
50
+ --wp-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
51
+ Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
52
+
53
+ --wp-toolbar-h: 32px;
54
+ --wp-menu-w: 230px; /* WP uses 160px; the labels here are bigger */
55
+ --wp-radius: 4px; /* corner radius of widgets and buttons */
56
+ --wp-icon: 20px; /* menu icons (dashicons = 20px) */
57
+ --wp-icon-bold: 8; /* extra weight for Phosphor icons (0 = regular, 8 = bold) */
58
+
59
+ /* derived values: fixed fallback for browsers without color-mix (the @supports below overrides) */
60
+ --wp-accent-hover: #135e96;
61
+ --wp-accent-deep: #0a4b78;
62
+ --wp-accent-soft: #72aee6;
63
+ --wp-accent-tint: #f0f6fc;
64
+ --wp-accent-contrast: #ffffff;
65
+ --wp-ink-hover: #2c3338;
66
+ --wp-ink-text: #f0f0f1;
67
+ --wp-ink-icon: rgba(240, 246, 252, .72);
68
+ --wp-ink-muted: rgba(240, 246, 252, .52);
69
+ --wp-ink-line: rgba(255, 255, 255, .08);
70
+ --wp-text: #1d2327;
71
+ --wp-muted: #646970;
72
+ --wp-line-soft: #dcdcde;
73
+ --wp-button-bg: #f6f7f7;
74
+
75
+ --wp-ease: cubic-bezier(.2, .7, .2, 1);
76
+ --wp-ease-out: cubic-bezier(.16, 1, .3, 1);
77
+ --wp-fast: .15s;
78
+ --wp-normal: .28s;
79
+ }
80
+
81
+ @supports (color: color-mix(in oklab, red 50%, white)) {
82
+ html:root[data-mode] {
83
+ --wp-accent-hover: color-mix(in oklab, var(--wp-accent) 82%, black);
84
+ --wp-accent-deep: color-mix(in oklab, var(--wp-accent) 66%, black);
85
+ --wp-accent-soft: color-mix(in oklab, var(--wp-accent) 52%, white);
86
+ --wp-accent-tint: color-mix(in oklab, var(--wp-accent) 7%, var(--wp-surface));
87
+ --wp-ink-hover: color-mix(in oklab, var(--wp-ink) 90%, white);
88
+ --wp-ink-text: color-mix(in oklab, var(--wp-ink) 6%, white);
89
+ --wp-ink-icon: color-mix(in oklab, var(--wp-ink-text) 72%, transparent);
90
+ --wp-ink-muted: color-mix(in oklab, var(--wp-ink-text) 56%, transparent);
91
+ --wp-ink-line: color-mix(in oklab, white 8%, transparent);
92
+ --wp-text: var(--wp-ink);
93
+ --wp-muted: color-mix(in oklab, var(--wp-ink) 66%, var(--wp-canvas));
94
+ /* round 5: no longer derived from --wp-line. It only feeds --color-kumo-recessed, which
95
+ is a BACKGROUND, not a border; with the new border (#dcdcde) the math returned
96
+ #eaeaeb and lightened that background without anyone asking for it. Pinned to the
97
+ value it always had. */
98
+ --wp-button-bg: color-mix(in oklab, var(--wp-canvas) 40%, var(--wp-surface));
99
+ }
100
+ }
101
+
102
+ /* ==========================================================================
103
+ [1] BRIDGE TO THE EMDASH DESIGN SYSTEM
104
+ ========================================================================== */
105
+ html:root[data-mode] {
106
+ /* brand and focus color: EmDash's blue becomes the client's accent color */
107
+ --color-kumo-brand: var(--wp-accent);
108
+ --color-kumo-brand-hover: var(--wp-accent-hover);
109
+ --color-kumo-focus: var(--wp-accent);
110
+ --text-color-kumo-link: var(--wp-accent);
111
+
112
+ /* surfaces */
113
+ --color-kumo-canvas: var(--wp-canvas);
114
+ --color-kumo-elevated: var(--wp-canvas);
115
+ --color-kumo-base: var(--wp-surface);
116
+ --color-kumo-control: var(--wp-surface);
117
+ --color-kumo-overlay: var(--wp-surface);
118
+ --color-kumo-tint: var(--wp-canvas);
119
+ --color-kumo-hairline: var(--wp-line);
120
+ --color-kumo-line: var(--wp-line);
121
+ --color-kumo-recessed: var(--wp-line-soft);
122
+
123
+ /* text */
124
+ --text-color-kumo-default: var(--wp-text);
125
+ --text-color-kumo-subtle: var(--wp-muted);
126
+
127
+ /* radius: WP is nearly square; 4px on widgets and buttons, 3px on fields */
128
+ --radius-sm: 2px;
129
+ --radius-md: 3px;
130
+ --radius-lg: var(--wp-radius);
131
+ --radius-xl: 6px;
132
+ --radius-2xl: 8px;
133
+
134
+ /* wp-admin type scale: 13 body, 12 secondary, 23 heading */
135
+ --text-base: 13px;
136
+ --text-base--line-height: 1.5;
137
+ --text-sm: 12px;
138
+ --text-sm--line-height: 1.5;
139
+ --text-xs: 11px;
140
+ --text-2xl: 23px;
141
+ --text-2xl--line-height: 1.3;
142
+ --font-emdash: var(--wp-font);
143
+
144
+ /* EmDash's shadows also switch to the client's ink color */
145
+ --color-kumo-shadow-drop: rgba(29, 35, 39, .08);
146
+ --color-kumo-shadow-edge: rgba(29, 35, 39, .12);
147
+ }
148
+
149
+ html[data-mode] body {
150
+ font-family: var(--wp-font);
151
+ }
152
+
153
+ /* menu width and colors: EmDash sets this inline on the wrapper */
154
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) {
155
+ --sidebar-width: var(--wp-menu-w) !important;
156
+ --sidebar-bg: var(--wp-ink) !important;
157
+ --sidebar-active-bg: var(--wp-accent) !important;
158
+ }
159
+
160
+ /* inside the menu, the text and line tokens switch to the dark ink ones;
161
+ anything in there using text-kumo-* or border-kumo-line follows on its own */
162
+ html[data-mode] :is(aside,nav).emdash-sidebar {
163
+ --text-color-kumo-default: var(--wp-ink-text);
164
+ --text-color-kumo-strong: var(--wp-accent-contrast);
165
+ --text-color-kumo-subtle: var(--wp-ink-muted);
166
+ --text-color-kumo-inactive: var(--wp-ink-muted);
167
+ --color-kumo-line: var(--wp-ink-line);
168
+ --color-kumo-hairline: var(--wp-ink-line);
169
+ --color-kumo-elevated: var(--wp-ink);
170
+ --color-kumo-base: var(--wp-ink);
171
+ --color-kumo-fill: var(--wp-ink-hover);
172
+ --color-kumo-fill-hover: var(--wp-ink-hover);
173
+ color: var(--wp-ink-text);
174
+ }
175
+
176
+ /* the toolbar too */
177
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header {
178
+ --text-color-kumo-default: var(--wp-ink-text);
179
+ --text-color-kumo-subtle: var(--wp-ink-icon);
180
+ --color-kumo-elevated: var(--wp-ink);
181
+ --color-kumo-base: var(--wp-ink);
182
+ --color-kumo-line: transparent;
183
+ --color-kumo-fill: var(--wp-ink-hover);
184
+ --color-kumo-fill-hover: var(--wp-ink-hover);
185
+ }
186
+
187
+ /* ==========================================================================
188
+ [2] TOOLBAR = #wpadminbar
189
+ ========================================================================== */
190
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header {
191
+ height: var(--wp-toolbar-h) !important;
192
+ min-height: 0 !important;
193
+ padding: 0 !important;
194
+ border-bottom: 0 !important;
195
+ font-size: 13px;
196
+ box-shadow: 0 1px 0 rgba(0, 0, 0, .25), 0 6px 16px -10px rgba(0, 0, 0, .45);
197
+ }
198
+
199
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header > div {
200
+ gap: 0 !important;
201
+ height: 100%;
202
+ }
203
+
204
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header a,
205
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header button {
206
+ height: var(--wp-toolbar-h) !important;
207
+ min-height: 0 !important;
208
+ padding: 0 8px !important;
209
+ margin: 0 !important;
210
+ border-radius: 0 !important;
211
+ background: transparent !important;
212
+ color: var(--wp-ink-text) !important;
213
+ font-size: 13px !important;
214
+ font-weight: 400 !important;
215
+ line-height: var(--wp-toolbar-h) !important;
216
+ box-shadow: none !important;
217
+ -webkit-transition: background-color var(--wp-fast) ease, color var(--wp-fast) ease;
218
+ transition: background-color var(--wp-fast) ease, color var(--wp-fast) ease;
219
+ }
220
+
221
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header a:hover,
222
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header button:hover,
223
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header a:focus-visible,
224
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header button:focus-visible {
225
+ background: var(--wp-ink-hover) !important;
226
+ color: var(--wp-accent-soft) !important;
227
+ }
228
+
229
+ /* FIX 6 (audit): without this, keyboard focus on the toolbar is invisible (the hover
230
+ background gives 1.16:1 against normal). Negative offset because the toolbar is 32px
231
+ and an outward ring would get clipped. WCAG 2.4.7 and 2.4.11. */
232
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header a:focus-visible,
233
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header button:focus-visible {
234
+ outline: 2px solid var(--wp-accent-soft) !important;
235
+ outline-offset: -2px !important;
236
+ }
237
+
238
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header svg {
239
+ width: 16px;
240
+ height: 16px;
241
+ }
242
+
243
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header button[data-base-ui-tooltip-trigger] {
244
+ width: var(--wp-toolbar-h) !important;
245
+ }
246
+
247
+ /* avatar: small square, like WP's Gravatar */
248
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header .rounded-full {
249
+ width: 18px !important;
250
+ height: 18px !important;
251
+ border-radius: 2px !important;
252
+ background: var(--wp-accent) !important;
253
+ color: var(--wp-accent-contrast) !important;
254
+ font-size: 10px !important;
255
+ font-weight: 600 !important;
256
+ }
257
+
258
+ /* --------------------------------------------------------------------------
259
+ Desktop layout: toolbar fixed on top; menu and content drop down 32px;
260
+ the menu header (logo + site name) becomes the toolbar's left corner.
261
+ Below 768px EmDash uses its own mobile layout and none of this applies.
262
+ -------------------------------------------------------------------------- */
263
+ @media (min-width: 768px) {
264
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header > [data-sidebar="trigger"] {
265
+ display: none !important; /* the collapse button is already in the menu footer */
266
+ }
267
+
268
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) {
269
+ padding-top: var(--wp-toolbar-h) !important;
270
+ -webkit-box-sizing: border-box;
271
+ box-sizing: border-box;
272
+ }
273
+
274
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header {
275
+ position: fixed !important;
276
+ top: 0 !important;
277
+ left: 0 !important;
278
+ right: 0 !important;
279
+ z-index: 30 !important; /* below the collapsed menu (z-40): the site name
280
+ needs to stay above the bar */
281
+ }
282
+
283
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="header"] {
284
+ position: fixed !important;
285
+ top: 0 !important;
286
+ left: 0 !important;
287
+ z-index: 31 !important;
288
+ width: auto !important;
289
+ max-width: 45vw;
290
+ padding-right: 12px !important;
291
+ }
292
+
293
+ /* collapsed menu: EmDash swaps the container to fixed inset-y-0;
294
+ here it starts below the toolbar */
295
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar)[data-state="collapsed"] :is(aside,nav).emdash-sidebar > [data-sidebar="content-container"] {
296
+ top: var(--wp-toolbar-h) !important;
297
+ bottom: 0 !important;
298
+ height: auto !important;
299
+ }
300
+ }
301
+
302
+ /* menu header (logo + site name) */
303
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="header"] {
304
+ height: var(--wp-toolbar-h) !important;
305
+ padding: 0 8px !important;
306
+ gap: 0 !important;
307
+ border: 0 !important;
308
+ background: var(--wp-ink) !important;
309
+ }
310
+
311
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="header"] a {
312
+ height: var(--wp-toolbar-h) !important;
313
+ width: auto !important;
314
+ max-width: none !important;
315
+ padding: 0 !important;
316
+ gap: 8px !important;
317
+ color: var(--wp-ink-text) !important;
318
+ font-size: 13px !important;
319
+ font-weight: 400 !important;
320
+ line-height: var(--wp-toolbar-h) !important;
321
+ text-decoration: none !important;
322
+ -webkit-transition: color var(--wp-fast) ease;
323
+ transition: color var(--wp-fast) ease;
324
+ }
325
+
326
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="header"] a:hover {
327
+ color: var(--wp-accent-soft) !important;
328
+ }
329
+
330
+ /* the site name doesn't truncate or disappear when the menu collapses */
331
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="header"] a > span {
332
+ grid-template-columns: 1fr !important;
333
+ }
334
+
335
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="header"] a span {
336
+ width: auto !important;
337
+ max-width: none !important;
338
+ font-weight: 400 !important;
339
+ }
340
+
341
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="header"] svg {
342
+ width: 18px !important;
343
+ height: 18px !important;
344
+ }
345
+
346
+ /* ==========================================================================
347
+ [3] SIDE MENU = #adminmenu
348
+ ========================================================================== */
349
+ html[data-mode] :is(aside,nav).emdash-sidebar,
350
+ html[data-mode] :is(aside,nav).emdash-sidebar > [data-sidebar="content-container"] {
351
+ background: var(--wp-ink) !important;
352
+ border-right: 0 !important;
353
+ box-shadow: inset -1px 0 0 rgba(0, 0, 0, .18);
354
+ }
355
+
356
+ /* edge to edge, no lateral breathing room, and NO scrollbar:
357
+ scrolling keeps working (mouse wheel, trackpad, keyboard),
358
+ only the track disappears. It was the track that ran over the little arrow. */
359
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="content"] > div {
360
+ padding: 0 !important;
361
+ scrollbar-width: none;
362
+ -ms-overflow-style: none;
363
+ }
364
+
365
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="content"] > div::-webkit-scrollbar {
366
+ display: none;
367
+ width: 0;
368
+ height: 0;
369
+ }
370
+
371
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="group"] {
372
+ gap: 0 !important;
373
+ margin: 0 !important;
374
+ }
375
+
376
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu"] {
377
+ gap: 0 !important;
378
+ }
379
+
380
+ /* item */
381
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"] {
382
+ position: relative !important;
383
+ min-height: 36px !important;
384
+ /* round 3: 16px on the right so the label doesn't touch the active item's arrow or the
385
+ bar's border. Asymmetric on purpose: the left stays at 8px because it's what fixes
386
+ the icon's horizontal position, the same in both states. */
387
+ padding: 0 16px 0 8px !important;
388
+ border-radius: 0 !important;
389
+ font-size: 14px !important;
390
+ font-weight: 400 !important;
391
+ line-height: 1.3 !important;
392
+ box-shadow: none !important;
393
+ text-decoration: none !important;
394
+ -webkit-transition: background-color var(--wp-fast) ease, color var(--wp-fast) ease;
395
+ transition: background-color var(--wp-fast) ease, color var(--wp-fast) ease;
396
+ }
397
+
398
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"] > div {
399
+ gap: 8px !important;
400
+ translate: none !important;
401
+ -webkit-transform: none !important;
402
+ transform: none !important;
403
+ }
404
+
405
+ /* hover: in EmDash, hover and active use the same token; in WP they're different */
406
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"]:hover:not([data-active="true"]) {
407
+ background: var(--wp-ink-hover) !important;
408
+ color: var(--wp-accent-soft) !important;
409
+ }
410
+
411
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"]:focus-visible,
412
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="trigger"]:focus-visible {
413
+ outline: 2px solid var(--wp-accent-soft) !important;
414
+ outline-offset: -2px !important;
415
+ box-shadow: none !important;
416
+ }
417
+
418
+ /* current item */
419
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"][data-active="true"] {
420
+ background: var(--wp-accent) !important;
421
+ color: var(--wp-accent-contrast) !important;
422
+ }
423
+
424
+ /* the little arrow: removed from content flow and placed on top of the active item */
425
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"][data-active="true"]::after {
426
+ content: "";
427
+ position: absolute;
428
+ right: 0;
429
+ top: 50%;
430
+ width: 0;
431
+ height: 0;
432
+ margin-top: -8px;
433
+ border: 8px solid transparent;
434
+ border-right-color: var(--wp-canvas);
435
+ pointer-events: none;
436
+ }
437
+
438
+ /* collapsed menu: the icon does NOT move. In round 3 the owner asked for the icon to stay in
439
+ the same horizontal position in both states, so `justify-content: center` was removed (it
440
+ becomes flex-start in block [17c]) and `padding: 0` was removed, which was what made the
441
+ icon shift when collapsing. Its spacing still comes from the single item padding rule, which
442
+ applies in both states: that's what guarantees they never diverge. */
443
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar)[data-state="collapsed"] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"] > div {
444
+ flex: 0 0 auto !important;
445
+ }
446
+
447
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar)[data-state="collapsed"] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"] > div > span {
448
+ display: none !important;
449
+ }
450
+
451
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar)[data-state="collapsed"] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"][data-active="true"]::after {
452
+ display: none;
453
+ }
454
+
455
+ /* submenu (if any item has children) */
456
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-sub"] {
457
+ background: var(--wp-ink-hover) !important;
458
+ border: 0 !important;
459
+ margin: 0 !important;
460
+ padding: 6px 0 !important;
461
+ gap: 0 !important;
462
+ }
463
+
464
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-sub-button"] {
465
+ min-height: 28px !important;
466
+ padding: 0 12px 0 40px !important;
467
+ border-radius: 0 !important;
468
+ background: transparent !important;
469
+ color: var(--wp-ink-icon) !important;
470
+ font-size: 13px !important;
471
+ font-weight: 400 !important;
472
+ box-shadow: none !important;
473
+ }
474
+
475
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-sub-button"]:hover {
476
+ color: var(--wp-accent-soft) !important;
477
+ }
478
+
479
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-sub-button"][data-active="true"] {
480
+ color: var(--wp-accent-contrast) !important;
481
+ font-weight: 600 !important;
482
+ }
483
+
484
+ /* group labels ("Conteudo", "Gerenciar", "Admin"): subtle separators */
485
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="group-label"] {
486
+ margin: 5px 0 0 !important;
487
+ border: 0 !important;
488
+ border-top: 1px solid var(--wp-ink-line) !important;
489
+ }
490
+
491
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="group-label"] > div > div {
492
+ margin: 0 !important;
493
+ padding: 10px 8px 4px !important;
494
+ color: var(--wp-ink-muted) !important;
495
+ font-size: 11px !important;
496
+ font-weight: 600 !important;
497
+ letter-spacing: .06em !important;
498
+ text-transform: uppercase !important;
499
+ }
500
+
501
+ /* footer = "Recolher menu" */
502
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] {
503
+ height: 40px !important;
504
+ padding: 0 !important;
505
+ background: var(--wp-ink) !important;
506
+ border-top: 1px solid var(--wp-ink-line) !important;
507
+ }
508
+
509
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] > div {
510
+ display: none !important; /* the site name is already in the toolbar */
511
+ }
512
+
513
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] [data-sidebar="trigger"] {
514
+ width: 100% !important;
515
+ height: 40px !important;
516
+ justify-content: flex-start !important;
517
+ gap: 8px !important;
518
+ padding: 0 8px !important;
519
+ border-radius: 0 !important;
520
+ background: transparent !important;
521
+ color: var(--wp-ink-icon) !important;
522
+ font-size: 14px !important;
523
+ -webkit-transition: background-color var(--wp-fast) ease, color var(--wp-fast) ease;
524
+ transition: background-color var(--wp-fast) ease, color var(--wp-fast) ease;
525
+ }
526
+
527
+ /* the text stays inside the button itself: the whole clickable area */
528
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] [data-sidebar="trigger"]::after {
529
+ content: "Recolher menu";
530
+ white-space: nowrap;
531
+ }
532
+
533
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] [data-sidebar="trigger"][aria-expanded="false"]::after {
534
+ display: none;
535
+ }
536
+
537
+ /* round 3: the collapse button also stops zeroing padding and centering, so its
538
+ icon doesn't shift when the bar shrinks. The justify becomes flex-start in block [17c]. */
539
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] [data-sidebar="trigger"][aria-expanded="false"] {
540
+ justify-content: flex-start !important;
541
+ }
542
+
543
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] [data-sidebar="trigger"]:hover {
544
+ background: var(--wp-ink-hover) !important;
545
+ color: var(--wp-accent-soft) !important;
546
+ }
547
+
548
+ /* FIX 3 (audit): the mobile drawer's scrim uses bg-kumo-recessed, which the token
549
+ bridge maps to a light gray. Without this the scrim comes out almost white over the page.
550
+ kumo's opacity-80 already gives the transparency: here it's just the ink color. */
551
+ html[data-mode] [data-sidebar-backdrop] {
552
+ background-color: var(--wp-ink) !important;
553
+ }
554
+
555
+ /* ==========================================================================
556
+ [4] MENU ICONS (Phosphor, regular weight)
557
+ Bigger and with more presence. The weight comes from a stroke laid over the path:
558
+ Phosphor regular is a 16-unit outline on a 256 grid;
559
+ a stroke of 8 turns it into 24, which is exactly the family's "bold" weight.
560
+ No icon is swapped, so an EmDash update doesn't break it.
561
+ ========================================================================== */
562
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"] svg {
563
+ width: var(--wp-icon) !important;
564
+ height: var(--wp-icon) !important;
565
+ flex: 0 0 var(--wp-icon);
566
+ opacity: 1 !important;
567
+ color: var(--wp-ink-icon);
568
+ overflow: visible;
569
+ /* round 3: only the color interpolates. The owner asked to remove the icon's movement on hover,
570
+ and the transform transition here was the leftover half after removing the translate. */
571
+ -webkit-transition: color var(--wp-fast) ease;
572
+ transition: color var(--wp-fast) ease;
573
+ }
574
+
575
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"] svg[viewBox="0 0 256 256"] path {
576
+ stroke: currentColor;
577
+ stroke-width: var(--wp-icon-bold);
578
+ stroke-linejoin: round;
579
+ stroke-linecap: round;
580
+ paint-order: stroke fill;
581
+ }
582
+
583
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"]:hover svg {
584
+ color: var(--wp-accent-soft);
585
+ }
586
+
587
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"][data-active="true"] svg {
588
+ color: var(--wp-accent-contrast);
589
+ }
590
+
591
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] [data-sidebar="trigger"] svg,
592
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header [data-sidebar="trigger"] svg {
593
+ width: var(--wp-icon) !important;
594
+ height: var(--wp-icon) !important;
595
+ opacity: 1 !important;
596
+ }
597
+
598
+ /* FIX 5 (audit): the extra weight only applies to Phosphor (256 grid). The "Recolher menu"
599
+ button's icon is kumo's PanelIcon, viewBox 24 with a 1.5 stroke: thickening it by 8
600
+ would paint a solid block. It's already born with the right weight, nothing to do here. */
601
+
602
+ /* ==========================================================================
603
+ [5] CONTENT
604
+ The core padding stays as EmDash does it (p-6). Only what is WP:
605
+ light-weight heading, "Adicionar novo" button next to the heading in lists,
606
+ widgets in the .postbox pattern, zebra-striped table.
607
+ ========================================================================== */
608
+
609
+ /* page title: 23px comes from the --text-2xl token; the weight WP wants is light */
610
+ html[data-mode] main h1 {
611
+ font-weight: 400 !important;
612
+ color: var(--wp-text) !important;
613
+ }
614
+
615
+ /* REMOVED on 2026-09-07: the rule that forced `justify-content: flex-start` on the
616
+ title row. It stuck the button next to the h1 like in WordPress, but only worked on
617
+ screens WITHOUT a subtitle. On screens with a subtitle (Menus, Categories, Comentarios)
618
+ the h1 and the subtitle live in the same div, the button rose to vertical center and the
619
+ whole row went out of alignment. EmDash's layout is already `flex justify-between`, which
620
+ solves both cases: the right call here is to have no rule, and not push toward the other
621
+ side. The rules below stay, because they paint the BUTTON (the .page-title-action's
622
+ clothing), never the row's geometry. */
623
+
624
+ /* = .page-title-action (on both screens) */
625
+ html[data-mode] main h1 + div > a,
626
+ html[data-mode] main h1 + div > button {
627
+ height: 30px !important;
628
+ min-height: 0 !important;
629
+ padding: 0 10px !important;
630
+ gap: 4px !important;
631
+ border: 1px solid var(--wp-accent) !important;
632
+ border-radius: 3px !important;
633
+ background: var(--wp-button-bg) !important;
634
+ color: var(--wp-accent) !important;
635
+ font-size: 13px !important;
636
+ font-weight: 400 !important;
637
+ line-height: 28px !important;
638
+ box-shadow: 0 1px 1px rgba(29, 35, 39, .04) !important;
639
+ text-decoration: none !important;
640
+ -webkit-transition: background-color var(--wp-fast) ease, border-color var(--wp-fast) ease, color var(--wp-fast) ease, box-shadow var(--wp-fast) ease, -webkit-transform var(--wp-fast) ease;
641
+ transition: background-color var(--wp-fast) ease, border-color var(--wp-fast) ease, color var(--wp-fast) ease, box-shadow var(--wp-fast) ease, transform var(--wp-fast) ease;
642
+ }
643
+
644
+ html[data-mode] main > div > div:has(> div > h1) > a,
645
+ html[data-mode] main > div > div:has(> div > h1) > button {
646
+ height: 30px !important;
647
+ min-height: 0 !important;
648
+ padding: 0 10px !important;
649
+ gap: 4px !important;
650
+ border: 1px solid var(--wp-accent) !important;
651
+ border-radius: 3px !important;
652
+ background: var(--wp-button-bg) !important;
653
+ color: var(--wp-accent) !important;
654
+ font-size: 13px !important;
655
+ font-weight: 400 !important;
656
+ line-height: 28px !important;
657
+ box-shadow: 0 1px 1px rgba(29, 35, 39, .04) !important;
658
+ text-decoration: none !important;
659
+ -webkit-transition: background-color var(--wp-fast) ease, border-color var(--wp-fast) ease, color var(--wp-fast) ease, box-shadow var(--wp-fast) ease, -webkit-transform var(--wp-fast) ease;
660
+ transition: background-color var(--wp-fast) ease, border-color var(--wp-fast) ease, color var(--wp-fast) ease, box-shadow var(--wp-fast) ease, transform var(--wp-fast) ease;
661
+ }
662
+
663
+ /* (the rules with :has are kept separate from the ones without :has on purpose:
664
+ a browser without support discards only the invalid rule, not both) */
665
+ html[data-mode] main h1 + div > a:hover,
666
+ html[data-mode] main h1 + div > button:hover {
667
+ background: var(--wp-surface) !important;
668
+ border-color: var(--wp-accent-deep) !important;
669
+ color: var(--wp-accent-deep) !important;
670
+ box-shadow: 0 2px 6px -2px rgba(29, 35, 39, .18) !important;
671
+ -webkit-transform: translateY(-1px);
672
+ transform: translateY(-1px);
673
+ }
674
+
675
+ html[data-mode] main > div > div:has(> div > h1) > a:hover,
676
+ html[data-mode] main > div > div:has(> div > h1) > button:hover {
677
+ background: var(--wp-surface) !important;
678
+ border-color: var(--wp-accent-deep) !important;
679
+ color: var(--wp-accent-deep) !important;
680
+ box-shadow: 0 2px 6px -2px rgba(29, 35, 39, .18) !important;
681
+ -webkit-transform: translateY(-1px);
682
+ transform: translateY(-1px);
683
+ }
684
+
685
+ html[data-mode] main h1 + div > a:active,
686
+ html[data-mode] main h1 + div > button:active {
687
+ -webkit-transform: translateY(0);
688
+ transform: translateY(0);
689
+ box-shadow: none !important;
690
+ }
691
+
692
+ html[data-mode] main > div > div:has(> div > h1) > a:active,
693
+ html[data-mode] main > div > div:has(> div > h1) > button:active {
694
+ -webkit-transform: translateY(0);
695
+ transform: translateY(0);
696
+ box-shadow: none !important;
697
+ }
698
+
699
+ html[data-mode] main h1 + div > a svg,
700
+ html[data-mode] main h1 + div > button svg {
701
+ width: 14px !important;
702
+ height: 14px !important;
703
+ }
704
+
705
+ /* FIX 7 (audit): the rules above write box-shadow with !important, and in Tailwind
706
+ v4 kumo's ring IS box-shadow: the focus ring dies. Two separate rules on purpose,
707
+ so a browser without :has() doesn't discard both. --wp-accent-deep because the button already
708
+ has a border in the accent color, and a ring of the same color would read as a thicker border. */
709
+ html[data-mode] main h1 + div > a:focus-visible,
710
+ html[data-mode] main h1 + div > button:focus-visible {
711
+ outline: 2px solid var(--wp-accent-deep) !important;
712
+ outline-offset: 2px !important;
713
+ }
714
+
715
+ html[data-mode] main > div > div:has(> div > h1) > a:focus-visible,
716
+ html[data-mode] main > div > div:has(> div > h1) > button:focus-visible {
717
+ outline: 2px solid var(--wp-accent-deep) !important;
718
+ outline-offset: 2px !important;
719
+ }
720
+
721
+ html[data-mode] main > div > div:has(> div > h1) > a svg,
722
+ html[data-mode] main > div > div:has(> div > h1) > button svg {
723
+ width: 14px !important;
724
+ height: 14px !important;
725
+ }
726
+
727
+ /* fields: WP's border (#8c8f94) comes in through Tailwind's own ring */
728
+ html[data-mode] main input.ring,
729
+ html[data-mode] main textarea.ring,
730
+ html[data-mode] main select.ring,
731
+ html[data-mode] main [role="combobox"].ring {
732
+ --tw-ring-color: var(--wp-field-line);
733
+ }
734
+
735
+ /* round 4: with the lighter resting border, the pointer restores the measured 3.245:1 */
736
+ html[data-mode] main input.ring:not(:disabled):hover,
737
+ html[data-mode] main textarea.ring:not(:disabled):hover,
738
+ html[data-mode] main select.ring:not(:disabled):hover,
739
+ html[data-mode] main [role="combobox"].ring:not(:disabled):hover {
740
+ --tw-ring-color: #8c8f94;
741
+ }
742
+ html[data-mode] main [role="combobox"].ring:not(:disabled):focus-visible {
743
+ --tw-ring-color: var(--wp-accent); /* combobox is a <button>, its focus comes only from the token */
744
+ }
745
+
746
+ html[data-mode] main input.ring:focus-visible,
747
+ html[data-mode] main textarea.ring:focus-visible,
748
+ html[data-mode] main select.ring:focus-visible {
749
+ --tw-ring-color: var(--wp-accent);
750
+ box-shadow: 0 0 0 1px var(--wp-accent), 0 0 0 4px var(--wp-accent-tint);
751
+ }
752
+
753
+ /* content links */
754
+ html[data-mode] main a {
755
+ -webkit-transition: color var(--wp-fast) ease, background-color var(--wp-fast) ease;
756
+ transition: color var(--wp-fast) ease, background-color var(--wp-fast) ease;
757
+ }
758
+
759
+ /* --------------------------------------------------------------------------
760
+ widgets = .postbox (border and radius already come from the tokens; here only the header
761
+ and the body without the second frame)
762
+ -------------------------------------------------------------------------- */
763
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline {
764
+ background: var(--wp-surface) !important;
765
+ overflow: hidden;
766
+ -webkit-transition: box-shadow var(--wp-normal) var(--wp-ease), -webkit-transform var(--wp-normal) var(--wp-ease);
767
+ transition: box-shadow var(--wp-normal) var(--wp-ease), transform var(--wp-normal) var(--wp-ease);
768
+ }
769
+
770
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline > div:first-child {
771
+ margin: 0 !important;
772
+ padding: 0 12px !important;
773
+ min-height: 36px !important;
774
+ border-bottom: 1px solid var(--wp-line) !important;
775
+ background: var(--wp-surface) !important;
776
+ color: var(--wp-text) !important;
777
+ }
778
+
779
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline > div:first-child h2,
780
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline > div:first-child h3 {
781
+ margin: 0 !important;
782
+ padding: 0 !important;
783
+ font-size: 14px !important;
784
+ font-weight: 600 !important;
785
+ line-height: 1.4 !important;
786
+ }
787
+
788
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline > .ring-kumo-fill,
789
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline > div:not(:first-child) {
790
+ --tw-ring-color: transparent; /* removes the inner frame, the Tailwind way */
791
+ margin: 0 !important;
792
+ padding: 12px !important;
793
+ border-radius: 0 !important;
794
+ background: var(--wp-surface) !important;
795
+ }
796
+
797
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline .text-3xl {
798
+ font-size: 24px !important;
799
+ font-weight: 600 !important;
800
+ line-height: 1.2 !important;
801
+ }
802
+
803
+ /* round 5 (2026-09-08): EDGE TO EDGE, the same rule already applied to the side menu.
804
+ The widget body loses its lateral breathing room WHEN it holds a list, and that room moves
805
+ inside the row itself. This way the hover highlight covers the card's full width, like
806
+ in wp-admin, instead of floating inset 12px on each side.
807
+
808
+ WHY `:has(> .space-y-1)` AND NOT THE WHOLE RULE: the same element (LayerCard.Primary,
809
+ anchored by .ring-kumo-fill) is also the body of the metric cards and of any
810
+ plugin widget, where the content has NO inset of its own and would touch the border. The
811
+ `.space-y-1` is the wrapper EmDash puts only around the Dashboard's link lists
812
+ (CollectionList and RecentActivity, admin/dist/index.js). Without :has(), nothing changes: it
813
+ degrades to today's inset.
814
+ The `space-y-1` also disappears: with a separator between rows, the 4px gap becomes a hole. */
815
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline > .ring-kumo-fill:has(> .space-y-1) {
816
+ padding: 0 !important;
817
+ }
818
+
819
+ /* round 5.1 (2026-09-08): `margin-block`, not `margin-top`. Measured in the fixture: the
820
+ Tailwind v4 EmDash compiles puts the `space-y-1` gap as `margin-block-end` on the
821
+ PREVIOUS element, not as margin-top on the next one, so `margin-top: 0` zeroed the half that
822
+ was already zero and a 4px gap remained between rows. That was the "empty space on hover":
823
+ the highlight band would stop and a white strip appeared before the next one. */
824
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline .space-y-1 > * {
825
+ margin-block: 0 !important;
826
+ }
827
+
828
+ /* list rows inside the widget */
829
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a[class*="justify-between"],
830
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a[class*="rounded-md"] {
831
+ /* 12px on the side = the same inset the body used to have, now inside the row */
832
+ padding: 8px 12px !important;
833
+ border-radius: 0 !important;
834
+ color: var(--wp-accent) !important;
835
+ border-bottom: 1px solid var(--wp-canvas);
836
+ }
837
+
838
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a[class*="justify-between"]:last-child,
839
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a[class*="rounded-md"]:last-child {
840
+ border-bottom: 0;
841
+ }
842
+
843
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a[class*="justify-between"]:hover,
844
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a[class*="rounded-md"]:hover {
845
+ color: var(--wp-accent-hover) !important;
846
+ background: var(--wp-accent-tint) !important;
847
+ }
848
+
849
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a .font-medium {
850
+ font-weight: 400 !important;
851
+ }
852
+
853
+ /* --------------------------------------------------------------------------
854
+ listing tables = .wp-list-table
855
+ -------------------------------------------------------------------------- */
856
+ html[data-mode] main table thead th {
857
+ background: var(--wp-surface) !important;
858
+ border-bottom: 1px solid var(--wp-line) !important;
859
+ font-weight: 400 !important;
860
+ padding: 8px 10px !important;
861
+ }
862
+
863
+ html[data-mode] main table tbody tr {
864
+ -webkit-transition: background-color var(--wp-fast) ease;
865
+ transition: background-color var(--wp-fast) ease;
866
+ }
867
+
868
+ html[data-mode] main table tbody tr:nth-child(odd) {
869
+ background: var(--wp-button-bg) !important;
870
+ }
871
+
872
+ html[data-mode] main table tbody tr:hover {
873
+ background: var(--wp-accent-tint) !important;
874
+ }
875
+
876
+ html[data-mode] main table tbody td {
877
+ border-top: 1px solid var(--wp-canvas) !important;
878
+ padding: 8px 10px !important;
879
+ vertical-align: middle !important;
880
+ }
881
+
882
+ html[data-mode] main table tbody td a[class*="font-medium"] {
883
+ font-weight: 600 !important;
884
+ }
885
+
886
+ /* ==========================================================================
887
+ [6] EDITOR'S RIGHT PANEL
888
+ This is another EmDash "sidebar". It stays in its own design system, it only gets
889
+ the border and surface background to match the rest.
890
+ ========================================================================== */
891
+ html[data-mode] aside[data-side="right"] > [data-sidebar="content-container"] {
892
+ background: var(--wp-surface) !important;
893
+ border-left: 1px solid var(--wp-line) !important;
894
+ box-shadow: -8px 0 24px -20px rgba(29, 35, 39, .25);
895
+ }
896
+
897
+ /* ==========================================================================
898
+ [7] SHADOW AND MOTION
899
+ ========================================================================== */
900
+
901
+ /* Elevated surface shadow (widgets, tables, settings blocks).
902
+ Four layers: 1px outline, contact, distant ambient and diffuse tip.
903
+
904
+ WHY `border-color: transparent` (measured on the panel on 2026-09-07):
905
+ the outline is drawn by the FIRST shadow layer (`0 0 0 1px`), and the targets arrive
906
+ here in two different border states:
907
+ - panel card: kumo's `ring`, `border-width: 0` -> nothing to erase
908
+ - table and settings: 1px `border` in `--color-kumo-line`, which is the SAME color
909
+ (#c3c4c7) as our first layer -> was drawing a DOUBLE 2px line
910
+ Zeroing the COLOR (not the width) keeps the box the same size, so nothing shifts, and
911
+ leaves a single 1px outline in both cases. The internal separator (`divide-y`) is not
912
+ affected: it lives on the children, not on this element.
913
+
914
+ GUARDS (2026-09-07 audit): `div.border.bg-kumo-base` is a painting utility, not a
915
+ contract, and it hits about 110 elements in the panel. Since the shadow is `!important`, it
916
+ was REPLACING the own elevation of anything that already had its own. Four families were
917
+ hit in production: the marketplace's loading skeleton (it pulsed with a real card's
918
+ shadow), the user detail skeleton (it doesn't carry the class, it inherits the pulsing parent,
919
+ hence `:not(.animate-pulse *)`), the text editor's floating bar and the plugin
920
+ consent dialog (they lost `shadow-lg`), and the Widgets drag layer.
921
+ The guard fails safe: if the class disappears in an upgrade, it stops excluding and the
922
+ behavior goes back to today's, never worse. Do NOT add `:has(.animate-pulse)`: that
923
+ would erase the shadow on the user list's skeleton table frame, where it's
924
+ correct. */
925
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline,
926
+ html[data-mode] main .overflow-x-auto.border:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]),
927
+ html[data-mode] main div.border.bg-kumo-base:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]) {
928
+ /* round 5.1 (2026-09-08, owner's preference): `border`, not `border-color`. With a 1px
929
+ TRANSPARENT border the card's own background paints underneath it (default background-clip
930
+ is border-box), so between the content and the shadow's first-layer hairline there's a 1px
931
+ gap of surface color left over: it's the "hairline one pixel away" that the table and the
932
+ settings blocks already had by accident (they arrive here with Tailwind's 1px `border`) and
933
+ that the Dashboard card didn't have (it arrives with `border-width: 0` and only the ring). One
934
+ word equalizes the technique across three families: metric cards, Dashboard widgets (Content
935
+ and Recent Activity), listing tables and settings blocks.
936
+ OUT OF SCOPE: the editor's right panel, which is a band, not a card (block [7] is
937
+ scoped to `main`, and its sections are <section>, not `div.border.bg-kumo-base`). */
938
+ border: 1px solid transparent !important;
939
+ box-shadow:
940
+ 0 0 0 1px var(--wp-line),
941
+ 0 1px 1px rgba(29, 35, 39, .03),
942
+ 0 32px 25px -35px rgba(29, 35, 39, .15),
943
+ 0 12px 28px -18px rgba(29, 35, 39, .22) !important;
944
+ }
945
+
946
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline:hover,
947
+ html[data-mode] main .overflow-x-auto.border:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]):hover,
948
+ html[data-mode] main div.border.bg-kumo-base:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]):hover {
949
+ box-shadow:
950
+ 0 0 0 1px var(--wp-line),
951
+ 0 1px 2px rgba(29, 35, 39, .04),
952
+ 0 36px 28px -34px rgba(29, 35, 39, .18),
953
+ 0 20px 40px -20px rgba(29, 35, 39, .28) !important;
954
+ }
955
+
956
+ /* only the card lifts on hover: table and settings block are not fully clickable */
957
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline:hover {
958
+ -webkit-transform: translateY(-1px);
959
+ transform: translateY(-1px);
960
+ }
961
+
962
+ /* entrances ------------------------------------------------------------- */
963
+ @-webkit-keyframes wp-rise {
964
+ from { opacity: 0; -webkit-transform: translateY(8px); transform: translateY(8px); }
965
+ to { opacity: 1; -webkit-transform: none; transform: none; }
966
+ }
967
+ @keyframes wp-rise {
968
+ from { opacity: 0; -webkit-transform: translateY(8px); transform: translateY(8px); }
969
+ to { opacity: 1; -webkit-transform: none; transform: none; }
970
+ }
971
+
972
+ @-webkit-keyframes wp-slide {
973
+ from { opacity: 0; -webkit-transform: translateX(-10px); transform: translateX(-10px); }
974
+ to { opacity: 1; -webkit-transform: none; transform: none; }
975
+ }
976
+ @keyframes wp-slide {
977
+ from { opacity: 0; -webkit-transform: translateX(-10px); transform: translateX(-10px); }
978
+ to { opacity: 1; -webkit-transform: none; transform: none; }
979
+ }
980
+
981
+ @-webkit-keyframes wp-fade {
982
+ from { opacity: 0; }
983
+ to { opacity: 1; }
984
+ }
985
+ @keyframes wp-fade {
986
+ from { opacity: 0; }
987
+ to { opacity: 1; }
988
+ }
989
+
990
+ /* load hierarchy: toolbar, then the menu item by item */
991
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header {
992
+ -webkit-animation: wp-fade .35s ease-out both;
993
+ animation: wp-fade .35s ease-out both;
994
+ }
995
+
996
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-item"] {
997
+ -webkit-animation: wp-slide .4s var(--wp-ease-out) both;
998
+ animation: wp-slide .4s var(--wp-ease-out) both;
999
+ -webkit-animation-delay: calc(.06s + var(--wp-i, 0) * 22ms);
1000
+ animation-delay: calc(.06s + var(--wp-i, 0) * 22ms);
1001
+ }
1002
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-item"]:nth-child(2) { --wp-i: 1; }
1003
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-item"]:nth-child(3) { --wp-i: 2; }
1004
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-item"]:nth-child(4) { --wp-i: 3; }
1005
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-item"]:nth-child(5) { --wp-i: 4; }
1006
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-item"]:nth-child(6) { --wp-i: 5; }
1007
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-item"]:nth-child(7) { --wp-i: 6; }
1008
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-item"]:nth-child(n+8) { --wp-i: 7; }
1009
+ /* each group enters a bit after the previous one */
1010
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="group"]:nth-child(2) [data-sidebar="menu-item"] { -webkit-animation-delay: calc(.14s + var(--wp-i, 0) * 22ms); animation-delay: calc(.14s + var(--wp-i, 0) * 22ms); }
1011
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="group"]:nth-child(3) [data-sidebar="menu-item"] { -webkit-animation-delay: calc(.30s + var(--wp-i, 0) * 22ms); animation-delay: calc(.30s + var(--wp-i, 0) * 22ms); }
1012
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="group"]:nth-child(n+4) [data-sidebar="menu-item"] { -webkit-animation-delay: calc(.46s + var(--wp-i, 0) * 22ms); animation-delay: calc(.46s + var(--wp-i, 0) * 22ms); }
1013
+
1014
+ /* content: title, then cards, then widgets; runs on every screen change
1015
+ because EmDash (an SPA) mounts the blocks again */
1016
+ html[data-mode] main > div > * {
1017
+ -webkit-animation: wp-rise .38s var(--wp-ease-out) both;
1018
+ animation: wp-rise .38s var(--wp-ease-out) both;
1019
+ }
1020
+ html[data-mode] main > div > *:nth-child(2) { -webkit-animation-delay: .06s; animation-delay: .06s; }
1021
+ html[data-mode] main > div > *:nth-child(3) { -webkit-animation-delay: .12s; animation-delay: .12s; }
1022
+ html[data-mode] main > div > *:nth-child(4) { -webkit-animation-delay: .18s; animation-delay: .18s; }
1023
+ html[data-mode] main > div > *:nth-child(5) { -webkit-animation-delay: .24s; animation-delay: .24s; }
1024
+ html[data-mode] main > div > *:nth-child(n+6) { -webkit-animation-delay: .30s; animation-delay: .30s; }
1025
+
1026
+ /* inside the widget, the rows arrive after the frame */
1027
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a[class*="justify-between"],
1028
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a[class*="rounded-md"],
1029
+ html[data-mode] main table tbody tr {
1030
+ -webkit-animation: wp-fade .3s ease-out both;
1031
+ animation: wp-fade .3s ease-out both;
1032
+ -webkit-animation-delay: calc(.22s + var(--wp-i, 0) * 30ms);
1033
+ animation-delay: calc(.22s + var(--wp-i, 0) * 30ms);
1034
+ }
1035
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a:nth-child(2), html[data-mode] main table tbody tr:nth-child(2) { --wp-i: 1; }
1036
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a:nth-child(3), html[data-mode] main table tbody tr:nth-child(3) { --wp-i: 2; }
1037
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a:nth-child(4), html[data-mode] main table tbody tr:nth-child(4) { --wp-i: 3; }
1038
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a:nth-child(5), html[data-mode] main table tbody tr:nth-child(5) { --wp-i: 4; }
1039
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a:nth-child(6), html[data-mode] main table tbody tr:nth-child(6) { --wp-i: 5; }
1040
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a:nth-child(7), html[data-mode] main table tbody tr:nth-child(7) { --wp-i: 6; }
1041
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a:nth-child(8), html[data-mode] main table tbody tr:nth-child(8) { --wp-i: 7; }
1042
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a:nth-child(n+9), html[data-mode] main table tbody tr:nth-child(n+9) { --wp-i: 8; }
1043
+
1044
+ /* browser view transitions: they apply when navigation reloads the
1045
+ document (login, returning from the site). Inside EmDash's SPA, a screen
1046
+ exit would need document.startViewTransition() in the router, which is JS. */
1047
+ @view-transition {
1048
+ navigation: auto;
1049
+ }
1050
+
1051
+ ::view-transition-old(root) {
1052
+ -webkit-animation: wp-vt-out .12s ease-out both;
1053
+ animation: wp-vt-out .12s ease-out both;
1054
+ }
1055
+
1056
+ ::view-transition-new(root) {
1057
+ -webkit-animation: wp-vt-in .24s var(--wp-ease-out) both;
1058
+ animation: wp-vt-in .24s var(--wp-ease-out) both;
1059
+ }
1060
+
1061
+ @-webkit-keyframes wp-vt-out { to { opacity: 0; } }
1062
+ @keyframes wp-vt-out { to { opacity: 0; } }
1063
+ @-webkit-keyframes wp-vt-in {
1064
+ from { opacity: 0; -webkit-transform: translateY(4px); transform: translateY(4px); }
1065
+ }
1066
+ @keyframes wp-vt-in {
1067
+ from { opacity: 0; -webkit-transform: translateY(4px); transform: translateY(4px); }
1068
+ }
1069
+
1070
+ /* anyone who turned off system animations doesn't see any of this */
1071
+ @media (prefers-reduced-motion: reduce) {
1072
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header,
1073
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-item"],
1074
+ html[data-mode] main > div > *,
1075
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline a,
1076
+ html[data-mode] main table tbody tr {
1077
+ -webkit-animation: none !important;
1078
+ animation: none !important;
1079
+ }
1080
+ /* FIX 8 (audit): in a separate rule. A browser without ::view-transition (Safari 17,
1081
+ Firefox ESR) invalidates the WHOLE list, and anyone who asked for less motion would receive all
1082
+ the animations. Here the worst case is losing only these two lines. */
1083
+ ::view-transition-old(root),
1084
+ ::view-transition-new(root) {
1085
+ -webkit-animation: none !important;
1086
+ animation: none !important;
1087
+ }
1088
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline:hover {
1089
+ -webkit-transform: none;
1090
+ transform: none;
1091
+ }
1092
+ }
1093
+
1094
+ /* content scrollbar: REMOVED in round 3 (2026-09-08). The track disappears,
1095
+ scrolling stays. The live rules are in block [R3.1] at the end of the file. */
1096
+
1097
+ /* ==========================================================================
1098
+ ROUND 2 OF ADJUSTMENTS (2026-09-07)
1099
+ Everything here was born from measuring the panel's bundle and source maps
1100
+ (@emdash-cms/admin 0.36.0, @cloudflare/kumo 2.6.0, embedded Base UI),
1101
+ never from reading a screenshot. Each rule states which CONTRACT it anchored on:
1102
+ a Base UI attribute, HTML semantics or a named class that kumo writes by
1103
+ hand. A Tailwind utility class only appears where no contract exists,
1104
+ and always with an upgrade-watch comment.
1105
+
1106
+ GOLDEN RULE OF THIS FILE: whoever positions a popup is Floating UI, via JS,
1107
+ in the Positioner. No rule below writes transform, position, inset or
1108
+ overflow on a popup. The only displacement used is the `translate` property,
1109
+ which is independent of `transform` and stays outside the popup's transition.
1110
+ ========================================================================== */
1111
+
1112
+
1113
+ /* ==========================================================================
1114
+ [R] LOG OF THE TWO REMOVALS IN THIS ROUND (already applied above)
1115
+ Kept here because the next person will want to know why they disappeared.
1116
+
1117
+ R1. The forced alignment on the page header. Exact snippet to
1118
+ look for (the comment that went with it also goes):
1119
+
1120
+ html[data-mode] main > div > div:has(> div > h1) {
1121
+ justify-content: flex-start !important;
1122
+ gap: 12px !important;
1123
+ }
1124
+
1125
+ WHY IT'S GONE: it matched whenever the h1 lives inside an intermediate
1126
+ div (subtitle, language selector, Media's min-w-0) and
1127
+ swapped EmDash's `justify-between` for flex-start. Result: the
1128
+ action rose to the middle of the row and hung next to the title,
1129
+ with the subtitle below it. The only two correct screens were Users and
1130
+ Dashboard, precisely the ones that did NOT match (h1 is a direct child). EmDash
1131
+ already handles both cases with justify-between: the right call here is to have no rule.
1132
+ STATE: already deleted in this file's working tree. What's still
1133
+ operational: `dist/client/painel-wp.css` still ships the old version,
1134
+ so it needs a build before deploy, otherwise the stale CSS goes live.
1135
+
1136
+ R2. The two selector lists in block [7] (elevated surface shadow)
1137
+ need a guard. Look for these three lines:
1138
+
1139
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline,
1140
+ html[data-mode] main .overflow-x-auto.border,
1141
+ html[data-mode] main div.border.bg-kumo-base {
1142
+
1143
+ and swap them for:
1144
+
1145
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline,
1146
+ html[data-mode] main .overflow-x-auto.border:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]),
1147
+ html[data-mode] main div.border.bg-kumo-base:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]) {
1148
+
1149
+ and do the same on the :hover list right below (same guards, with
1150
+ `:hover` at the end of each guarded line).
1151
+
1152
+ WHY: `div.border.bg-kumo-base` is a painting utility, not a
1153
+ contract, and it hits about 110 elements in the panel, not just table
1154
+ and settings block. Since the shadow is `!important`, it REPLACES the
1155
+ own elevation of anything that already had its own. Measured, four families were
1156
+ hit today, in production: the marketplace and theme loading skeleton
1157
+ (`animate-pulse ... border bg-kumo-base`), which pulsed with a
1158
+ real card's shadow; the user detail skeleton card, which doesn't carry
1159
+ the class but inherits the pulsing parent (hence
1160
+ `:not(.animate-pulse *)`); the text editor's floating bar and the
1161
+ plugin consent dialog, which lost `shadow-lg`; and the
1162
+ Widgets drag layer, which went flat during the drag.
1163
+ The guard fails safe: if the class disappears in an upgrade, it stops
1164
+ excluding and the behavior goes back to today's, never worse.
1165
+ Do NOT add `:has(.animate-pulse)`: that would erase the shadow on the
1166
+ UserList skeleton table frame, where the shadow is correct.
1167
+ ========================================================================== */
1168
+
1169
+
1170
+ /* ==========================================================================
1171
+ [8] USER MENU IN THE BAR = #wp-admin-bar-my-account .ab-sub-wrapper
1172
+
1173
+ The user menu is NOT a DropdownMenu: it's a kumo Popover
1174
+ (Header.tsx:66-113, `<Popover.Content className="w-56 p-2" align="end">`)
1175
+ with a PORTAL into <body>. Real DOM chain:
1176
+ <div data-base-ui-portal> direct child of <body>
1177
+ <div role="presentation" ...> Positioner, transform inline via JS
1178
+ <div role="dialog" class="... kumo-popover-popup"> Popup
1179
+
1180
+ In other words: the popup does NOT live inside the <header>, so no ancestor
1181
+ reaches it, and the toolbar's token bridge (block [1], lines 165-173) doesn't
1182
+ reach it. That's why the tokens are redeclared here, on the element itself,
1183
+ exactly the same trick already used in the side menu (lines 148-162).
1184
+
1185
+ HOOK: the security link, which only exists in this popover.
1186
+ `.kumo-popover-popup` alone is NOT enough: it's a named class kumo
1187
+ writes by hand (popover.tsx:217) on EVERY Popover.Content, including the
1188
+ content ones (BylineFilter, ContentList's date range, byline selector,
1189
+ code block language, PortableTextEditor's link editor). The href
1190
+ comes from TanStack Router with basepath "/_emdash/admin", so
1191
+ "/_emdash/admin/settings/security"; we use `*=` and not `$=` to survive
1192
+ a future trailingSlash or querystring. The other three occurrences of
1193
+ /settings/security in the bundle are the Settings page nav, the command
1194
+ palette item and the route definition: none of them inside a popover.
1195
+ The hook keeps holding during the exit animation, because the link only
1196
+ unmounts together with the popup.
1197
+
1198
+ WHY NOT THE TRIGGER: it would need a `:has()` anchored on <html> with a
1199
+ descendant argument, re-evaluated on every DOM mutation in a panel whose
1200
+ editor changes the DOM on every keystroke, and today it wouldn't catch anything
1201
+ besides this same menu (the theme toggle is a plain Button, the palette is CommandPalette).
1202
+ WATCH: if EmDash gains a SECOND menu in the bar, extend this
1203
+ selector with that menu's content hook, never with :has() at the root.
1204
+ ========================================================================== */
1205
+ html[data-mode] [role="dialog"].kumo-popover-popup:has(a[href*="/settings/security"]) {
1206
+ /* token bridge: custom properties inherit, so children read them without needing
1207
+ their own selector */
1208
+ --text-color-kumo-default: var(--wp-ink-text);
1209
+ --text-color-kumo-subtle: var(--wp-ink-muted);
1210
+ --text-color-kumo-strong: var(--wp-accent-contrast);
1211
+ --text-color-kumo-link: var(--wp-accent-soft);
1212
+ /* 8.0:1 on #1d2327 and 6.5:1 on the hover #2c3338. The #b32d2e from line 30
1213
+ was tuned for a WHITE surface and measures 2.5:1 on the dark ink, meaning it
1214
+ would fail AA right on the "Sair" item. */
1215
+ --text-color-kumo-danger: #f4a3a3;
1216
+ --color-kumo-base: var(--wp-ink);
1217
+ --color-kumo-tint: var(--wp-ink-hover); /* hover:bg-kumo-tint on the items */
1218
+ --color-kumo-line: var(--wp-ink-line);
1219
+ --color-kumo-hairline: var(--wp-ink-line);
1220
+ --color-kumo-fill: var(--wp-ink-line); /* outline outline-kumo-fill */
1221
+ /* the arrow is drawn by three <path>s: the body uses fill-kumo-base, already
1222
+ darkened above, but the border uses fill-kumo-tip-shadow, which under the
1223
+ `color-scheme: light` from line 26 resolves to light gray. Without these
1224
+ two lines a light hairline would remain around it. */
1225
+ --color-kumo-tip-shadow: var(--wp-ink-line);
1226
+ --color-kumo-tip-stroke: transparent;
1227
+
1228
+ background-color: var(--wp-ink) !important;
1229
+ color: var(--wp-ink-text) !important;
1230
+ outline-color: var(--wp-ink-line) !important;
1231
+ /* round 3: the radius is now asymmetric and the shadow has four layers. Both things
1232
+ live in block [16], which comes later and wins by source order. Here only what didn't change remains. */
1233
+ }
1234
+
1235
+ /* [8b] STUCK TO THE BAR. kumo asks for `sideOffset: 8` (popover.tsx:189) and
1236
+ Floating UI applies that to the Positioner's TRANSFORM, which is forbidden to touch.
1237
+ The `translate` property is independent of `transform`, composes before it,
1238
+ doesn't enter the popup's `transition-[transform,scale,opacity]` (so it applies
1239
+ instantly, without animating) and doesn't change the layout box that Floating UI
1240
+ measures. The popup also doesn't use its own `transform`: Tailwind v4 compiles
1241
+ `scale-90` into the standalone `scale` property, so there's no collision with the
1242
+ entry and exit animation.
1243
+ Both directions are covered because flip() throws the menu upward in a
1244
+ short window. The bar has 32px forced on line 179, the popup used to be born at 40px
1245
+ and now returns to 32px, flush, without overlapping the trigger.
1246
+ Older Safari ignores `translate` and falls back to the 8px gap: clean degradation.
1247
+ The 8px fallback is deliberate: if the number should ever live in a single
1248
+ token, just create `--wp-popover-offset: 8px` in block [0] and nothing else changes. */
1249
+ /* round 3: this pair is gone. `translate` is ONE property, so the X and Y axes need
1250
+ to be written together, and the new pair (with the 5px that Base UI's shift() steals on the
1251
+ right) is in block [16a]. Keeping both pairs would make the lower one silently erase the upper one. */
1252
+
1253
+ /* [8c] THE ARROW DISAPPEARS. WordPress has no beak on the bar's submenu, and with the menu
1254
+ flush it would paint a triangle right on top of the bar itself.
1255
+ ANCHOR: kumo renders <Popover.Arrow> as the popup's first child and
1256
+ Base UI delivers it as <div aria-hidden="true" data-side ...>. No other
1257
+ direct child of the popup has the aria-hidden + data-side pair.
1258
+ WHY `visibility` AND NOT `display: none`: the arrow element is read by JS
1259
+ at three points, not one (the shift limiter, which IS positioning; the
1260
+ --transform-origin calculation; and Floating UI's arrow() middleware, which
1261
+ measures getDimensions). `display: none` zeroes all three and changes geometry;
1262
+ `visibility: hidden` keeps the measured 20x10 and leaves everything identical to
1263
+ today, just out of the paint. The arrow is decorative (aria-hidden), so leaving
1264
+ the paint is enough and there's no accessibility cost.
1265
+ `!important` because the arrow carries Tailwind's `flex` utility. */
1266
+ html[data-mode] [role="dialog"].kumo-popover-popup:has(a[href*="/settings/security"])
1267
+ > [aria-hidden="true"][data-side] {
1268
+ visibility: hidden !important;
1269
+ }
1270
+
1271
+ /* [8d] INTERNAL BORDERS. Tailwind v4's preflight leaves `border: 0 solid`,
1272
+ so the identity block's `border-b` and the <hr> before "Sair" inherit
1273
+ currentColor: over dark ink that turns into a glaring white streak.
1274
+ The rule is SCOPED to the menu on purpose. The generic version (a `*` applying
1275
+ to every `.kumo-popover-popup`) has specificity (0,3,1) and would override
1276
+ any `border-kumo-*` utility (0,1,0) inside ANY
1277
+ popover: measured, it would break the `border-t border-kumo-line` on the
1278
+ ContentList's date selector footer, swapping a 10%-black hairline for a solid
1279
+ black line. */
1280
+ html[data-mode] [role="dialog"].kumo-popover-popup:has(a[href*="/settings/security"])
1281
+ :is(div, hr) {
1282
+ border-color: var(--wp-ink-line);
1283
+ }
1284
+
1285
+ /* [8e] OPTIONAL, just finishing, and it does NOT darken anything. The CONTENT menus
1286
+ are already light on their own, by construction: neither Select (popup role="presentation"
1287
+ with a role="listbox" inside) nor DropdownMenu (popup role="menu") carries the
1288
+ kumo-popover-popup class, and the content popover doesn't satisfy the [8]
1289
+ hook. This here is the WP card applied to those two families, so they
1290
+ don't end up with kumo's light shadow next to the listings.
1291
+
1292
+ MEASURED PITFALL: in Tailwind v4 the `ring ring-kumo-line` these two
1293
+ popups use is NOT its own property, it's drawn INSIDE box-shadow.
1294
+ Overwriting box-shadow without re-adding the layer erases the 1px border and the menu
1295
+ becomes a white card with no outline over a white surface. That's why the line
1296
+ comes back explicit as the FIRST layer, in the same color the ring already had.
1297
+ Don't repeat `border-radius` or `--color-kumo-line` here: both already apply
1298
+ by inheritance from block [1] and would be a no-op. */
1299
+ html[data-mode] [data-base-ui-portal] [role="menu"],
1300
+ html[data-mode] [data-base-ui-portal] [role="presentation"]:has(> [role="listbox"]) {
1301
+ box-shadow:
1302
+ 0 0 0 1px var(--wp-line),
1303
+ 0 3px 5px rgba(0, 0, 0, .2) !important;
1304
+ }
1305
+
1306
+
1307
+ /* [8f] EDGE TO EDGE (round 5, 2026-09-08). Same rule as the side menu: the item occupies
1308
+ the popup's full width and the highlight has no rounded corner.
1309
+
1310
+ WHAT WAS HAPPENING, measured in the fixture with the panel's real CSS: the user
1311
+ menu's `Popover.Content` is born with `px-4 py-3` from kumo's base plus the
1312
+ `w-56 p-2` that EmDash's Header passes, and `px-4` WINS over `p-2` in the Tailwind
1313
+ v4 cascade (padding-inline after padding). Measured result: 12px 16px padding on the popup and
1314
+ 3px border-radius on the items, meaning every hover floated inset 16px on each side.
1315
+
1316
+ `overflow: hidden` because, without padding, the first and last item's highlight would
1317
+ spill over the popup's rounded corner.
1318
+ Don't touch the arrow: it's already out of the picture in [8c]. */
1319
+ html[data-mode] [role="dialog"].kumo-popover-popup:has(a[href*="/settings/security"]) {
1320
+ padding: 0 !important;
1321
+ overflow: hidden;
1322
+ }
1323
+
1324
+ /* the identity block (name + email) is the only child with border-b */
1325
+ html[data-mode] [role="dialog"].kumo-popover-popup:has(a[href*="/settings/security"])
1326
+ > div.border-b {
1327
+ margin-bottom: 0 !important;
1328
+ padding: 10px 12px !important;
1329
+ }
1330
+
1331
+ /* the item grid loses the gap: flush rows, the wp-admin way */
1332
+ html[data-mode] [role="dialog"].kumo-popover-popup:has(a[href*="/settings/security"])
1333
+ > div.grid {
1334
+ gap: 0 !important;
1335
+ }
1336
+
1337
+ html[data-mode] [role="dialog"].kumo-popover-popup:has(a[href*="/settings/security"])
1338
+ :is(a, button) {
1339
+ border-radius: 0 !important;
1340
+ padding: 8px 12px !important;
1341
+ }
1342
+
1343
+ /* the <hr> before "Sair" runs corner to corner (its color already comes from [8d]) */
1344
+ html[data-mode] [role="dialog"].kumo-popover-popup:has(a[href*="/settings/security"]) hr {
1345
+ margin: 0 !important;
1346
+ }
1347
+
1348
+ /* [8g] THE SAME FOR THE MENUS PORTED FROM KUMO (DropdownMenu, Select, Combobox,
1349
+ Autocomplete). Measured in the installed package: DropdownMenu's popup is born `min-w-36 p-1.5`
1350
+ with item `rounded-md px-2 py-1.5` and separator `-mx-1 my-1`; Select and Autocomplete
1351
+ swap the popup's padding for `mx-1.5` on EACH item. Both forms produce the same look:
1352
+ an inset highlight with a round corner. Both die here.
1353
+
1354
+ SCOPE: only menu items and list options (their own `role`). `margin-inline` is NOT zeroed
1355
+ on just any child on purpose: the Combobox's search field is `mx-1.5` with
1356
+ `w-[calc(100%-0.75rem)]`, so zeroing its margin would leave 12px of slack on the right. */
1357
+ html[data-mode] [data-base-ui-portal] [role="menu"],
1358
+ html[data-mode] [data-base-ui-portal] [role="listbox"] {
1359
+ padding: 0 !important;
1360
+ overflow: hidden;
1361
+ }
1362
+
1363
+ html[data-mode] [data-base-ui-portal] :is([role="menuitem"], [role="menuitemcheckbox"],
1364
+ [role="menuitemradio"], [role="option"]) {
1365
+ border-radius: 0 !important;
1366
+ margin-inline: 0 !important;
1367
+ padding-inline: 12px !important;
1368
+ }
1369
+
1370
+ /* separator: `-mx-1` left 2px on each side inside the `p-1.5`; with no padding on the popup it
1371
+ just needs to stop pulling outward. Two selectors because kumo draws the separator
1372
+ sometimes with a role, sometimes as a plain <div class="h-px">. */
1373
+ html[data-mode] [data-base-ui-portal] :is([role="menu"], [role="listbox"]) [role="separator"],
1374
+ html[data-mode] [data-base-ui-portal] :is([role="menu"], [role="listbox"]) > .h-px {
1375
+ margin-inline: 0 !important;
1376
+ }
1377
+
1378
+
1379
+ /* ==========================================================================
1380
+ [9] MODAL SCRIM = wp-admin's overlay (rgba(0,0,0,.7))
1381
+
1382
+ Two of the panel's scrims LIGHTEN instead of darkening, and both come from our
1383
+ own token bridge in block [1]:
1384
+ Dialog and AlertDialog: `bg-kumo-recessed opacity-80` (dialog.tsx:231), and
1385
+ --color-kumo-recessed becomes var(--wp-line-soft) = #dcdcde (line 109).
1386
+ Command palette: `bg-kumo-overlay opacity-80` (command-palette.tsx:164,
1387
+ mounted in the Shell), and --color-kumo-overlay becomes var(--wp-surface) =
1388
+ #ffffff (line 105). This is the worst one: a WHITE scrim at 80%.
1389
+ The same defect was already fixed in the mobile drawer (FIX 3, line 537) and wasn't
1390
+ propagated. It can't be fixed at the token level: bg-kumo-recessed and bg-kumo-overlay
1391
+ also paint recessed surfaces on the menubar, tabs, select item, combobox
1392
+ and sidebar.
1393
+
1394
+ The hook has two halves, STRUCTURE and the WRONG CLASS:
1395
+ [data-base-ui-portal] > ... every Base UI portal is a <div
1396
+ data-base-ui-portal> on <body>, and the scrim is a
1397
+ DIRECT child of it. The ">" matters: the Select
1398
+ popup is also role="presentation", but it lives
1399
+ inside the Positioner.
1400
+ role="presentation" the DialogBackdrop's contract.
1401
+ [data-open] / [data-closed] both states, otherwise the scrim would flash
1402
+ light again during the 150ms exit animation, when
1403
+ Base UI swaps data-open for data-closed.
1404
+ :is(.bg-kumo-recessed, ONLY the scrims painted with a light token. Without this
1405
+ .bg-kumo-overlay) half the rule would also catch the user detail
1406
+ scrim, which is already `bg-black/50` and does NOT have
1407
+ opacity-80: its alpha lives in the
1408
+ background-color itself, so overwriting it would give
1409
+ a 100% opaque screen instead of a scrim.
1410
+ MEASURED ON THE PANEL ON 2026-09-07, and it dropped a guard the audit asked for:
1411
+ the previous version of this rule had `:not([data-base-ui-inert])`, to exclude
1412
+ the InternalBackdrop (invisible click blocker, also a direct child of the
1413
+ portal). Except the one carrying `data-base-ui-inert` is the VISIBLE scrim, not the
1414
+ blocker: with the guard, the rule matched ZERO elements and the background kept
1415
+ lightening. What actually separates the two is the `.bg-kumo-recessed` class, which the
1416
+ blocker doesn't have. Verified in the DOM with the "Criar novo menu" modal open:
1417
+ the selector below matches exactly 1 element, 1223 by 879 pixels.
1418
+
1419
+ Don't touch opacity: kumo animates .8 to 0 on exit, and an !important there
1420
+ would kill the fade and leave the scrim opaque until unmount. A lighter scrim is made in the
1421
+ ink's alpha, never in the property kumo uses to animate.
1422
+ POPOVER HAS NO SCRIM: kumo doesn't render Popover.Backdrop, only Dialog,
1423
+ AlertDialog and the drawer. The bar popover's darkening is block [8].
1424
+ ========================================================================== */
1425
+ html[data-mode] [data-base-ui-portal] > [role="presentation"]:is([data-open], [data-closed]):is(.bg-kumo-recessed, .bg-kumo-overlay) {
1426
+ background-color: var(--wp-ink) !important; /* #1d2327 at 80% = WP's rgba(0,0,0,.7) */
1427
+ }
1428
+
1429
+
1430
+ /* ==========================================================================
1431
+ [10] EDITOR'S RIGHT PANEL = METABOXES
1432
+
1433
+ There's no conflict with block [6]: it paints the <div data-sidebar="content-container">
1434
+ and the rules here paint DESCENDANTS of that div. They're different elements,
1435
+ not the same declaration fighting.
1436
+
1437
+ ANCHOR: <section data-sorting data-disclosure>, emitted by
1438
+ SortableContentSettingsSection. That PAIR of attributes is exclusive to this
1439
+ component across the whole admin, so there's no positional chain here. The drag
1440
+ handle is [data-sortable-handle] and the collapsible header is
1441
+ button[aria-expanded] (Base UI's Collapsible.Trigger, which emits
1442
+ aria-expanded in both states): also an attribute.
1443
+ FORBIDDEN in this section, and it's not a preference: `:nth-child` to find a block.
1444
+ The order is draggable and gets saved per user and per collection in localStorage,
1445
+ so a child position would paint the wrong block after the client's first drag. And no
1446
+ `transform` or `position` on the section: dnd-kit writes an inline
1447
+ transform during the drag and the handle is absolute inside it.
1448
+
1449
+ TWO NUMBERS TO WATCH ON EMDASH UPGRADE:
1450
+ 1. the -16px inset mirrors the `p-4` EmDash puts on the section's child.
1451
+ If it becomes p-5, the inset becomes -20px.
1452
+ 2. the title's DEPTH. Measured today, the h3 is exactly two
1453
+ levels below the section:
1454
+ section > div.p-4 > h3 (Publicar, Propriedade, SEO, plugins)
1455
+ section > div.p-4 > div > h3 (Traducoes, Taxonomias)
1456
+ section > div.p-4 > div.flex > h3 (Bylines)
1457
+ The rules below lock onto these two levels on purpose. A loose
1458
+ descendant (`section h3`) would also reach the INTERNAL h3 of a
1459
+ plugin panel, which is third-party markup: there the negative inset would throw the
1460
+ plugin's title outside the card, and since the tree above is overflow-x-hidden
1461
+ the result is CLIPPED text. Gaining a wrapper level in an upgrade, the
1462
+ title loses the band (silent degradation, doesn't break) and the fix is
1463
+ adding `> div` to the chain.
1464
+ RTL WATCH: like block [6], these rules use data-side="right",
1465
+ which becomes "left" in an RTL interface. The panel is pt-BR, so it applies today.
1466
+ ========================================================================== */
1467
+
1468
+ /* the gap between the boxes becomes WP's gray screen.
1469
+ Two lines on purpose: the first catches the sections' direct wrapper
1470
+ (ContentSettingsPanel's root), the second catches the scroll area, otherwise
1471
+ a white band is left below the last box. The content-container stays white
1472
+ (block [6]) so the top action bar keeps sitting on a surface. */
1473
+ /* round 5 (2026-09-08): the gray gap is GONE. The owner asked for the end of the floating
1474
+ metabox look: the sections become flush bands, separated only by a hairline, and with no gap
1475
+ there's no gray screen to paint. Background goes back to being a surface. */
1476
+ html[data-mode] :is(aside, nav)[data-side="right"] div:has(> section[data-sorting]),
1477
+ html[data-mode] :is(aside, nav)[data-side="right"] div.overflow-y-auto:has(section[data-sorting]) {
1478
+ background: var(--wp-surface) !important;
1479
+ }
1480
+
1481
+ /* the scrollbar OVERLAYS, and the only way to do that is to not draw one at all.
1482
+ Measured and already noted in round 4 (line ~1880): "only shows up while scrolling" isn't CSS, it's a
1483
+ system preference; with "Always" on macOS any visible bar takes up layout
1484
+ space, and that's what showed up as a dark strip next to "Publicar". Hidden,
1485
+ scrolling stays fully working (wheel, trackpad, keyboard, drag), exactly as it already is on the
1486
+ side menu since round 3. ACCEPTED COST: the permanent hint that there's more
1487
+ content below disappears. */
1488
+ html[data-mode] :is(aside, nav)[data-side="right"] div.overflow-y-auto {
1489
+ scrollbar-width: none !important;
1490
+ -ms-overflow-style: none;
1491
+ }
1492
+
1493
+ html[data-mode] :is(aside, nav)[data-side="right"] div.overflow-y-auto::-webkit-scrollbar {
1494
+ display: none;
1495
+ width: 0;
1496
+ height: 0;
1497
+ }
1498
+
1499
+ /* the box */
1500
+ html[data-mode] section[data-sorting][data-disclosure] {
1501
+ /* NOT OPTIONAL: React sets `inline-size: 100%` as an INLINE style. With
1502
+ the 12px side margin that gives 100% + 24px inside a parent that is
1503
+ overflow-x-hidden: it doesn't become a scrollbar (the visual gate would slip past unnoticed), it becomes a
1504
+ CLIP, and every box's right border disappears. */
1505
+ inline-size: auto !important;
1506
+ /* round 5: ZERO margin. The box becomes a band: no corner, no shadow, no side border, just
1507
+ the top hairline separating one section from the next (EmDash already emits `border-t` with
1508
+ `first:border-t-0`, so the first one doesn't get a doubled hairline). The alignment with the
1509
+ action bar's buttons no longer comes from the margin: it comes from the `p-4` EmDash itself puts
1510
+ on the section's child, which is the same 16px as that bar's `px-4`. */
1511
+ margin: 0 !important;
1512
+ border: 0 !important;
1513
+ border-top: 1px solid var(--wp-line) !important;
1514
+ border-radius: 0 !important;
1515
+ background: var(--wp-surface) !important;
1516
+ box-shadow: none !important;
1517
+ }
1518
+
1519
+ /* `:first-of-type` and not `:first-child`: dnd-kit's DndContext renders the
1520
+ HiddenText/LiveRegion pair as a sibling of the sections, with no portal. Today it comes out
1521
+ AFTER, so `:first-child` would work; `:first-of-type` stays correct if
1522
+ it ever comes out before. */
1523
+ html[data-mode] section[data-sorting][data-disclosure]:first-of-type {
1524
+ margin-top: 0 !important;
1525
+ border-top: 0 !important; /* the action bar above already closes with its own hairline */
1526
+ }
1527
+
1528
+ /* header band: the title is a loose <h3> (Publicar, SEO, Taxonomias,
1529
+ Propriedade, Traducoes, plugin panels). Two levels because the
1530
+ depth changes from block to block (see note 2 above). */
1531
+ html[data-mode] section[data-sorting] > div > h3,
1532
+ html[data-mode] section[data-sorting] > div > div > h3 {
1533
+ display: block;
1534
+ margin: -16px -16px 16px !important;
1535
+ padding-block: 8px;
1536
+ /* round 5: 16px, not 12px. With the section edge to edge and its body in `p-4`, the
1537
+ title only lines up with the field below and with the action bar's button if it uses the
1538
+ same 16px. 40px still reserves room for the drag handle. */
1539
+ padding-inline: 16px 40px;
1540
+ border-bottom: 1px solid var(--wp-line);
1541
+ background: var(--wp-button-bg);
1542
+ color: var(--wp-text);
1543
+ font-size: 14px !important;
1544
+ font-weight: 400 !important;
1545
+ line-height: 1.4 !important;
1546
+ }
1547
+
1548
+ /* "Bylines" wraps the title in a flex row alongside a help button: there
1549
+ the band is the WHOLE row, and the h3 goes back to being just text.
1550
+ The `flex` class is the discriminator because `div:has(> h3)` without the class would also
1551
+ catch the Traducoes panel's root, which wraps the h3 AND the whole list:
1552
+ the panel would become one giant band. If EmDash removes `flex` from that row,
1553
+ Bylines loses the band (silent degradation, doesn't break). */
1554
+ html[data-mode] section[data-sorting] > div > div.flex:has(> h3) {
1555
+ margin: -16px -16px 16px !important;
1556
+ padding-block: 8px !important;
1557
+ padding-inline: 16px 40px !important; /* round 5: same ruler as the loose h3 */
1558
+ border-bottom: 1px solid var(--wp-line);
1559
+ background: var(--wp-button-bg);
1560
+ }
1561
+
1562
+ html[data-mode] section[data-sorting] > div > div.flex:has(> h3) > h3 {
1563
+ margin: 0 !important;
1564
+ padding: 0 !important;
1565
+ border: 0 !important;
1566
+ background: none !important;
1567
+ }
1568
+
1569
+ /* collapsible header (Sumario, Revisoes) = Collapsible.Trigger. EmDash sets
1570
+ `width: calc(100% - 1.5rem)` and `inset-inline-start: -0.75rem` INLINE: only
1571
+ !important wins. */
1572
+ html[data-mode] section[data-disclosure="true"] button[aria-expanded] {
1573
+ width: calc(100% + 32px) !important;
1574
+ inset-inline-start: 0 !important;
1575
+ margin: -16px -16px 12px !important;
1576
+ padding-block: 8px !important;
1577
+ padding-inline: 12px 40px !important;
1578
+ height: auto !important;
1579
+ min-height: 36px !important;
1580
+ border-radius: 0 !important;
1581
+ border-bottom: 1px solid var(--wp-line) !important;
1582
+ background: var(--wp-button-bg) !important;
1583
+ }
1584
+
1585
+ /* The label does NOT inherit the button's typography: it's a <Text bold as="span"> and
1586
+ kumo puts `text-base font-medium` on the span ITSELF. Inheritance doesn't beat the
1587
+ element's own class. With `--text-base: 13px` (line 123) and
1588
+ `--font-weight-medium: 500`, "Sumario" and "Revisoes" would come out 13px/500
1589
+ sitting next to seven 14px/400 titles, in the same column. */
1590
+ html[data-mode] section[data-disclosure="true"] button[aria-expanded] span {
1591
+ font-size: 14px !important;
1592
+ font-weight: 400 !important;
1593
+ line-height: 1.4 !important;
1594
+ }
1595
+
1596
+ html[data-mode] section[data-disclosure="true"] button[aria-expanded]:hover {
1597
+ background: var(--wp-accent-tint) !important;
1598
+ }
1599
+
1600
+ /* the drag handle now lives inside the band.
1601
+ `>` on purpose and safe: the credits editor also emits
1602
+ [data-sortable-handle] on the credit rows, but there the button is a GRANDCHILD of the
1603
+ section; the DIRECT child is only the section's own. */
1604
+ html[data-mode] section[data-sorting="false"] > button[data-sortable-handle] {
1605
+ top: 4px !important; /* round 5.2: (36.6 - 28) / 2, the same math as the other two */
1606
+ inset-inline-end: 6px !important;
1607
+ border-radius: 3px !important; /* round 5.2: same as the (i) and the "+", see block [21] */
1608
+ }
1609
+
1610
+ /* while dragging, EmDash hides the content and shows [data-sortable-heading] */
1611
+ html[data-mode] section[data-sorting="true"] [data-sortable-heading] {
1612
+ min-height: 36px !important; /* EmDash sets 48 inline */
1613
+ padding-block: 8px !important;
1614
+ padding-inline: 12px 40px !important;
1615
+ background: var(--wp-button-bg);
1616
+ }
1617
+
1618
+ html[data-mode] section[data-sorting="true"] [data-sortable-heading] span {
1619
+ font-size: 14px !important; /* EmDash sets text-[15px] on the span */
1620
+ font-weight: 400 !important;
1621
+ line-height: 1.4 !important;
1622
+ }
1623
+
1624
+ /* the delete block, at the panel's foot, uses the same framing (round 5.1: the
1625
+ framing is now a band, not a box. With the sections already edge to edge, this was the
1626
+ last floating card in the right panel: a frame was left around the
1627
+ "Mover para a lixeira" button, inside another frame. Only the top hairline remains, like the sections. */
1628
+ html[data-mode] [data-testid="content-trash-actions"] {
1629
+ margin: 0 !important;
1630
+ border: 0 !important;
1631
+ border-top: 1px solid var(--wp-line) !important;
1632
+ border-radius: 0 !important;
1633
+ padding: 12px 16px !important;
1634
+ background: var(--wp-surface) !important;
1635
+ }
1636
+
1637
+
1638
+ /* ==========================================================================
1639
+ [11] LISTING TABLE STATE CELL (empty, loading, error)
1640
+
1641
+ The squeeze was OURS, not EmDash's: it sets `py-8` (32px top and
1642
+ bottom) on those cells, and the table block's `main table tbody td { padding: 8px 10px
1643
+ !important }` rule (line 824) erases it. Here the breathing room comes back,
1644
+ and ONLY where it's a state, never on a data row.
1645
+
1646
+ ANCHOR: td[colspan] (an attribute that doesn't disappear, it's what makes the cell span the
1647
+ table) plus the breathing-room class. Measured in the bundle, this selector matches 13
1648
+ cells, ALL state rows, ZERO data rows:
1649
+ Comentarios (loading and empty), Artigos e colecoes (loading, empty and
1650
+ no-search-results), Lixeira, Tipos de conteudo, byline schema
1651
+ (loading, error with a Repetir button, and empty at py-12) and Usuarios.
1652
+ LEFT OUT, on purpose, are the other td[colspan] that are NOT a state:
1653
+ "Carregar mais pastas" and Media's folder error row (py-3), which are
1654
+ action and alert, and the two loading rows that coexist WITH data rows
1655
+ (py-6 in Media and py-4 in Usuarios), where a 96px band in the middle of the
1656
+ list would be worse than the misalignment. Accepted side effect: on those two
1657
+ screens there's still a small jump between "Carregando" and "Nenhum".
1658
+ The table the client writes in the article body is also left out with no
1659
+ guard needed: it's editor HTML, it carries no utility class.
1660
+
1661
+ `height` on a table cell is a MINIMUM HEIGHT, so a long state message
1662
+ (the "nenhum usuario" one has a button alongside) grows normally, doesn't clip.
1663
+ WATCH: if an upgrade swaps `py-8` for another value, the cell just goes back to
1664
+ being squeezed, nothing breaks, and the fix is adding the class to the :is().
1665
+ ========================================================================== */
1666
+ html[data-mode] main table tbody td[colspan]:is(.py-8, .py-12) {
1667
+ height: 96px; /* minimum, about 3 lines */
1668
+ padding: 24px 12px !important;
1669
+ text-align: center; /* EmDash already sets text-center; this is belt and suspenders */
1670
+ }
1671
+
1672
+ /* the state row is excluded from the zebra stripe and hover: in WP it's a clean band, not
1673
+ a clickable row. Specificity (0,3,6) against the (0,2,5) of the zebra and
1674
+ hover (lines 816 and 820), so it wins even with both at !important.
1675
+ A browser without :has() only loses THIS rule: the row goes back to getting the zebra stripe and the
1676
+ breathing room above still applies. */
1677
+ html[data-mode] main table tbody tr:has(> td[colspan]:is(.py-8, .py-12)),
1678
+ html[data-mode] main table tbody tr:has(> td[colspan]:is(.py-8, .py-12)):hover {
1679
+ background: var(--wp-surface) !important;
1680
+ }
1681
+
1682
+
1683
+ /* ==========================================================================
1684
+ [12] LISTING EMDASH BUILDS WITH <div> INSTEAD OF <table>
1685
+ (Taxonomias/Categorias and Redirecionamentos)
1686
+
1687
+ WHY IT WAS MISSING: this container arrives as `border rounded-lg` WITHOUT `bg-kumo-base`
1688
+ and WITHOUT `overflow-x-auto`, so it doesn't match any of block [7]'s three
1689
+ card selectors. It was left with a raw 1px outline, no background and no shadow,
1690
+ alone among the listings (comments, for example, is
1691
+ `rounded-md border bg-kumo-base overflow-x-auto` and is caught by the last two).
1692
+
1693
+ ANCHOR = the header band WITH ITS BACKGROUND TINT: a direct child that has
1694
+ `border-b` AND `bg-kumo-tint/50`. `border-b` alone is NOT enough: measured in the bundle,
1695
+ `div.border.rounded-lg:has(> .border-b)` matches 17 times, and 14 are a card,
1696
+ modal or panel (the plugin consent dialog, the editor's image detail
1697
+ panel, which has TWO children like this, the type editor's fields panel,
1698
+ the device-authorization badge, and ten WordPress importer cards,
1699
+ including the two red ERROR ones, which would end up white).
1700
+ Worse: in Redirecionamentos every list ROW is a direct child with
1701
+ `border-b`, so the band's rule would repaint the whole list as a header.
1702
+ With `bg-kumo-tint/50` added, the pair exists on 6 divs in the bundle and only 3 are
1703
+ a DIRECT child of a `div.border.rounded-lg`: the taxonomy and the two redirect
1704
+ lists. Zero false positives.
1705
+
1706
+ `rounded-lg` is left as is on purpose: the sibling listing (comments) is
1707
+ `rounded-md` and isn't squared by the skin either. Squaring it only here
1708
+ would recreate the very divergence being fixed.
1709
+ ========================================================================== */
1710
+ html[data-mode] main div.border.rounded-lg:has(> .border-b.bg-kumo-tint\/50) {
1711
+ background: var(--wp-surface) !important;
1712
+ border-color: transparent !important; /* the outline comes from the shadow's first layer, like in [7] */
1713
+ box-shadow:
1714
+ 0 0 0 1px var(--wp-line),
1715
+ 0 1px 1px rgba(29, 35, 39, .03),
1716
+ 0 32px 25px -35px rgba(29, 35, 39, .15),
1717
+ 0 12px 28px -18px rgba(29, 35, 39, .22) !important;
1718
+ }
1719
+
1720
+ html[data-mode] main div.border.rounded-lg:has(> .border-b.bg-kumo-tint\/50):hover {
1721
+ box-shadow:
1722
+ 0 0 0 1px var(--wp-line),
1723
+ 0 1px 2px rgba(29, 35, 39, .04),
1724
+ 0 36px 28px -34px rgba(29, 35, 39, .18),
1725
+ 0 20px 40px -20px rgba(29, 35, 39, .28) !important;
1726
+ }
1727
+
1728
+ /* the header band gets the same treatment as real tables' <thead>
1729
+ (line 804), MINUS the padding: in a <table> the th and td both get
1730
+ 8px 10px and stay aligned; here only the band would be touched and the
1731
+ labels would slide 6px to the left relative to the cells, which are
1732
+ px-4. The `py-2 px-4` that already comes from EmDash gives the same 8px vertically. */
1733
+ html[data-mode] main div.border.rounded-lg:has(> .border-b.bg-kumo-tint\/50) > .border-b.bg-kumo-tint\/50 {
1734
+ border-bottom: 1px solid var(--wp-line) !important;
1735
+ background: var(--wp-surface) !important;
1736
+ font-weight: 400 !important;
1737
+ }
1738
+
1739
+
1740
+ /* ==========================================================================
1741
+ [13] HEADER BUTTONS WITH THE SAME VISUAL WEIGHT
1742
+
1743
+ Two things measured, one about CASCADE and one about REACH.
1744
+
1745
+ (a) CASCADE. The two existing .page-title-action families (lines
1746
+ 610 and 629) write `color: ... !important` in a sheet WITHOUT @layer, and
1747
+ that LOSES to kumo's !important, which lives inside
1748
+ `@layer utilities` (`.\!text-kumo-default{color:...!important}`, and
1749
+ `!text-kumo-default` is in the `secondary` variant, which is <Button>'s
1750
+ default). Cascade rule with layers: important INSIDE a layer
1751
+ beats important OUTSIDE a layer. Practical effect today, on every screen:
1752
+ the button already uses WordPress's background and border, but the text stays #1d2327
1753
+ instead of the accent blue. The path that works is going through the TOKEN, which
1754
+ kumo's own !important resolves. One line per family, additive.
1755
+
1756
+ (b) REACH. Neither family reaches an action living inside a GROUPING WRAPPER.
1757
+ In Taxonomias both buttons live inside a `div.flex.gap-2` because
1758
+ of the language selector, and in Media inside a `div.shrink-0`:
1759
+ in both, the h1 is in the block with a subtitle (kills the `h1 + div` family) and
1760
+ the buttons are GRANDCHILDREN (kills the `:has(> div > h1) > a` family). It's not
1761
+ a design decision, it's a gap between the two families: the family WITHOUT
1762
+ a subtitle already paints an action inside a grouping wrapper (Bylines screen,
1763
+ `h1 + div > a`). The block below only completes the symmetry, no new shape.
1764
+ `:not(:has(h1))` excludes the title block, the only other div-child
1765
+ of the row (checked in Tipos de conteudo, Redirecionamentos, Secoes,
1766
+ Widgets, Menus, byline schema, Conteudo and Comentarios: in none
1767
+ of them does the other div have a button).
1768
+ ACCEPTED CONSEQUENCE: EmDash's hierarchy on that screen (outline for
1769
+ "Nova taxonomia", secondary for "Adicionar termo") disappears, as it already
1770
+ disappears on the other nine screens where the skin dresses up the header action.
1771
+ WATCH: if EmDash puts a control that is NOT an action in that wrapper,
1772
+ it will get dressed as .page-title-action. Visual failure, doesn't break.
1773
+ ========================================================================== */
1774
+
1775
+ /* (a) the two families that already exist: the text color goes through the token */
1776
+ html[data-mode] main h1 + div > a,
1777
+ html[data-mode] main h1 + div > button {
1778
+ --text-color-kumo-default: var(--wp-accent);
1779
+ }
1780
+
1781
+ html[data-mode] main > div > div:has(> div > h1) > a,
1782
+ html[data-mode] main > div > div:has(> div > h1) > button {
1783
+ --text-color-kumo-default: var(--wp-accent);
1784
+ }
1785
+
1786
+ html[data-mode] main h1 + div > a:hover,
1787
+ html[data-mode] main h1 + div > button:hover {
1788
+ --text-color-kumo-default: var(--wp-accent-deep);
1789
+ }
1790
+
1791
+ html[data-mode] main > div > div:has(> div > h1) > a:hover,
1792
+ html[data-mode] main > div > div:has(> div > h1) > button:hover {
1793
+ --text-color-kumo-default: var(--wp-accent-deep);
1794
+ }
1795
+
1796
+ /* (b) the missing family: action inside the grouping wrapper, on the row WITH a subtitle.
1797
+ Same declarations as the other two, on purpose: in the header WordPress
1798
+ dresses every action as .page-title-action.
1799
+ Contrast checked: #2271b1 on #f6f7f7 = 4.80:1, passes AA for normal
1800
+ 13px text, and it's the same pair the panel already uses on the other nine screens.
1801
+ Touch target: 30px tall, above the gate's 24px. */
1802
+ html[data-mode] main > div > div:has(> div > h1) > div:not(:has(h1)) > a,
1803
+ html[data-mode] main > div > div:has(> div > h1) > div:not(:has(h1)) > button {
1804
+ --text-color-kumo-default: var(--wp-accent);
1805
+ height: 30px !important;
1806
+ min-height: 0 !important;
1807
+ padding: 0 10px !important;
1808
+ gap: 4px !important;
1809
+ border: 1px solid var(--wp-accent) !important;
1810
+ border-radius: 3px !important;
1811
+ background: var(--wp-button-bg) !important;
1812
+ color: var(--wp-accent) !important;
1813
+ font-size: 13px !important;
1814
+ font-weight: 400 !important;
1815
+ line-height: 28px !important;
1816
+ box-shadow: 0 1px 1px rgba(29, 35, 39, .04) !important;
1817
+ text-decoration: none !important;
1818
+ -webkit-transition: background-color var(--wp-fast) ease, border-color var(--wp-fast) ease, color var(--wp-fast) ease, box-shadow var(--wp-fast) ease, -webkit-transform var(--wp-fast) ease;
1819
+ transition: background-color var(--wp-fast) ease, border-color var(--wp-fast) ease, color var(--wp-fast) ease, box-shadow var(--wp-fast) ease, transform var(--wp-fast) ease;
1820
+ }
1821
+
1822
+ html[data-mode] main > div > div:has(> div > h1) > div:not(:has(h1)) > a:hover,
1823
+ html[data-mode] main > div > div:has(> div > h1) > div:not(:has(h1)) > button:hover {
1824
+ --text-color-kumo-default: var(--wp-accent-deep);
1825
+ background: var(--wp-surface) !important;
1826
+ border-color: var(--wp-accent-deep) !important;
1827
+ color: var(--wp-accent-deep) !important;
1828
+ box-shadow: 0 2px 6px -2px rgba(29, 35, 39, .18) !important;
1829
+ -webkit-transform: translateY(-1px);
1830
+ transform: translateY(-1px);
1831
+ }
1832
+
1833
+ html[data-mode] main > div > div:has(> div > h1) > div:not(:has(h1)) > a:active,
1834
+ html[data-mode] main > div > div:has(> div > h1) > div:not(:has(h1)) > button:active {
1835
+ -webkit-transform: translateY(0);
1836
+ transform: translateY(0);
1837
+ box-shadow: none !important;
1838
+ }
1839
+
1840
+ /* the box-shadow above is !important and kumo's ring IS box-shadow: without this
1841
+ rule the focus ring dies. Same FIX 7 already applied to the other two
1842
+ families (lines 694 and 700). */
1843
+ html[data-mode] main > div > div:has(> div > h1) > div:not(:has(h1)) > a:focus-visible,
1844
+ html[data-mode] main > div > div:has(> div > h1) > div:not(:has(h1)) > button:focus-visible {
1845
+ outline: 2px solid var(--wp-accent-deep) !important;
1846
+ outline-offset: 2px !important;
1847
+ }
1848
+
1849
+ html[data-mode] main > div > div:has(> div > h1) > div:not(:has(h1)) > a svg,
1850
+ html[data-mode] main > div > div:has(> div > h1) > div:not(:has(h1)) > button svg {
1851
+ width: 14px !important;
1852
+ height: 14px !important;
1853
+ }
1854
+
1855
+ /* ==========================================================================
1856
+ [4B] "RECOLHER MENU" BUTTON: arrow in a circle, flipping with the state
1857
+ kumo already puts an icon there: Sidebar.PanelIcon, a panel rectangle
1858
+ with an inner line that slides (viewBox 24, stroke 1.5). It reads as
1859
+ "panel", not as "collapse", and it gets lost among the menu icons.
1860
+ Swapped for Phosphor 2.1.10's ArrowCircle, which is already installed: same
1861
+ family as the other items, same 256 grid, same weight trick
1862
+ (stroke 8 = the family's bold) that section [4] applies to the rest of the menu.
1863
+ State hook = the button's OWN aria-expanded, not the wrapper's data-state:
1864
+ kumo has a third state ("peeking") that breaks a rule pinned to
1865
+ [data-state="collapsed"].
1866
+ mask-image + currentColor (not background-image): the icon inherits the theme's
1867
+ color and gets the blue hover for free, with a single SVG per direction.
1868
+ ========================================================================== */
1869
+
1870
+ /* kumo's panel icon is removed, only in the footer (the header trigger stays as is) */
1871
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] [data-sidebar="trigger"] > svg {
1872
+ display: none !important;
1873
+ }
1874
+
1875
+ /* expanded: arrow points LEFT (about to shrink) */
1876
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] [data-sidebar="trigger"]::before {
1877
+ content: "";
1878
+ flex: 0 0 var(--wp-icon);
1879
+ width: var(--wp-icon);
1880
+ height: var(--wp-icon);
1881
+ background-color: currentColor;
1882
+ -webkit-mask-repeat: no-repeat;
1883
+ mask-repeat: no-repeat;
1884
+ -webkit-mask-position: center;
1885
+ mask-position: center;
1886
+ -webkit-mask-size: contain;
1887
+ mask-size: contain;
1888
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20256%20256'%20fill='%23000'%20stroke='%23000'%20stroke-width='8'%20stroke-linejoin='round'%20stroke-linecap='round'%20paint-order='stroke'%3E%3Cpath%20d='M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48-88a8,8,0,0,1-8,8H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168A8,8,0,0,1,176,128Z'/%3E%3C/svg%3E");
1889
+ mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20256%20256'%20fill='%23000'%20stroke='%23000'%20stroke-width='8'%20stroke-linejoin='round'%20stroke-linecap='round'%20paint-order='stroke'%3E%3Cpath%20d='M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48-88a8,8,0,0,1-8,8H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168A8,8,0,0,1,176,128Z'/%3E%3C/svg%3E");
1890
+ }
1891
+
1892
+ /* collapsed: arrow points RIGHT (about to expand) */
1893
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] [data-sidebar="trigger"][aria-expanded="false"]::before {
1894
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20256%20256'%20fill='%23000'%20stroke='%23000'%20stroke-width='8'%20stroke-linejoin='round'%20stroke-linecap='round'%20paint-order='stroke'%3E%3Cpath%20d='M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm45.66-93.66a8,8,0,0,1,0,11.32l-32,32a8,8,0,0,1-11.32-11.32L148.69,136H88a8,8,0,0,1,0-16h60.69l-18.35-18.34a8,8,0,0,1,11.32-11.32Z'/%3E%3C/svg%3E");
1895
+ mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20256%20256'%20fill='%23000'%20stroke='%23000'%20stroke-width='8'%20stroke-linejoin='round'%20stroke-linecap='round'%20paint-order='stroke'%3E%3Cpath%20d='M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm45.66-93.66a8,8,0,0,1,0,11.32l-32,32a8,8,0,0,1-11.32-11.32L148.69,136H88a8,8,0,0,1,0-16h60.69l-18.35-18.34a8,8,0,0,1,11.32-11.32Z'/%3E%3C/svg%3E");
1896
+ }
1897
+
1898
+ /* ==========================================================================
1899
+ ROUND 3 OF ADJUSTMENTS (2026-09-07)
1900
+ Paste at the END of public/painel-wp.css, after block [4B].
1901
+
1902
+ Everything here was measured in the bundle, the source maps and the running panel
1903
+ (@emdash-cms/admin 0.36, @cloudflare/kumo 2.6.0, embedded Base UI), and each
1904
+ item went through a skeptic who refuted the proposal before it became a rule.
1905
+
1906
+ THREE LAWS THAT APPLY TO THE WHOLE BLOCK:
1907
+ 1. NO @layer, on purpose. EmDash's [data-theme=classic]:not([data-mode=dark])
1908
+ block is UNLAYERED (measured with postcss: zero ancestors, 28 declarations,
1909
+ offset 199597 in styles.css), and in the cascade a rule with no layer beats ANY
1910
+ layer. Emitting these rules inside @layer base would make all of them silently
1911
+ lose to classic: no error, no console, just the wrong color.
1912
+ 2. html[data-mode] prefix on everything. The admin's ThemeProvider writes
1913
+ root.setAttribute("data-theme","classic") and ("data-mode", resolvedTheme) in the
1914
+ SAME useEffect (dist/index.js:95-97), so [data-mode] always exists
1915
+ whenever classic applies. html:root[data-mode] = (0,2,1) beats classic
1916
+ (0,2,0). Where the target is a TOKEN, the selector is html:root[data-mode] and not
1917
+ html[data-mode]: (0,1,1) would lose to block [0] on line 20 and the rule
1918
+ would become dead letter.
1919
+ 3. Whoever positions a popup is Floating UI, via JS, in the Positioner. No rule
1920
+ below writes transform, position, inset or overflow on a popup. The only
1921
+ displacement used is `translate`, a property independent of `transform`.
1922
+
1923
+ WHAT DIDN'T MAKE IT INTO THIS ROUND, and why (so nobody re-proposes it):
1924
+ . Route B for the collapsed label (the <span> itself becoming a tab via position:fixed):
1925
+ measured, the tab doesn't follow the menu's scroll, a 1-to-1 error against the
1926
+ displacement, and the menu really does scroll here (scrollHeight 1030 against
1927
+ clientHeight 773). Route A stays, where Floating UI keeps owning the
1928
+ position. It would only be worth revisiting if it ever relies on anchor positioning.
1929
+ . `mask-image: none` on the menu's scroll viewport: the only measured reason to
1930
+ kill it was to free up route B. Without route B, the fade at the ends becomes the
1931
+ ONLY hint that the menu scrolls, because the track is already hidden on lines
1932
+ 349-357. Left as is.
1933
+ . Removing the `translateY(-1px)` from the panel card's hover (lines 895-898) and
1934
+ from the header buttons (656, 666, 1676): it's the owner's preference about
1935
+ motion in the CONTENT, and this round he asked for MORE interpolation in
1936
+ the content. It needs explicit sign-off, and if it goes in it has to go into all four
1937
+ spots, otherwise the card stops rising and the button keeps rising.
1938
+ . JS-based menu resize handle: kumo's `isResizing` is unreachable from outside,
1939
+ so the resize would have a 250ms transition fighting !important on every
1940
+ frame. The wp-admin being imitated has no resizable menu: it has a fixed width and
1941
+ a fold button, which the panel already has.
1942
+ ========================================================================== */
1943
+
1944
+
1945
+ /* ==========================================================================
1946
+ [R3] WHAT GOES OUT OF THE CURRENT FILE
1947
+
1948
+ The new rules below win by ORDER (same selector, same specificity,
1949
+ declared later), so the panel is already correct even if the removals
1950
+ aren't done. TWO exceptions are MANDATORY, marked with [!]: they erase
1951
+ `padding: 0 !important` in rules of HIGHER specificity, and without the removal
1952
+ the collapsed menu's icon touches the border instead of stopping at 8px.
1953
+
1954
+ R3.1 Content scrollbar. Delete lines 1032 to 1046 entirely:
1955
+
1956
+ html[data-mode] main {
1957
+ scrollbar-width: thin;
1958
+ scrollbar-color: #a7aaad var(--wp-canvas);
1959
+ }
1960
+ html[data-mode] main::-webkit-scrollbar { width: 12px; }
1961
+ html[data-mode] main::-webkit-scrollbar-thumb {
1962
+ background: #a7aaad;
1963
+ border: 3px solid var(--wp-canvas);
1964
+ border-radius: 8px;
1965
+ }
1966
+
1967
+ Replaced by the live rules right below, in this same section.
1968
+
1969
+ R3.2 [!] Collapsed menu, item centering. On lines 423 to 432, delete
1970
+ the TWO marked declarations:
1971
+
1972
+ ... [data-sidebar="menu-button"] {
1973
+ justify-content: center !important; <- becomes flex-start in [17c]
1974
+ padding: 0 !important; <- DELETE, no replacement
1975
+ }
1976
+ ... [data-sidebar="menu-button"] > div {
1977
+ flex: 0 0 auto !important;
1978
+ justify-content: center !important; <- becomes flex-start in [17c]
1979
+ }
1980
+
1981
+ The collapsed item's padding has to keep coming from line 372, which
1982
+ applies in BOTH states: that's exactly what stops the icon from moving on
1983
+ collapse. Redeclaring 8px here, in a more specific rule, is what would make
1984
+ the two states silently diverge on the next upgrade.
1985
+
1986
+ R3.3 [!] Collapsed "Recolher menu" button. On lines 524 to 527, delete only the
1987
+ `padding: 0 !important;` line (the justify-content becomes flex-start in
1988
+ [17c]). Its breathing room now comes from line 500, which also applies to
1989
+ both states.
1990
+
1991
+ R3.4 Icon displacement on hover. Two edits in block [4]:
1992
+ . lines 555 and 556: remove the `transform var(--wp-normal)
1993
+ var(--wp-ease-out)` half from the list, leaving only `color var(--wp-fast) ease`.
1994
+ . lines 569 and 570: delete both transform lines:
1995
+ -webkit-transform: translateX(1px);
1996
+ transform: translateX(1px);
1997
+ The movement was OURS, not kumo's: on its side there's only
1998
+ `group-hover/menu-button:opacity-100` on the chevron, pure opacity.
1999
+
2000
+ R3.5 Breathing room on the item's right. Line 372, swap
2001
+ padding: 0 8px !important;
2002
+ for
2003
+ padding: 0 16px 0 8px !important;
2004
+ (rule [17d] does the same on top, for anyone who'd rather just append).
2005
+ THE MENU WIDTH STAYS AT 230px. The owner asked for a NARROWER bar this
2006
+ round, so the +10px compensation on line 45 is left out: the label
2007
+ drops from 186px to 178px, about one character. If truncation ever
2008
+ becomes a problem, the fix is ONE edit on line 45 (230px to 240px), never
2009
+ a second declaration of the token further down here, which would silently
2010
+ win over whoever edited section [0].
2011
+
2012
+ R3.6 User menu shadow. Line 1183, inside block [8]:
2013
+ box-shadow: 0 3px 5px rgba(0, 0, 0, .2) !important;
2014
+ replaced by [16b]'s four layers.
2015
+
2016
+ R3.7 User menu displacement and corner. Lines 1200 to 1206 (the two
2017
+ current [8b] rules): replaced by [16a]'s pair, which adds the X axis and
2018
+ the corners. It has to be the WHOLE pair: `translate` is a single property,
2019
+ so writing the X axis in one rule and the Y in another would erase the first.
2020
+ ========================================================================== */
2021
+
2022
+ /* R3.1, live part: the content loses the track, not the scrolling.
2023
+ Wheel, trackpad, keyboard and touch drag keep working: only the
2024
+ artwork disappears. ACCEPTED COST, and it's real: the permanent hint that there's more
2025
+ content below disappears, and so does the DRAG target, which is the only scrolling path
2026
+ for someone using a mouse with no wheel. No WCAG criterion requires a visible bar, and the
2027
+ <main> already wasn't keyboard-focusable in Safari before this rule (that's
2028
+ pre-existing, doesn't originate here).
2029
+ MEASURED SIDE EFFECT: `scrollbar-color` IS INHERITED, so line 1035
2030
+ used to tint, by inheritance, every scrollable area INSIDE main (overflow-x-auto table,
2031
+ code block, textarea). Without it, those go back to the browser's default gray,
2032
+ which is similar to the previous #a7aaad.
2033
+ The content widens by 12px, because the 12px ::-webkit-scrollbar turned
2034
+ macOS's overlay bar into a classic bar, which took up space. */
2035
+ /* round 4: GLOBAL RULE. Thin (11px) and discreet across every scrollable area of the panel, except the
2036
+ side bar (hidden on line 349). No ::-webkit-scrollbar anywhere: it's the one
2037
+ that kills macOS's overlay bar. "Only shows up while scrolling" is NOT CSS: it's the
2038
+ system preference (Appearance > Show scroll bars > When scrolling); with it set to
2039
+ "Always", any bar takes up space. Color #787c82 = 3.68:1 on the canvas, passes AA. */
2040
+ html[data-mode] {
2041
+ scrollbar-color: #787c82 transparent;
2042
+ }
2043
+ html[data-mode] main,
2044
+ html[data-mode] main *:not(:where(.no-scrollbar, .kumo-tabs-list)),
2045
+ html[data-mode] :is(aside, nav)[data-side="right"] div.overflow-y-auto {
2046
+ scrollbar-width: thin;
2047
+ }
2048
+
2049
+ /* kumo still draws its OWN bar inside the menu, which isn't the native one: it's
2050
+ a Base UI ScrollArea.Scrollbar (sidebar.tsx:804-813), `w-1.5 opacity-0`
2051
+ that rises to 1 on data-[scrolling] and data-[hovering]. Lines 349 to 357
2052
+ hide only the native one, so it used to reappear when hovering over the menu.
2053
+ ANCHOR: Base UI converts every unmapped state into data-<key>, and
2054
+ `orientation` isn't in scrollAreaStateAttributesMapping, so the Scrollbar
2055
+ comes out with data-orientation="vertical" and the Viewport doesn't (its state doesn't have
2056
+ that key). No :nth-child.
2057
+ Hiding it does NOT unmount or lock the component: Base UI's `shouldRender` reads
2058
+ getHiddenState(viewportEl), from the VIEWPORT, not the bar, and its
2059
+ getBoundingClientRect reads only run on onPointerDown/onPointerMove, which don't
2060
+ fire on an element with display none. The menu's fade mask stays
2061
+ alive, because it's fed by variables written on the Viewport. */
2062
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="content"] > [data-orientation] {
2063
+ display: none !important;
2064
+ }
2065
+
2066
+ /* --------------------------------------------------------------------------
2067
+ R3.8 THE THEME TOGGLE COMES OUT OF THE BAR
2068
+
2069
+ The panel has no real dark mode and never will: wp-admin doesn't have one,
2070
+ EmDash's classic theme doesn't either (measured, the string "data-theme=classic"
2071
+ appears ONCE in the entire styles.css, inside
2072
+ [data-theme=classic]:not([data-mode=dark]), with 28 tokens for light and ZERO for
2073
+ dark), and line 26 of this sheet already forces `color-scheme: light`, which
2074
+ neutralizes kumo's 130 light-dark() calls at once. Keeping the button means keeping a
2075
+ control that promises one thing and delivers another, whose only real effect today
2076
+ is silently degrading 14 tokens (see R3.9).
2077
+
2078
+ DOUBLE ANCHOR, on purpose. The positional one alone (a[href][target] + button) would be
2079
+ dangerous: if an EmDash bump removes or moves the ThemeToggle, it would start matching
2080
+ the NEXT BUTTON, which is the user menu (Sair, Configuracoes). A client
2081
+ with no logout button is far worse than a client with a useless theme button. The
2082
+ data-base-ui-tooltip-trigger is stamped by Base UI on the Tooltip's trigger, and
2083
+ ThemeToggle is the only header control with a `title` prop (kumo's Button
2084
+ only wraps in a Tooltip when there's a title): the user menu and the Sidebar.Trigger
2085
+ have no title, so they never match. With both requirements together, an
2086
+ upgrade's failure becomes "the sun reappears", visible right away, and never "the wrong
2087
+ button disappeared".
2088
+ UPGRADE WATCH: if the button comes back, check whether EmDash removed the `title`
2089
+ (lost the tooltip) or reordered the header.
2090
+ The rule on line 231 (32px width on that same button) stays inert, and doesn't
2091
+ get in the way: if EmDash puts another button with a tooltip in the bar, it still
2092
+ gives it the right width.
2093
+ -------------------------------------------------------------------------- */
2094
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > div > header
2095
+ a[href][target] + button[data-base-ui-tooltip-trigger] {
2096
+ display: none !important;
2097
+ }
2098
+
2099
+ /* --------------------------------------------------------------------------
2100
+ R3.9 THE 14 TOKENS THAT EVAPORATE WHEN data-mode=dark
2101
+
2102
+ EmDash's block is [data-theme=classic]:not([data-mode=dark]). When
2103
+ data-mode=dark enters the <html>, the whole block stops matching and its 28 tokens
2104
+ fall back to kumo's `:root,:host` light-dark() (it falls there, not to
2105
+ :root[data-mode=dark], because :root,:host is UNLAYERED and the dark block lives
2106
+ inside @layer base: a layer loses to what has no layer).
2107
+
2108
+ And data-mode=dark IS REACHABLE TODAY, with nobody clicking anything: the
2109
+ ThemeProvider uses defaultTheme "system" and reads prefers-color-scheme, and
2110
+ admin.astro has a BLOCKING is:inline script in the head that stamps data-mode
2111
+ before this sheet's <link>. It only takes the client's system being in dark mode.
2112
+
2113
+ Of those 28, the skin already redeclares 18. The 14 below were inherited, and in
2114
+ dark mode they'd change value on their own, leaving WordPress's palette for Tailwind's
2115
+ light ramp (line 26's `color-scheme: light` forces light-dark's light branch,
2116
+ so it doesn't go dark: it goes off-brand).
2117
+ THE MOST IMPORTANT ONE IS HOVER: in wp-admin the fill DARKENS from #dcdcde to
2118
+ #c3c4c7; without this, in dark mode it LIGHTENS (oklch(0.922) to oklch(0.965)) and the
2119
+ button gets lighter on hover, the opposite of the rest of the
2120
+ panel.
2121
+
2122
+ Values copied literally from the classic block, checked one by one: in light mode
2123
+ nothing changes (it's additive), in dark mode the panel now looks the same as light.
2124
+ Specificity (0,2,1) beats classic (0,2,0) and loses to the skin's scoped
2125
+ blocks (0,3,3) for the menu, the bar and the popover, which is exactly what's
2126
+ wanted. None of these 14 has an !important declaration inside @layer utilities
2127
+ (zero occurrences in the bundle), so the unlayered declaration truly wins.
2128
+ Measured accessibility gain: with the fallback, info text #2271b1 over Tailwind's
2129
+ blue-100 gave 4.24:1 and failed AA; fixing info-tint to
2130
+ #e7f4fe raises that same text to 4.62:1 and it passes, without touching text color.
2131
+ REGRESSION TO WATCH ON EVERY EMDASH BUMP: if classic changes, these 14 don't
2132
+ change with it (that's the point), but the other 18 remain a valid
2133
+ comparison against the new styles.css.
2134
+ -------------------------------------------------------------------------- */
2135
+ html:root[data-mode] {
2136
+ --color-kumo-fill: #dcdcde;
2137
+ --color-kumo-fill-hover: #c3c4c7; /* hover DARKENS, like in wp-admin */
2138
+ --color-kumo-contrast: #1d2327;
2139
+ --color-kumo-interact: #c3c4c7;
2140
+ --text-color-kumo-strong: #50575e;
2141
+ --text-color-kumo-inactive: #a7aaad;
2142
+ --color-kumo-info: #72aee6;
2143
+ --color-kumo-info-tint: #e7f4fe;
2144
+ --color-kumo-success: #00a32a;
2145
+ --color-kumo-success-tint: #edfaef;
2146
+ --color-kumo-warning: #dba617;
2147
+ --color-kumo-warning-tint: #fcf9e8;
2148
+ --color-kumo-danger: #d63638;
2149
+ --color-kumo-danger-tint: #fcf0f1;
2150
+ }
2151
+ /* note: these 14 are status BACKGROUND. The 4 status TEXT tokens keep
2152
+ our own AA values from lines 30 to 33 (#007017, #b32d2e, #996800, #2271b1),
2153
+ deliberately different from classic. Do not overwrite.
2154
+ Measured contrast of the resulting pairs: 5.86:1 on success, 5.67:1 on danger,
2155
+ 4.58:1 on warning, 4.62:1 on info. All pass AA. */
2156
+
2157
+
2158
+ /* ==========================================================================
2159
+ [14] EMPTY-STATE ACTION ON THE BLOCK AXIS
2160
+
2161
+ CAUSE MEASURED IN THE BUNDLE, not from a screenshot. kumo's <Button> is born with
2162
+ `group flex w-max shrink-0 items-center` (buttonVariants, button.tsx:134), which
2163
+ styles.css compiles to display:flex plus width:max-content: a BLOCK BOX
2164
+ with a shrunk width. The empty-state card is `border rounded-lg p-12
2165
+ text-center` (MenuList.tsx:187), and text-align centers text and icon in
2166
+ line, but does NOT shift a block box: it stays flush against the start border.
2167
+ MEASURED IN PRODUCTION (/_emdash/admin/menus): the "Criar menu" button sits 49px from
2168
+ the start border and 1290px from the end. With this rule: 669.5px on both sides,
2169
+ and the horizontal-scroll gate stays clean.
2170
+ justify-content on the parent doesn't help, because the parent isn't flex: what centers
2171
+ a shrunk block box is the AUTO MARGIN.
2172
+
2173
+ ANCHOR: `data-kumo-component` is an attribute kumo writes by hand on the
2174
+ element itself (button.tsx:322 for Button, 406 for LinkButton), so it survives
2175
+ a utility swap in an upgrade. The third line covers EmDash's RouterLinkButton,
2176
+ which builds the same look by calling buttonVariants but does NOT emit the
2177
+ attribute. `w-max` only exists in buttonVariants (the only occurrence in kumo/src, and
2178
+ the admin only uses `min-w-max`), so `a.flex.w-max` doesn't reach a regular link.
2179
+ WATCH: `.text-center` is a utility, not a contract. If EmDash swaps
2180
+ these blocks for its own EmptyState component, the rule stops matching and the
2181
+ button goes back to the left (visual failure, never breaks).
2182
+
2183
+ MEASURED INVENTORY (every Button/LinkButton/RouterLinkButton in the admin, with the parent
2184
+ resolved by indentation). 16 cases, none gets worse:
2185
+ CHANGE (12, all empty-state, error, or "load more" actions):
2186
+ MenuList:187 (Menus), Sections:255 (Secoes), ContentTypeEditor:625,
2187
+ RepeaterField:164 and PortableTextEditor:2226 (repeater),
2188
+ MarketplaceBrowse:131 and ThemeMarketplaceBrowse:110 (marketplace errors),
2189
+ DeviceAuthorizePage:196, ContentPickerModal:216, MediaLibrary:1244 (<td>),
2190
+ byline-schema:267 (the same "Repetir" block [11] cites, <td>) and
2191
+ router:2524 (the app's error screen).
2192
+ MATCH AND CHANGE NOTHING (4, verified in the browser): LoginPage:117, which arrives
2193
+ with `w-full` (twMerge erases the w-max, width 100%, margin resolves to
2194
+ zero) and MediaPickerModal:687, BylineCreditsEditor:468 and
2195
+ MediaUploadDialog:421, which are `flex flex-col items-center` and were already
2196
+ centered.
2197
+ DON'T MATCH, checked one by one: an action inside a grouping wrapper (not a direct child), the
2198
+ <span> with buttonVariants inside a <label> on the importer's two dropzones,
2199
+ the `inline-block` link on the plugin detail error, animate-pulse skeleton
2200
+ (none has text-center), kumo's dropdown menu, popover, toast and pagination (the
2201
+ 7 text-center in kumo/src have no button inside), and the sidebar, the header
2202
+ and the shell (zero text-center).
2203
+ The only `.text-center` that is a ROW flex across the whole admin
2204
+ (BylineCreditsEditor:540) has no button inside. WATCH: if an upgrade puts
2205
+ a button there, the auto margin pushes the siblings to the edges. That's the only
2206
+ way this rule could misalign something.
2207
+
2208
+ NOT SCOPED TO `main` ON PURPOSE: this way it also applies in the content-picker
2209
+ modal, the device authorization screen and the app's error screen, which live
2210
+ outside <main>. `!important` isn't needed: an unlayered declaration beats a
2211
+ utility inside @layer utilities (`.mx-auto` lives there) and none of these
2212
+ buttons carries mx-*. `margin-inline` is logical, so it follows RTL.
2213
+ ========================================================================== */
2214
+ html[data-mode] .text-center > :is([data-kumo-component="Button"],
2215
+ [data-kumo-component="LinkButton"],
2216
+ a.flex.w-max) {
2217
+ margin-inline: auto;
2218
+ }
2219
+
2220
+
2221
+ /* ==========================================================================
2222
+ [15a] WHAT'S BORN SQUARE STAYS SQUARE
2223
+
2224
+ MEASURED CAUSE: the badge on the Tipos de conteudo row is
2225
+ `flex h-8 w-8 items-center justify-center rounded-lg` (ContentTypeList.tsx:290)
2226
+ inside `div.flex.items-center.space-x-3`, WITHOUT shrink-0. A flex item without
2227
+ shrink-0 doesn't stop at its declared width: its automatic minimum size is the
2228
+ SMALLER of the specified width (32px) and the content's minimum, which here is the
2229
+ 16px svg. The box drops from 32 to 16 and the height stays at h-8's 32. Since the
2230
+ text sibling has neither min-w-0 nor truncate, the badge is always the one that yields, and only
2231
+ on the ROW with the longest text: hence two sizes in the same column.
2232
+
2233
+ MEASURED ON THE RUNNING PANEL (Chrome via CDP, route /content-types): at 320px and
2234
+ 390px six badges shrink to 16x32 (one of them 16.42x32) and at 1440px one shrinks to
2235
+ 22.13x32 while the others stay 32x32. With this rule all six go to 32x32.
2236
+ Across the eight measured routes (root, Tipos de conteudo, Usuarios, Midia, Ajustes,
2237
+ Conteudo, Comentarios, Menus), at 320, 390 and 1440, and with the user popover
2238
+ OPEN, no other element changed size, the document's and main's scrollWidth stayed
2239
+ the same and the popup's transform/position stayed intact.
2240
+
2241
+ CENSUS IN THE SHIPPED CODE: the admin bundle has 498 class strings with a square
2242
+ pair (h-N together with w-N, or size-N), 42 with shrink-0 or flex-none and 456 without;
2243
+ kumo adds 35, 7 of them with shrink-0.
2244
+
2245
+ WHY IT DOESN'T CATCH WHAT SHOULD STRETCH, in four safeguards:
2246
+ 1. `flex: 0 0 auto` only has an effect on a FLEX ITEM. On any other element the
2247
+ property is ignored: Media's thumbnail, which today is a block inside
2248
+ <td>, doesn't move a pixel, and the same goes for anything in position absolute.
2249
+ 2. `aspect-ratio` only kicks in when ONE of the two dimensions is automatic. Since the selector
2250
+ requires the PAIR, these elements declare both width AND height, so the line is
2251
+ a no-op today: it's the safety net for the day an upgrade removes one of the two.
2252
+ Checked: of the 100 squares that are flex items on the swept routes, the 94
2253
+ healthy ones didn't move a pixel.
2254
+ 3. An element meant to grow never declares a fixed width: in the admin and in kumo,
2255
+ ZERO occurrences combine a square pair with grow, flex-1, flex-auto, basis-*,
2256
+ w-full or h-full. The :not()s come for free, as an upgrade guarantee.
2257
+ 4. Scoped to main and to the Base UI portals. The side menu is left out on
2258
+ purpose (its icons already have their own geometry in section [4], `flex: 0 0
2259
+ var(--wp-icon)`, and two rules fighting over the same element is the start of the
2260
+ next pitfall). The top bar is also left out, because the Header
2261
+ lives OUTSIDE <main> (Shell.tsx:105-116): its avatar isn't reached, and
2262
+ today it doesn't need to be, since the button wrapping it is w-max.
2263
+
2264
+ CASCADE: styles.css doesn't put !important on any utility involved (the
2265
+ file's only !important is in @layer properties), so this unlayered sheet
2266
+ wins. The measurement confirms it: the badge changes from 16 to 32 in the live DOM.
2267
+
2268
+ UPGRADE WATCH: the size ladder is a Tailwind utility, not a
2269
+ contract. Swap `h-8 w-8` for a new size, and that screen's badge goes back to
2270
+ shrinking (visual failure, doesn't break) and the fix is adding the pair to the :is().
2271
+ The ladder below is TODAY's measurement, counted in the admin bundle PLUS kumo's.
2272
+ Several (size-9 and size-10 on the icon-button, size-8.5 on the menu item) are already born
2273
+ with shrink-0 and are therefore no-ops right now: they're in the ladder so it doesn't lie, and for
2274
+ the day an upgrade removes shrink-0 from them.
2275
+ ========================================================================== */
2276
+ html[data-mode] :is(main, [data-base-ui-portal]) :is(.flex, .inline-flex) > :is(
2277
+ .h-2.w-2, .h-3.w-3, .h-3\.5.w-3\.5, .h-4.w-4, .h-5.w-5, .h-6.w-6,
2278
+ .h-7.w-7, .h-8.w-8, .h-9.w-9, .h-10.w-10, .h-12.w-12, .h-16.w-16,
2279
+ .size-2, .size-3, .size-3\.5, .size-4, .size-4\.5, .size-5, .size-6,
2280
+ .size-6\.5, .size-7, .size-8\.5, .size-9, .size-10, .size-14
2281
+ ):not(.grow):not(.flex-1):not(.flex-auto) {
2282
+ flex: 0 0 auto;
2283
+ aspect-ratio: 1 / 1;
2284
+ }
2285
+
2286
+ /* [15b] AN ICON THAT COMES IN THROUGH THE `size` PROP, not through a class.
2287
+ kumo's Phosphor and Loader turn the prop into a width/height ATTRIBUTE on the
2288
+ <svg>, with no class at all, so [15a], which finds it by class, doesn't reach it.
2289
+
2290
+ CENSUS, counted in the 0.36 bundle itself: 285 svgs are DIRECT children of a flex row
2291
+ inside main/portal. 251 already carry h-N/w-N/size-N and fall under [15a]. 34 are left,
2292
+ and this rule exists for them:
2293
+ 28x kumo's Loader, on the "loading" rows of Ajustes, WordPress import,
2294
+ user list and media picker. It sets width/height in an INLINE
2295
+ style, which does NOT stop flex from squeezing it: an inline style doesn't
2296
+ touch flex-shrink.
2297
+ 2x WarningCircle on the Redirecionamentos loop warning.
2298
+ 1x Hero illustration for Media's empty state, 1x Plus, 1x UserCircleIcon,
2299
+ 1x FileKindIcon, all in a fixed-size box: there the rule is just a safety net.
2300
+ Real squeeze measured in only one spot: the 14px WarningCircle inside
2301
+ `div.w-20.flex.items-center.justify-end.gap-1`.
2302
+
2303
+ WHAT THIS RULE DOESN'T COVER, on purpose: in Redirecionamentos the
2304
+ `<ArrowNext size={14} />` lives in `div.w-8.text-center` and the empty-state
2305
+ icon lives in `div.py-12.text-center`. Neither is a flex child, so
2306
+ neither is a flex item and neither shrinks; what shrinks in that row is the
2307
+ `div.w-8` itself. Worth remembering that a shrunk svg doesn't distort either: with the
2308
+ default preserveAspectRatio (xMidYMid meet, which Phosphor doesn't override) the
2309
+ artwork just gets smaller and stays centered.
2310
+
2311
+ Covering every svg that's a direct flex child is safe: the bundle sweep found no
2312
+ svg with w-full, h-full, size-full, flex-1, grow or basis-* in that position. The
2313
+ kumo svgs this rule also catches stay inert since they already have fixed width and
2314
+ height: the Popover's and Tooltip's arrow (20x10), the Breadcrumbs separator and
2315
+ the Link's external icon. The arrow is positioned by Floating UI on the PARENT element,
2316
+ and here nothing writes transform, position, inset or overflow.
2317
+
2318
+ `flex-shrink` and not the `flex` shortcut: the shortcut also zeroes grow and rewrites
2319
+ basis, and since this rule is unlayered it would silently override a `grow` or
2320
+ `basis-*` that kumo might one day put on an svg.
2321
+ No companion aspect-ratio rule: Phosphor's <svg> always carries both width AND
2322
+ height (IconBase emits both), so aspect-ratio would never come into play, not today,
2323
+ not in the imagined upgrade. It would be dead code. */
2324
+ html[data-mode] :is(main, [data-base-ui-portal]) :is(.flex, .inline-flex) > svg {
2325
+ flex-shrink: 0;
2326
+ }
2327
+
2328
+ /* --------------------------------------------------------------------------
2329
+ [15c] OPTIONAL, AND SHIPPED DISABLED ON PURPOSE.
2330
+
2331
+ The fixed-width columns on the TWO Redirecionamentos lists (the rules
2332
+ list and the 404s panel) are flex items without shrink-0. MEASURED: `.flex-1{flex:1}`
2333
+ is grow 1, shrink 1, basis 0%, and the shrinkage is distributed by the factor
2334
+ scaled by the BASIS, so basis 0 has weight 0: the truncate columns give up
2335
+ nothing and the whole squeeze falls on the fixed columns from the very first pixel of
2336
+ deficit. Each one stops at its own min-content, which in the HEADER is smaller than in the
2337
+ ROW, and the result is columns out of alignment.
2338
+
2339
+ WHY IT'S LEFT COMMENTED OUT: locking the shrinkage without containing the overflow trades
2340
+ "squeezed columns" for LEAKAGE, because the container arrives without overflow-x-auto
2341
+ and block [12] paints the background and border shadow ON IT. That's why the fix comes
2342
+ bundled with `overflow-x: auto`, and that means accepting HORIZONTAL SCROLLING inside
2343
+ the card at phone width (the row's minimum goes up to about 664px).
2344
+ `bun run gate` only runs the 5 public routes and NEVER opens /_emdash/admin, so
2345
+ it's no safety net here.
2346
+ TO ENABLE: check it by eye, at 320, 390 and 768, inside the panel, and only then
2347
+ uncomment the whole block. If the studio doesn't want a horizontal bar inside the
2348
+ panel, the right call is to DISCARD the item: the original defect is a cosmetic
2349
+ drift in the header at narrow widths.
2350
+
2351
+ html[data-mode] main div.border.rounded-lg:has(> .border-b.bg-kumo-tint\/50.text-kumo-subtle.flex.items-center.gap-4) {
2352
+ overflow-x: auto;
2353
+ }
2354
+ html[data-mode] main div.border.rounded-lg:has(> .border-b.bg-kumo-tint\/50.text-kumo-subtle.flex.items-center.gap-4)
2355
+ .flex.items-center.gap-4 {
2356
+ min-width: min-content;
2357
+ }
2358
+ html[data-mode] main div.border.rounded-lg:has(> .border-b.bg-kumo-tint\/50.text-kumo-subtle.flex.items-center.gap-4)
2359
+ .flex.items-center.gap-4 > :is(.w-8, .w-14, .w-16, .w-20, .w-32) {
2360
+ flex: 0 0 auto;
2361
+ }
2362
+ html[data-mode] main div.border.rounded-lg:has(> .border-b.bg-kumo-tint\/50.text-kumo-subtle.flex.items-center.gap-4)
2363
+ .flex.items-center.gap-4 > .flex-1 {
2364
+ min-width: 7rem;
2365
+ }
2366
+
2367
+ The anchor is narrower than block [12]'s on purpose: just
2368
+ `:has(> .border-b.bg-kumo-tint/50)` would reach THREE containers, and the
2369
+ Taxonomias one has a different set of columns (w-16 / w-24). Requiring the band
2370
+ itself to be the `flex items-center gap-4` AND `text-kumo-subtle` row leaves
2371
+ exactly the two redirect lists. The 404 panel's `.w-32`
2372
+ MUST be in the set: leaving it out would make it the only elastic column,
2373
+ absorbing 100% of the deficit, which would be WORSE than today. The 7rem floor exists
2374
+ because, with basis 0 and negative free space, the URL columns would end up at exactly
2375
+ 0px (a defect that already happens today, below 440px of usable width).
2376
+ -------------------------------------------------------------------------- */
2377
+
2378
+
2379
+ /* ==========================================================================
2380
+ [16] USER MENU (replaces parts of block [8])
2381
+ ========================================================================== */
2382
+
2383
+ /* [16a] REPLACES the two [8b] rules on lines 1200 to 1206. Paste the WHOLE
2384
+ pair: `translate` is a single property, so you can't write the X axis
2385
+ in one rule and the Y in another, the second would erase the first.
2386
+
2387
+ Y AXIS, already known: kumo asks for `sideOffset: 8` (popover.tsx:189).
2388
+ X AXIS, new, and it gives back the 5px that Base UI's `shift()` steals. kumo doesn't
2389
+ pass `collisionPadding`, so the Positioner falls back to the default of 5
2390
+ (vendor-base-ui:4423); with side="bottom" that default becomes
2391
+ {top:6, right:5, bottom:5, left:5} and shift runs with mainAxis on. Since the
2392
+ skin already zeroes the header's padding (line 181) and pins it to left/right 0 on
2393
+ desktop (262-266), the popup's right border is born exactly on the clipping
2394
+ border: detectOverflow.right = 5 > 0, and Floating UI subtracts 5px from X. The
2395
+ limitShift would only kick in above 15px, so the 5px pass through whole.
2396
+ MEASURED by simulating the real middleware chain across three viewports (1440x900,
2397
+ a short window forcing a flip, and 320px): shift = {x:-5} in all three, a 5px gap on
2398
+ both sides. The value is the same on bottom and on top because the padding
2399
+ calculation comes from the `side` PROP, not from the side flip resolved to.
2400
+
2401
+ Nothing here writes transform: `translate` is independent, composes BEFORE it, doesn't
2402
+ enter the popup's `transition-[transform,scale,opacity]` (applies instantly) and
2403
+ Floating UI measures the POSITIONER, the parent, whose box doesn't change when the child
2404
+ translates. The popup also doesn't use its own transform: Tailwind v4 compiles
2405
+ `scale-90` into the standalone `scale` property.
2406
+
2407
+ CORNER: which one touches the bar depends on the side. In `bottom` (the normal case) it touches the
2408
+ TOP border; in `top` (flip due to lack of space) it touches the BOTTOM one.
2409
+ `!important` IS MANDATORY, not decoration: line 1182 declares
2410
+ `border-radius: var(--wp-radius) !important` and IMPORTANCE is evaluated BEFORE
2411
+ specificity. Without it these declarations lose to 1182 even at
2412
+ (0,5,2) against (0,4,2), and the rule becomes a no-op (measured: 4px on all four
2413
+ corners). The 1182 shorthand still applies as a fallback for lateral data-side
2414
+ (left/right), which IS reachable: Base UI uses fallbackAxisSide 'end', so in a
2415
+ window too short the menu goes sideways, and there no border touches the
2416
+ bar, so all four corners are the right call.
2417
+
2418
+ WATCH: if a kumo upgrade passes an explicit `collisionPadding`, only the
2419
+ fallback number changes. In an RTL interface align=end flips to the left and the sign
2420
+ inverts (--wp-popover-inset: -5px); the panel is pt-BR, so it applies today. Safari
2421
+ without `translate` support falls back to the 8px-plus-5px gap: clean degradation. */
2422
+ html[data-mode] [role="dialog"].kumo-popover-popup[data-side="bottom"]:has(a[href*="/settings/security"]) {
2423
+ translate: var(--wp-popover-inset, 5px) calc(var(--wp-popover-offset, 8px) * -1);
2424
+ border-radius: 0 0 var(--wp-radius) var(--wp-radius) !important;
2425
+ }
2426
+
2427
+ html[data-mode] [role="dialog"].kumo-popover-popup[data-side="top"]:has(a[href*="/settings/security"]) {
2428
+ translate: var(--wp-popover-inset, 5px) var(--wp-popover-offset, 8px);
2429
+ border-radius: var(--wp-radius) var(--wp-radius) 0 0 !important;
2430
+ }
2431
+
2432
+ /* [16b] REPLACES line 1183 (`box-shadow: 0 3px 5px rgba(0,0,0,.2)`).
2433
+ That one was a literal copy from wp-admin: a single, short layer, made for a
2434
+ LIGHT card over a light gray. With a #1d2327 popup over the #f0f0f1 canvas it barely
2435
+ shows up, because the first shadow pixels land right next to an element that's already
2436
+ dark: what makes the menu float here is REACH, not opacity.
2437
+ Four layers, from near to far:
2438
+ 1. contact seals the border on the light background, stops a hard clipping edge
2439
+ 2. penumbra gives thickness to the card
2440
+ 3. ambient the mass that separates the menu from the page
2441
+ 4. diffuse the long reach that gives height, low alpha because the radius is large
2442
+ The negative spread on the last two SHRINKS the lateral leak and pushes the mass
2443
+ downward (doesn't eliminate it: they still leak 5px and 6px on each side).
2444
+ Reach upward is practically nil (layer 2: 0px; 3: -7px; 4: -22px), so
2445
+ nothing dirties the dark bar above the flush menu.
2446
+ DON'T ADD BACK A 1px LAYER: this popup's border is `outline outline-kumo-fill`
2447
+ (popover.tsx:212), a standalone property, not a ring inside box-shadow like
2448
+ the content popups in block [8e]. It survives this declaration, and its
2449
+ color already comes from line 1181.
2450
+ The ink is rgba(29,35,39,...), --wp-ink itself: a shadow tinted with the surface's
2451
+ color sits better on the page's gray than pure black. The shadow isn't in
2452
+ the popup's `transition-[transform,scale,opacity]`, so it doesn't animate.
2453
+ If it gets too heavy, lower the alphas on layers 3 and 4 (that's where the sense of height
2454
+ lives); if it gets too weak, raise layer 4's RADIUS, never layer 1's alpha. */
2455
+ html[data-mode] [role="dialog"].kumo-popover-popup:has(a[href*="/settings/security"]) {
2456
+ box-shadow:
2457
+ 0 1px 2px rgba(29, 35, 39, .16),
2458
+ 0 4px 8px rgba(29, 35, 39, .12),
2459
+ 0 12px 22px -6px rgba(29, 35, 39, .14),
2460
+ 0 28px 44px -16px rgba(29, 35, 39, .20) !important;
2461
+ }
2462
+
2463
+
2464
+ /* ==========================================================================
2465
+ [17] SIDE BAR WITH NO MOTION
2466
+ The owner asked for two things that coexist: MORE hover interpolation in the content
2467
+ (block [19]) and ZERO animation on the bar. Everything here is scoped to the menu, and
2468
+ `transition-property` isn't inherited, so zeroing a parent never reaches the
2469
+ content.
2470
+ ========================================================================== */
2471
+
2472
+ /* [17a] THE ICON DOESN'T SHIFT ON HOVER, it only changes color.
2473
+ The movement was OURS (translateX(1px), lines 569-570), fed by the
2474
+ transition on lines 555-556. There's no competitor on kumo's side: sidebar.tsx
2475
+ only has `group-hover/menu-button:opacity-100` on the chevron (pure opacity), and its
2476
+ `translate-x-[-3px]` on line 1136 is for the COLLAPSED state, already neutralized
2477
+ by lines 383-388.
2478
+ IDEAL: edit it in place (R3.4). These two rules do the same on top: the same
2479
+ selector declared later wins, no !important needed.
2480
+ MAINTENANCE: the transition list now only has `color`. Anyone wanting to animate the
2481
+ icon again needs to re-add the property to the list, otherwise the transform
2482
+ shows up abrupt. */
2483
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"] svg {
2484
+ -webkit-transition: color var(--wp-fast) ease;
2485
+ transition: color var(--wp-fast) ease;
2486
+ }
2487
+
2488
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"]:hover svg {
2489
+ color: var(--wp-accent-soft);
2490
+ -webkit-transform: none;
2491
+ transform: none;
2492
+ }
2493
+
2494
+ /* [17b] COLLAPSE AND EXPAND WITH NO INTERPOLATION.
2495
+ kumo does ALL the menu's transitions with
2496
+ `duration-(--sidebar-animation-duration)` and writes that variable INLINE on the
2497
+ wrapper, at 250ms (sidebar.tsx:376, SIDEBAR_ANIMATION_DURATION_MS = 250, curve
2498
+ cubic-bezier(0.77,0,0.175,1)). !important in an author sheet with NO layer beats the
2499
+ style attribute: measured in Chromium, the wrapper resolves to 0ms with the inline saying
2500
+ 250ms, and the utility .duration-(--sidebar-animation-duration), which lives in
2501
+ @layer utilities with no !important, computes transition-duration 0s and
2502
+ transition-property none.
2503
+
2504
+ One line zeroes all 13 consumers: aside width (659), content-container width
2505
+ (694), footer width and padding (847), viewport padding (794), the item's
2506
+ inner block translate (1136), the trigger icon's little stroke (1512), header
2507
+ padding and the title's grid-cols-[1fr] to [0fr] collapse (admin/Sidebar.tsx
2508
+ 373 and 384), PLUS the mobile drawer (nav with transition-transform, 606) and the mobile
2509
+ backdrop's fade (575). DECLARED CONSEQUENCE: on mobile the menu now opens
2510
+ without sliding.
2511
+ <main> has no transition of its own (Shell.tsx: `flex-1 overflow-y-auto`), so it
2512
+ follows the aside and becomes instant along with it. The collapsed item's tooltip exits through a
2513
+ portal in body, OUTSIDE the wrapper, and keeps whatever section [18] dictates.
2514
+ Doesn't leave a stuck state: there's no transitionend listener in sidebar.tsx, and
2515
+ `resizable` and `peekable` are false by default and the Shell doesn't pass either
2516
+ one. The panel also doesn't use MenuSub, Collapsible or SlidingViews, so
2517
+ zeroing the variable doesn't kill any accordion that exists today. If one ever exists, the
2518
+ way out is to give the duration back only on that node.
2519
+ This selector already exists on line 142 for the same reason (EmDash writes it inline
2520
+ on the wrapper): if editing by hand, merge the declaration there. */
2521
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) {
2522
+ --sidebar-animation-duration: 0ms !important;
2523
+ }
2524
+
2525
+ /* Safety net, in case an upgrade swaps the variable for a hand-written value. Lists
2526
+ ONLY the four nodes whose geometry actually moves and that this sheet
2527
+ doesn't already immobilize some other way: aside and content-container animate width,
2528
+ the footer animates width (its padding is already 0 !important on 491) and the
2529
+ trigger's little stroke moves from translate-x-px to translate-x-[10.5px].
2530
+ LEFT OUT on purpose:
2531
+ . [data-sidebar="header"]: padding already pinned at 0 8px !important (293).
2532
+ . [data-sidebar="menu-button"] > div: translate and transform already pinned at
2533
+ none !important (385-387).
2534
+ . [data-sidebar="content"] > div: padding already pinned at 0 !important (348), and
2535
+ this selector would also catch Base UI's Scrollbar, which is the SECOND div child
2536
+ of ScrollArea.Root and has its own 150ms fade that doesn't take part in the
2537
+ collapse.
2538
+ . [data-sidebar="menu-button"] and [data-sidebar="trigger"] itself: both have
2539
+ OUR color transition on hover (379-380 and 510-511), which the owner wants kept. */
2540
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) > :is(aside,nav).emdash-sidebar,
2541
+ html[data-mode] :is(aside,nav).emdash-sidebar > [data-sidebar="content-container"],
2542
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"],
2543
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] [data-sidebar="trigger"] svg path {
2544
+ -webkit-transition-property: none !important;
2545
+ transition-property: none !important;
2546
+ }
2547
+
2548
+ /* [17c] COLLAPSED: THE ICON DOESN'T MOVE, AND THE BAR SHRINKS FROM 57px TO 36px.
2549
+
2550
+ OPEN-STATE MEASUREMENT: the scroll viewport has zeroed padding (347-351), and
2551
+ neither group, menu, nor menu-item puts padding in kumo (`p-0`, only `gap-y-px`).
2552
+ All the breathing room comes from the item, on line 372. So the icon's left border sits
2553
+ 8px from the menu's border.
2554
+ COLLAPSED TODAY: a 57px bar (SIDEBAR_WIDTH_ICON, sidebar.tsx:93, written
2555
+ inline on the wrapper at 375) with the item centered, so the icon lands at
2556
+ (57 - 20) / 2 = 18.5px. A 10.5px jump on every click.
2557
+ NEW MATH: 8px padding + 20px icon (--wp-icon) + 8px on the other side =
2558
+ 36px, which is wp-admin's collapsed #adminmenu. With the item aligned left, the
2559
+ jump becomes ZERO.
2560
+
2561
+ The new token goes into html:root[data-mode] and not html[data-mode]: (0,1,1)
2562
+ would lose to block [0] on line 20 and the declaration would become dead letter. In
2563
+ calc() from --wp-icon so the bar keeps up on its own if the icon changes.
2564
+ IDEAL: move the token line to block [0], next to line 47, which is where
2565
+ per-client changes happen.
2566
+
2567
+ DOESN'T LEAK INTO THE EDITOR: the right panel is a SECOND Sidebar.Provider
2568
+ (ContentEditor.tsx:667) and is immune for three independent reasons: its wrapper
2569
+ doesn't match :has(.emdash-sidebar), it writes its own --sidebar-width-icon
2570
+ inline (a declaration on the element beats inheritance) and it's collapsible="offcanvas", which
2571
+ doesn't even use the variable.
2572
+ TOUCH TARGET: the collapsed item is 36 by 36, above the gate's 24px. The label
2573
+ stays accessible via the tooltip, because `peekable` is false by default and
2574
+ kumo's condition is `state === "collapsed" && !peekable`.
2575
+ The `border-r` kumo puts on the footer when collapsing doesn't need to go: measured,
2576
+ a right border doesn't move an icon anchored to the left (a 36px footer with
2577
+ border-box, a 35px content box, the icon still sits at 8px and
2578
+ 8 + 20 = 28 < 35, doesn't even clip). */
2579
+ html:root[data-mode] {
2580
+ --wp-menu-w-icon: calc(8px + var(--wp-icon) + 8px); /* 36px today */
2581
+ }
2582
+
2583
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar) {
2584
+ --sidebar-width-icon: var(--wp-menu-w-icon) !important;
2585
+ }
2586
+
2587
+ /* padding-LEFT still comes from line 372, which applies in both states: that's what
2588
+ stops the icon from moving. Only padding-RIGHT is readjusted here, because the
2589
+ 16px in [17d] exist to keep the label away from the active item's little arrow, and in the
2590
+ collapsed state there's no label or arrow (line 438 hides the ::after). */
2591
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar)[data-state="collapsed"] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"] {
2592
+ justify-content: flex-start !important;
2593
+ padding-right: 8px !important;
2594
+ }
2595
+
2596
+ html[data-mode] [data-sidebar-wrapper]:has(.emdash-sidebar)[data-state="collapsed"] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"] > div {
2597
+ flex: 0 0 auto !important;
2598
+ justify-content: flex-start !important;
2599
+ }
2600
+
2601
+ /* the "Recolher menu" button follows the same ruler. Anchored on aria-expanded, which is
2602
+ boolean (sidebar.tsx:1544) and doesn't fall into the third "peeking" state, same as the
2603
+ criterion already adopted in section [4B]. The padding comes from line 500. The icon here is the
2604
+ ::before with a mask from [4B] (kumo's <svg> is hidden on 1718), also at
2605
+ 20px: same geometry, same math. */
2606
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="footer"] [data-sidebar="trigger"][aria-expanded="false"] {
2607
+ justify-content: flex-start !important;
2608
+ }
2609
+
2610
+ /* [17d] BREATHING ROOM ON THE ITEM'S RIGHT.
2611
+ Today padding-right is 8px and the active item's little arrow occupies exactly those
2612
+ 8px: the ::after on line 410 is a 16x16 box (border 8px) anchored to
2613
+ right: 0, and its visible part is the triangle from the rightmost 8px. The text
2614
+ ends right at the tip, zero slack. With 16px, 8px is left between the end of the text and
2615
+ the arrow's tip, and the same breathing room against the border on items with no arrow.
2616
+ THE ARROW DOESN'T MOVE: `right: 0` anchors to the padding-box, and padding doesn't shift
2617
+ the padding-box.
2618
+ ACCEPTED COST: the label drops from 186px to 178px with the menu at 230px, about
2619
+ one character at 14px. The compensation (menu at 240px) is left out because this
2620
+ round asked for a narrower bar, and the note is in R3.5.
2621
+ The clickable area doesn't shrink: padding is part of the button's area. The submenu item's
2622
+ chevron (`ml-auto`) and the MenuBadge get the same breathing room, which is an improvement.
2623
+ IDEAL: edit line 372 (R3.5). This rule does the same on top. */
2624
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="menu-button"] {
2625
+ padding: 0 16px 0 8px !important;
2626
+ }
2627
+
2628
+
2629
+ /* ==========================================================================
2630
+ [18] COLLAPSED MENU: THE LABEL BECOMES A TAB STUCK TO THE ICON (wp-admin)
2631
+
2632
+ The label is a kumo Tooltip (Base UI) PORTALED into <body>, so it's not a
2633
+ descendant of the menu and no ancestor reaches it. The anchor has two halves:
2634
+ :has([data-sidebar="menu-button"][data-popup-open])
2635
+ whoever opened the popup is a menu item. Without this guard the rule would catch
2636
+ ANY popup with data-side="right", and Base UI produces that value on
2637
+ its own: its default is fallbackAxisSideDirection "end"
2638
+ (internals/useAnchorPositioning.js:183), so a side="bottom" tooltip from the
2639
+ editor bar with no room above or below tries the perpendicular axis
2640
+ and lands on "right". The guard doesn't flicker on close: with no animation, Base UI
2641
+ does a flushSync unmount before paint (useAnimationsFinished.js:70-85).
2642
+ .kumo-tooltip-popup[data-side="right"]
2643
+ a class kumo writes by hand on the Popup plus the attribute Base UI
2644
+ emits (TooltipPopup.js puts side and align in the state). The second line of each
2645
+ pair targets the same thing via the Positioner, in case the library moves data-side up a level.
2646
+ `side="right"` is hardcoded in Sidebar.MenuButton (sidebar.tsx:1225) and
2647
+ EmDash only passes `tooltip` when `state === "collapsed"`, with peekable false:
2648
+ there's no empty popup with the menu open.
2649
+
2650
+ Positioning is still Floating UI's job: no rule here writes
2651
+ transform, position, inset or overflow. The only displacement is `translate`, and
2652
+ it exists to undo kumo's `sideOffset: 10` (Base UI applies
2653
+ offset({mainAxis}) without adding the arrow), which sticks the tab to the rail's border.
2654
+
2655
+ MEASURED IN THE BROWSER with styles.css and this sheet loaded: Positioner at
2656
+ x=57, popup painted at x=47, 36px height same as the item's, text 9.4px from the top
2657
+ and 10.1px from the bottom. Contrast 5.78:1 on normal (#90b5d8 on #303539) and 5.17:1
2658
+ on active (#ffffff on #2271b1), both AA.
2659
+
2660
+ MEASURED PITFALL, and it's the reason for the `flex-direction` line: kumo's Popup
2661
+ carries `flex-col` together with `flex`. Without undoing that, `align-items: center`
2662
+ centers on the HORIZONTAL axis and the label sticks to the top, with 19px left over
2663
+ at the bottom. The tab would ship looking fixed and come out crooked.
2664
+
2665
+ WHAT HAS NO CSS FIX, and is stated here: the 600ms delay on the first
2666
+ pass (Base UI's OPEN_DELAY, and neither kumo nor EmDash passes `delay`; inside
2667
+ the TooltipProvider group the following items open instantly), and the tab not
2668
+ being clickable, because it's a popup, not a link.
2669
+ WATCH: the -10px is tied to kumo's sideOffset. If the library changes the value,
2670
+ a gap or an overlap comes back (visible, doesn't break). If the tooltip needs to
2671
+ flip sides for lack of room, data-side stops being "right" and the skin
2672
+ simply stops applying, falling back to kumo's balloon: graceful failure.
2673
+ ========================================================================== */
2674
+
2675
+ /* [18a] THE TRANSITION DIES OUTSIDE THE GUARD, AND THIS POSITION IS THE FIX.
2676
+ MEASURED DEFECT (round 3, three tabs stuck on screen while sweeping down the column): the
2677
+ `transition: none` line lived INSIDE the block guarded by
2678
+ :has([data-sidebar="menu-button"][data-popup-open]). A global guard means
2679
+ it TURNS ITSELF ON AND OFF, and Base UI unmounts the popup by waiting for
2680
+ its own `getAnimations()` (popupStoreUtils.js:448 calls
2681
+ useOpenChangeComplete with ref = popupRef, which lands in useAnimationsFinished.js:85,
2682
+ `Promise.all(getAnimations().map(a => a.finished))`).
2683
+ The sequence that gets stuck, timed in the browser with getAnimations instrumented:
2684
+ 1. item A closes, Base UI removes data-popup-open from trigger A;
2685
+ 2. at that instant NO trigger has data-popup-open, the guard drops, this sheet
2686
+ stops applying to popup A and kumo's transition comes back to life;
2687
+ 3. Base UI looks at popup A and sees TWO transitions running (measured: opacity and
2688
+ scale, computed transition "transform .15s, scale .15s", not "none"),
2689
+ so it waits for both `finished` promises;
2690
+ 4. item B opens 30ms later, data-popup-open reappears, the guard rises again
2691
+ and reimposes `transition: none` on popup A too, because the selector is global;
2692
+ 5. `transition: none` CANCELS both transitions, `finished` rejects, and
2693
+ popup A never unmounts. Every icon in the sweep leaves one more stuck.
2694
+ COUNT: a sweep of 5 icons, 900ms pause on each, 1.5s idle at the end.
2695
+ Before: 3 to 4 popups stuck with data-closed and data-ending-style. With this line
2696
+ outside the guard, ZERO.
2697
+ The native escape path EXISTS and is the right one: with no animation at all,
2698
+ `Promise.all([])` resolves instantly and the unmount happens via flushSync before paint. What
2699
+ was missing was guaranteeing the popup is ALWAYS animation-free, not only
2700
+ while the guard is standing. That's why the line doesn't go back to being a 1ms
2701
+ transition: the problem was never the duration, it was the toggling.
2702
+ SCOPE: only the tooltip popup with data-side "right", the same set the skin
2703
+ already paints. The declared cost is that a tooltip from another component that lands on
2704
+ "right" for lack of space loses the 150ms fade. Trading animation for nothing is the
2705
+ direction of this round, so the cost is aligned with the request. */
2706
+ html[data-mode] [data-base-ui-portal] .kumo-tooltip-popup[data-side="right"],
2707
+ html[data-mode] [data-base-ui-portal] [data-side="right"] > .kumo-tooltip-popup {
2708
+ transition: none !important;
2709
+ }
2710
+
2711
+ /* [18b] THE PAINTING STAYS GUARDED, BUT NOW TIED TO THE OPEN POPUP.
2712
+ `[data-open]` on the popup itself is the only bridge that exists: MEASURED in the browser,
2713
+ the popup has no id, no data-anchor, no attribute at all pointing back to the
2714
+ trigger, and the trigger (id="base-ui-_r_l_", data-base-ui-tooltip-trigger) isn't
2715
+ referenced by anything on the portal side. A real bridge between EACH tab and
2716
+ ITS button doesn't exist in CSS.
2717
+ What `[data-open]` solves: Base UI only leaves one popup with data-open at a time,
2718
+ so the dark painting stops leaking onto the tabs that are closing. Without it,
2719
+ the guard is global and paints EVERY tab in the portal, which is exactly the screenshot of
2720
+ three dark tabs stacked up. With [18a] in place there's only ever one tab at a time, and
2721
+ this line is the belt: if one is ever left over, it comes out without the skin instead of becoming
2722
+ a second dark tab on screen. */
2723
+ html[data-mode]:has([data-sidebar="menu-button"][data-popup-open]) [data-base-ui-portal] .kumo-tooltip-popup[data-side="right"][data-open],
2724
+ html[data-mode]:has([data-sidebar="menu-button"][data-popup-open]) [data-base-ui-portal] [data-side="right"] > .kumo-tooltip-popup[data-open] {
2725
+ translate: -10px 0 !important;
2726
+ display: flex !important;
2727
+ flex-direction: row !important; /* undoes kumo's flex-col */
2728
+ align-items: center !important; /* only centers vertically after the row */
2729
+ justify-content: flex-start !important;
2730
+ min-height: 36px !important; /* the same height as the menu item */
2731
+ padding: 0 12px !important;
2732
+ background: var(--wp-ink-hover) !important;
2733
+ color: var(--wp-accent-soft) !important;
2734
+ font-family: var(--wp-font) !important;
2735
+ font-size: 14px !important;
2736
+ font-weight: 400 !important;
2737
+ line-height: 1.3 !important;
2738
+ white-space: nowrap !important;
2739
+ border: 0 !important;
2740
+ border-radius: 0 !important;
2741
+ outline: 0 !important;
2742
+ box-shadow: none !important;
2743
+ /* zero animation, which is the bar's side of the deal this round. The `transition` line
2744
+ that used to be HERE moved to block [18a], outside the guard: inside it, it turned on and
2745
+ off and kept the popup mounted forever (the measurement is up above).
2746
+ kumo's entry `scale-90` compiles into the standalone `scale` property,
2747
+ so it can be killed without touching transform. */
2748
+ scale: 1 !important;
2749
+ opacity: 1 !important;
2750
+ }
2751
+
2752
+ /* the beak doesn't exist in wp-admin. Base UI's Arrow is the aria-hidden child.
2753
+ Same `[data-open]` as [18b]: the beak only disappears on the tab that's open, so the
2754
+ pair of rules doesn't drift apart if a closing popup is ever left on screen. */
2755
+ html[data-mode]:has([data-sidebar="menu-button"][data-popup-open]) [data-base-ui-portal] .kumo-tooltip-popup[data-side="right"][data-open] > [aria-hidden="true"],
2756
+ html[data-mode]:has([data-sidebar="menu-button"][data-popup-open]) [data-base-ui-portal] [data-side="right"] > .kumo-tooltip-popup[data-open] > [aria-hidden="true"] {
2757
+ display: none !important;
2758
+ }
2759
+
2760
+ /* current item: in wp-admin the active item's tab comes out blue, not gray. The popup is in the
2761
+ portal, far from the item, so the bridge is the TRIGGER: Base UI puts data-popup-open
2762
+ on the <a> itself while the label is open, and only one stays open at a time.
2763
+ The popup's `[data-open]` closes the other end of the bridge: without it, an active item with
2764
+ its label opening would paint ANY tab in the portal blue, including ones that
2765
+ were closing. */
2766
+ html[data-mode]:has([data-sidebar="menu-button"][data-active="true"][data-popup-open]) [data-base-ui-portal] .kumo-tooltip-popup[data-side="right"][data-open],
2767
+ html[data-mode]:has([data-sidebar="menu-button"][data-active="true"][data-popup-open]) [data-base-ui-portal] [data-side="right"] > .kumo-tooltip-popup[data-open] {
2768
+ background: var(--wp-accent) !important;
2769
+ color: var(--wp-accent-contrast) !important;
2770
+ }
2771
+
2772
+
2773
+ /* ==========================================================================
2774
+ [19] HOVER INTERPOLATION IN THE CONTENT
2775
+ The other side of this round's request. Everything scoped to `main`, which is a SIBLING of
2776
+ the side bar in the Shell (Sidebar.Provider > [SidebarNav, div.flex-1 > [Header,
2777
+ main]]): disjoint subtrees, so nothing here reaches the menu.
2778
+ No rule uses `transition: all`: an unenumerated property is a property
2779
+ that a kumo upgrade could start animating without anyone deciding to.
2780
+ ========================================================================== */
2781
+
2782
+ /* [19a] WHERE NOBODY DECLARED ONE.
2783
+ MEASURED in the bundle: kumo 2.6.0 is inconsistent. Of the six Button variants, only
2784
+ "outline" carries `transition-colors`; the DEFAULT one ("secondary": `bg-kumo-base ...
2785
+ not-disabled:hover:bg-kumo-tint`) and "ghost" change color on hover with no
2786
+ transition at all. The same goes for SettingsNavRow (index.js:35998), which
2787
+ arrives with `hover:bg-kumo-tint` and zero transition class. A className string
2788
+ sweep: 112 carry `hover:bg|text|border-kumo`, and 100 of them have no
2789
+ transition whatsoever.
2790
+
2791
+ ANCHOR = the very CLASS that declares the hover, not the component. Tailwind v4
2792
+ keeps the literal name in the attribute, so `hover:bg-kumo` as a substring matches
2793
+ exactly whoever changes color on hover, and it also catches `group-hover:`,
2794
+ `not-disabled:hover:` and `aria-selected:hover:`, which are the same case.
2795
+
2796
+ THREE GUARDS:
2797
+ 1. `:not([class*="transition"])` = only where Tailwind hasn't already declared one.
2798
+ Avoids replacing Base UI's `transition-[transform,scale,opacity]` and the
2799
+ outline variant's `transition-colors`.
2800
+ 2. `:not(.emdash-sidebar *)` = an upgrade safety net, since the `main` scope alone
2801
+ already makes it impossible to reach the bar.
2802
+ 3. `:where()` on the WHOLE match, which counts as zero specificity. Without it the
2803
+ rule would weigh (0,4,2) and would RUN OVER the transitions this skin already declares,
2804
+ because `transition` is a shorthand: whichever one wins REPLACES the whole list, doesn't add to it.
2805
+ The victims were real and measured: lines 610, 629 and 1649 (buttons next to the
2806
+ h1, which animate background-color, border-color, color, box-shadow and transform,
2807
+ and are default-variant Buttons, meaning they match `hover:bg-kumo`), 737
2808
+ (card), 728 (links) and 811 (table row). Without :where(), the "Novo"
2809
+ button would lose the list's box-shadow and transform and, since :hover keeps
2810
+ applying translateY(-1px), it would start SNAP-JUMPING 1px. The opposite of what was asked.
2811
+ With :where() the rule weighs (0,1,2), loses to all of them and only paints what
2812
+ nobody else declared.
2813
+
2814
+ REACH, stated plainly: NO POPUP GETS IN. The user menu is a Popover with a
2815
+ portal in body, so outside `main`, and stays without interpolation, along with the toast and
2816
+ the side menu's popup. This is deliberate: widening the scope to body would touch
2817
+ the popups' entry and exit animation.
2818
+ KNOWN RESIDUE: at (0,1,2) rule 728 (`main a`) still wins on
2819
+ links, and it lists only color and background-color. So an <a> with
2820
+ `hover:border-kumo-*` (the editor's index) keeps a jerky border. Fixing that means
2821
+ editing 728, which is a different rule and deserves its own round. */
2822
+ html[data-mode] main :where(
2823
+ [class*="hover:bg-kumo"],
2824
+ [class*="hover:text-kumo"],
2825
+ [class*="hover:border-kumo"]
2826
+ ):where(:not([class*="transition"])):where(:not(.emdash-sidebar *)) {
2827
+ -webkit-transition:
2828
+ background-color var(--wp-fast) ease,
2829
+ color var(--wp-fast) ease,
2830
+ border-color var(--wp-fast) ease;
2831
+ transition:
2832
+ background-color var(--wp-fast) ease,
2833
+ color var(--wp-fast) ease,
2834
+ border-color var(--wp-fast) ease;
2835
+ }
2836
+
2837
+ /* [19b] THE SHADOW FOR THE OTHER TWO FAMILIES IN BLOCK [7].
2838
+ The hover on line 884 lifts the shadow on THREE families, but only the first (the
2839
+ panel card) has a transition, on 740. The table frame (7 listings) and the
2840
+ settings block (10 Ajustes screens) used to swap all four box-shadow layers at
2841
+ once: a snap, not a lift. That's what the owner complained about.
2842
+ DURATION: --wp-normal with --wp-ease, identical to line 740, so the listing rises
2843
+ at the SAME pace as the widget he already approved. Elevation is a bigger visual delta
2844
+ than a background tone, so it can take the slower curve without feeling sluggish.
2845
+
2846
+ The first four guards are a faithful copy of lines 885-886 and aren't decoration:
2847
+ they exclude the marketplace skeleton, the user detail skeleton, the editor's
2848
+ floating bar and the plugin consent dialog.
2849
+ FIFTH GUARD, `:not([class*="transition"])`: `transition` is a SHORTHAND and resets
2850
+ transition-property. Tailwind's transition-* utilities live in
2851
+ @layer utilities WITHOUT !important, and a normal declaration OUTSIDE a layer beats a normal
2852
+ one INSIDE a layer. Without this guard the rule was ERASING the own transition of nine
2853
+ elements, among them the editor's HTML block (HtmlBlockNode.tsx:101) and plugin block
2854
+ (PluginBlockNode.tsx:281), which animate the selected state's background with
2855
+ `transition-colors`: it would trade a shadow snap in the listing for a color snap
2856
+ in the editor. The guard fails safe (whoever already has their own transition keeps it
2857
+ as is) and loses no target: the 7 frames and the 10 settings blocks
2858
+ don't carry "transition" in their class string.
2859
+ No `transform`: these two boxes don't lift, only the shadow changes (line 895
2860
+ is only for the card). */
2861
+ html[data-mode] main .overflow-x-auto.border:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]):not([class*="transition"]),
2862
+ html[data-mode] main div.border.bg-kumo-base:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]):not([class*="transition"]) {
2863
+ -webkit-transition: box-shadow var(--wp-normal) var(--wp-ease);
2864
+ transition: box-shadow var(--wp-normal) var(--wp-ease);
2865
+ }
2866
+
2867
+ /* [19c] SAME FIX FOR THE <div>-BASED LISTING IN BLOCK [12] (Taxonomias and the
2868
+ two Redirecionamentos ones). Without this they'd end up being the only listings
2869
+ that still snap after the fix, and the divergence would just move somewhere else. Same
2870
+ pace as [19b]. The only value that changes on line 1567's :hover is box-shadow,
2871
+ so the list is exact, no leftovers.
2872
+ None of the three containers carries a transition-* class, and there's no
2873
+ `!transition*` utility in styles.css: a normal, unlayered declaration wins on its own,
2874
+ with no !important. */
2875
+ html[data-mode] main div.border.rounded-lg:has(> .border-b.bg-kumo-tint\/50) {
2876
+ -webkit-transition: box-shadow var(--wp-normal) var(--wp-ease);
2877
+ transition: box-shadow var(--wp-normal) var(--wp-ease);
2878
+ }
2879
+
2880
+ /* [19d] EDITOR'S COLLAPSIBLE HEADER (Sumario, Revisoes).
2881
+ Matches exactly TWO buttons: the DocumentOutline's and RevisionHistory's
2882
+ Collapsible.Trigger, the only two sections created with `disclosure: true`
2883
+ (index.js:10385 and 10400). The drag handle on the same section does NOT get in: dnd-kit
2884
+ only emits aria-describedby, aria-disabled, aria-pressed and
2885
+ aria-roledescription, never aria-expanded. The other six sections come out with
2886
+ data-disclosure="false".
2887
+ WHY ITS OWN RULE AND NOT the generic [19a]: the button IS inside `main` and
2888
+ kumo's ghost variant already puts `hover:bg-kumo-tint` on it, so the generic rule could
2889
+ reach it. This rule exists to win by specificity and LOCK the property
2890
+ list. The hover that applies here is line 1448's, and the base on 1424 has no
2891
+ transition at all.
2892
+ `border-color` is included because the band has a border-bottom and an upgrade that starts
2893
+ swapping it on hover would already arrive interpolated. NO `width` at all: line 1425 writes
2894
+ `width: calc(100% + 32px) !important` to beat EmDash's inline style, and animating
2895
+ that would wobble the whole band. Also no transform or position: dnd-kit
2896
+ writes an inline transform on the SECTION during the drag.
2897
+ No !important on purpose: a normal, unlayered rule beats a normal one in a layer, and the
2898
+ only `transition` with !important in styles.css lives in `.transition-none!`,
2899
+ `.t-text-swap` and `[data-sidebar-popup]`, none of which this button carries. */
2900
+ html[data-mode] section[data-disclosure="true"] button[aria-expanded] {
2901
+ -webkit-transition: background-color var(--wp-fast) ease, border-color var(--wp-fast) ease;
2902
+ transition: background-color var(--wp-fast) ease, border-color var(--wp-fast) ease;
2903
+ }
2904
+
2905
+ /* [19e] WHOEVER TURNED OFF SYSTEM ANIMATIONS.
2906
+ A COLOR change isn't motion and stays: it's what makes hover legible. What
2907
+ bothers someone who asked for less animation is the four-layer shadow at .28s, so
2908
+ that one (and any transform) becomes instant, and only that one.
2909
+ WHY THE WHOLE LIST AND NOT `transition: none`: `none` is a blanket switch and
2910
+ would also erase the color transition. Measured: excluding the shadow-lg
2911
+ and animate-pulse guards, [7]'s selectors match 41 class strings and 8 already carry
2912
+ EmDash's OWN transition (transition-colors on the theme card, the plugin
2913
+ card and the dropzone that gets a ring on isOver; transition-all and
2914
+ transition-opacity on the Media cards). They live in @layer utilities without
2915
+ !important, so the erasure would actually happen.
2916
+ ORDER MATTERS: the selectors repeat [19b]'s and [19c]'s, so this block has
2917
+ to stay AFTER them, as it does. The `:hover` is here because, if the
2918
+ transition is ever also declared on the hover rule, the base-only version would cover the
2919
+ exit and not the entry.
2920
+ The block on line 1009 still applies to the entry animations and to the
2921
+ card's transform. */
2922
+ @media (prefers-reduced-motion: reduce) {
2923
+ html[data-mode] main .overflow-x-auto.border:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]),
2924
+ html[data-mode] main .overflow-x-auto.border:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]):hover,
2925
+ html[data-mode] main div.border.bg-kumo-base:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]),
2926
+ html[data-mode] main div.border.bg-kumo-base:not([class*="shadow-lg"]):not([class*="animate-pulse"]):not(.animate-pulse *):not([data-emdash-inline-bubble-menu]):hover,
2927
+ html[data-mode] main div.border.rounded-lg:has(> .border-b.bg-kumo-tint\/50),
2928
+ html[data-mode] main div.border.rounded-lg:has(> .border-b.bg-kumo-tint\/50):hover {
2929
+ -webkit-transition:
2930
+ background-color var(--wp-fast) ease,
2931
+ border-color var(--wp-fast) ease,
2932
+ color var(--wp-fast) ease,
2933
+ opacity var(--wp-fast) ease,
2934
+ box-shadow 0s,
2935
+ -webkit-transform 0s;
2936
+ transition:
2937
+ background-color var(--wp-fast) ease,
2938
+ border-color var(--wp-fast) ease,
2939
+ color var(--wp-fast) ease,
2940
+ opacity var(--wp-fast) ease,
2941
+ box-shadow 0s,
2942
+ transform 0s;
2943
+ }
2944
+ }
2945
+
2946
+ /* ==========================================================================
2947
+ [20] THE SITE LOGO DOESN'T MOVE WHEN THE BAR COLLAPSES
2948
+ ========================================================================== */
2949
+
2950
+ /* MEASURED in dev at 1400px, the <img> inside [data-sidebar="header"]:
2951
+ expanded left = 8px (MENU icon right below: 8px)
2952
+ collapsed left = 5px (MENU icon right below: 8px)
2953
+ In other words, round 3 stopped the menu icon in both states and only the
2954
+ header was left out of square, 3px to the left.
2955
+
2956
+ CULPRIT, and it's not ours: kumo puts the utility
2957
+ `group-data-[state=collapsed]/sidebar:-translate-x-[3px]` on the header's <a> (plus its
2958
+ sibling `rtl:group-data-[state=collapsed]/sidebar:translate-x-[3px]`). In Tailwind v4
2959
+ that utility writes the `translate` property, not `transform`: the computed value
2960
+ is `translate: -3px` when collapsed and `none` when expanded. The 3px is exactly the
2961
+ measured difference, and that's why the <a> already appears shifted without any ancestor
2962
+ having changed its box.
2963
+
2964
+ PADDING DOESN'T TAKE PART, checked element by element all the way to the <aside>:
2965
+ . [data-sidebar="header"]: 8px in both states, pinned on line 293 on top
2966
+ of kumo's px-[11px] / px-3.5 (which does change with the state).
2967
+ . the <a> itself: 0, pinned on line 303 on top of ps-2.5.
2968
+ . content-container and aside: no padding-left.
2969
+ Sanity check: if the header's padding had been left out of round 3,
2970
+ the jump would have been 3 + 3 = 6px, not 3px.
2971
+
2972
+ FIX IN THE SPIRIT OF ROUND 3: a SINGLE value that applies in BOTH states. On
2973
+ purpose there is NO [data-state="collapsed"] selector here with the other side's value,
2974
+ nor a more specific rule redeclaring padding: a pair of per-state values is exactly
2975
+ what diverges again on the next upgrade. The logo stays put, it's the bar that shrinks.
2976
+ `transform: none` comes along as a safety net, exactly like the rule
2977
+ on lines 386-391 already does on the menu item's inner block, in case a Tailwind
2978
+ upgrade goes back to compiling translate-x into the transform matrix.
2979
+
2980
+ DOESN'T BREAK THE GOLDEN RULE: the header's <a> isn't a popup and there's no Floating UI
2981
+ on it; what's written here ZEROES a displacement from kumo instead of creating one
2982
+ of our own.
2983
+
2984
+ THE SITE NAME LABEL NEXT TO IT DOESN'T PUSH THE LOGO, and isn't being touched
2985
+ here. It doesn't even disappear when the bar collapses: line 320 pins
2986
+ `grid-template-columns: 1fr` on the grid <span>, canceling out kumo's
2987
+ `grid-cols-[1fr]` -> `grid-cols-[0fr]` collapse, because the header here is the
2988
+ fixed toolbar's left corner (lines 271-279), in the spirit of wp-admin, not
2989
+ a column row that shrinks. A prior decision, kept. Even if kumo's collapse is
2990
+ ever wanted back, it wouldn't touch the logo: the label comes AFTER
2991
+ the <img> in the flex order and the track closes from right to left.
2992
+
2993
+ IDEAL: merge these three declarations into the rule on line 299, which is their
2994
+ natural home. Left for the consolidation round, alongside the other debts
2995
+ already noted in R3.5, because touching the middle of the file now would shift more
2996
+ than forty file-line pointers in this very sheet. */
2997
+ html[data-mode] :is(aside,nav).emdash-sidebar [data-sidebar="header"] a {
2998
+ translate: none !important;
2999
+ -webkit-transform: none !important;
3000
+ transform: none !important;
3001
+ }
3002
+
3003
+
3004
+ /* ==========================================================================
3005
+ [20] TWO FIXES WITH NO HOME IN THE BLOCKS ABOVE
3006
+ (round 5, 2026-09-08)
3007
+ ========================================================================== */
3008
+
3009
+ /* [20a] THE SEARCH FIELD FILLS ITS OWN WRAPPER.
3010
+
3011
+ UPSTREAM BUG, measured in the fixture: EmDash wraps every panel search in
3012
+ `<div class="relative flex-1 max-w-md">` with the icon at `absolute start-3`, but
3013
+ kumo's `<Input>` does NOT carry `w-full` (inputVariants, input-f2ct7obgdzypjmp2.js: only
3014
+ `h-9 gap-1.5 rounded-lg px-3`). The field ends up with the native <input>'s intrinsic width
3015
+ while the wrapper still reserves `max-w-md`'s 448px. Measured on the Secoes page:
3016
+ wrapper 448px, field 197px, meaning a 250px hole between the search and the
3017
+ "Todas as origens" filter, which was the owner's complaint (it appeared floating loose in the middle of the row).
3018
+
3019
+ One rule fixes all NINE searches in the panel, which all share the same design (Secoes,
3020
+ ContentPicker, MediaPicker, SectionPicker, Marketplace, Registry, ThemeMarketplace,
3021
+ 404): with the field filling the wrapper, the filter next to it sits flush against the container's own
3022
+ 16px gap, which is the requested "stuck to the section search" look.
3023
+ SCOPE: `> input` direct child, so as not to reach a button or card that also uses
3024
+ `relative ... flex-1` (MediaFolderCard). */
3025
+ html[data-mode] main div.relative[class*="flex-1"] > input {
3026
+ width: 100% !important;
3027
+ }
3028
+
3029
+ /* [20b] ANTI-CLS ON THE PANEL'S METRIC CARDS.
3030
+
3031
+ Measured in the fixture: loaded card 89px, loading card 68px. The 21px jump comes
3032
+ entirely from the body (LayerCard.Primary), which holds the number at a 28.8px line height and
3033
+ the skeleton at 8px. The header no longer jumps, because line ~760 pins min-height 36px
3034
+ on it.
3035
+
3036
+ ANCHOR: `.text-3xl` is the class EmDash puts on the body in BOTH states (SummaryMetrics
3037
+ passes `text-3xl font-semibold leading-none` on both the `loading` branch and the loaded one),
3038
+ so the minimum height applies right from the first paint.
3039
+ THE NUMBER: 1.2em of 24px = 28.8px line height, the same the `.text-3xl` on line ~795
3040
+ defines, plus the body's 24px padding. If one of the two ever changes, change this one along with it. */
3041
+ html[data-mode] main .bg-kumo-elevated.ring-kumo-hairline > .ring-kumo-fill.text-3xl {
3042
+ min-height: calc(1.2em + 24px);
3043
+ }
3044
+
3045
+ /* [20c] CHECKABLE LISTS (taxonomies, bylines, create taxonomy): only the corner.
3046
+
3047
+ Measured in the admin bundle: `CategoryCheckboxTree`, `BylineFilter` and `CreateTaxonomyDialog`
3048
+ draw each row as `rounded px-2 hover:bg-kumo-tint/50`, the same inset highlight with a
3049
+ round corner as the other menus. Here ONLY the corner dies, not the inset: these three lists
3050
+ live inside a popover and a dialog whose wrapper hasn't been measured yet in the logged-in DOM, and
3051
+ zeroing a container's padding without measuring is the kind of guess that breaks silently. It's
3052
+ noted as a debt: with the panel open, measure the wrapper and complete the edge-to-edge treatment.
3053
+ ANCHORED by attribute, because the Tailwind class carries `/` and `:`. */
3054
+ html[data-mode] [class*="hover:bg-kumo-tint/50"][class*="rounded"] {
3055
+ border-radius: 0 !important;
3056
+ }
3057
+
3058
+ /* [20d] THE CREDITS BLOCK'S "+" BUTTON FITS INSIDE THE BAND (round 5.1).
3059
+
3060
+ Measured in the bundle: EmDash positions that trigger ABSOLUTE, `className="absolute end-14
3061
+ top-2"`, counting on the section's original layout (with no header band). Our
3062
+ skin turns the title into a 37px band, and a 36px button starting at 8px
3063
+ overflows by 7px: that's why its focus ring appeared clipped, on top of the band's
3064
+ border, and not aligned with the title.
3065
+ NUMBERS: 37px band, 32px button, 2.5px left over on each side; `top: 2px` stays within
3066
+ half-a-pixel rounding error and needs no transform (forbidden on an element dnd-kit drags).
3067
+ The band's `padding-inline-end` goes from 40px (just the handle) to 92px (handle + button + slack)
3068
+ ONLY on the section that has the button, so the title never runs under it. */
3069
+ html[data-mode] section[data-sorting] .absolute.end-14.top-2 {
3070
+ top: 2px !important;
3071
+ width: 32px !important;
3072
+ height: 32px !important;
3073
+ min-height: 0 !important;
3074
+ }
3075
+
3076
+ html[data-mode] section[data-sorting]:has(.absolute.end-14.top-2) > div > div.flex:has(> h3),
3077
+ html[data-mode] section[data-sorting]:has(.absolute.end-14.top-2) > div > h3,
3078
+ html[data-mode] section[data-sorting]:has(.absolute.end-14.top-2) > div > div > h3 {
3079
+ padding-inline-end: 92px !important;
3080
+ }
3081
+
3082
+
3083
+ /* ==========================================================================
3084
+ [21] THE RIGHT PANEL'S HEADER BAND: TITLE ON THE LEFT, EVERYTHING ELSE ON
3085
+ THE RIGHT, ALL THE SAME SIZE AND WITH THE SAME HOVER
3086
+ (round 5.2, 2026-09-08, measured on the logged-in panel via CDP)
3087
+ ========================================================================== */
3088
+
3089
+ /* WHAT WAS MEASURED, in the editor's "Creditos" block (36.6px band, x from 1361 to 1728):
3090
+ h3 "Creditos" x=1377 (16px from the border, correct)
3091
+ button (i) "Why are bylines..." x=1437.6 14x14, radius 2px <- stuck to the title
3092
+ button "+" (Popover.Trigger) absolute end-14 top-2, 36x36 <- was overflowing
3093
+ button handle [data-sortable-handle] x=1694 28x28, radius 0
3094
+ Three sibling buttons with related roles, three sizes, three positions and three different hovers.
3095
+
3096
+ THE TARGET: title on the left; (i), "+" and the handle flush right, 28x28, 4px between them,
3097
+ 6px from the border. The band's breathing-room math comes from that: 6 + 28 + 4 = 38px without the "+",
3098
+ 6 + 28 + 4 + 28 + 4 = 70px with it.
3099
+ WHY `margin-inline-start: auto` on the (i): it's a flex child; the other two are
3100
+ `absolute`, positioned by EmDash, so it's the only piece that needs to MOVE.
3101
+ DEGRADATION: a browser without :has() only loses the extra breathing room; the buttons stay the same
3102
+ size and with the same hover. */
3103
+
3104
+ /* SINGLE HEIGHT: the band is now 36px on every block, with content centered. Without this the
3105
+ 28px button was pushing the header itself (measured: the Creditos band went to 45px and the
3106
+ handle, anchored at `top: 4px`, was 4px off center). The h3 INSIDE the flex band needs to
3107
+ go back to being plain text: the height rule matches it too, and then the band would add up to 36 + 8. */
3108
+ html[data-mode] section[data-sorting] > div > h3,
3109
+ html[data-mode] section[data-sorting] > div > div > h3,
3110
+ html[data-mode] section[data-sorting] > div > div.flex:has(> h3) {
3111
+ display: flex !important;
3112
+ align-items: center !important;
3113
+ min-height: 36px !important;
3114
+ padding-block: 4px !important;
3115
+ }
3116
+
3117
+ html[data-mode] section[data-sorting] > div > div.flex:has(> h3) > h3 {
3118
+ display: block !important;
3119
+ min-height: 0 !important;
3120
+ padding-block: 0 !important;
3121
+ }
3122
+
3123
+ /* band's breathing room: 38px reserves the handle, 70px reserves handle + "+" */
3124
+ html[data-mode] section[data-sorting] > div > h3,
3125
+ html[data-mode] section[data-sorting] > div > div > h3,
3126
+ html[data-mode] section[data-sorting] > div > div.flex:has(> h3) {
3127
+ padding-inline-end: 38px !important;
3128
+ }
3129
+
3130
+ html[data-mode] section[data-sorting]:has(.absolute.end-14.top-2) > div > h3,
3131
+ html[data-mode] section[data-sorting]:has(.absolute.end-14.top-2) > div > div > h3,
3132
+ html[data-mode] section[data-sorting]:has(.absolute.end-14.top-2) > div > div.flex:has(> h3) {
3133
+ padding-inline-end: 70px !important;
3134
+ }
3135
+
3136
+ /* the help button goes to the end of the row */
3137
+ html[data-mode] section[data-sorting] > div > div.flex:has(> h3) > button {
3138
+ margin-inline-start: auto !important;
3139
+ }
3140
+
3141
+ /* the "+" stops overflowing: 28px centered in the band, flush to the left of the handle */
3142
+ html[data-mode] section[data-sorting] .absolute.end-14.top-2 {
3143
+ top: 4px !important;
3144
+ inset-inline-end: 38px !important;
3145
+ }
3146
+
3147
+ /* all three, same box and same hover */
3148
+ html[data-mode] section[data-sorting] > div > div.flex:has(> h3) > button,
3149
+ html[data-mode] section[data-sorting] .absolute.end-14.top-2,
3150
+ html[data-mode] section[data-sorting="false"] > button[data-sortable-handle] {
3151
+ display: grid !important;
3152
+ place-items: center !important;
3153
+ width: 28px !important;
3154
+ height: 28px !important;
3155
+ min-height: 0 !important;
3156
+ padding: 0 !important;
3157
+ border-radius: 3px !important;
3158
+ background: transparent !important;
3159
+ color: var(--wp-muted) !important;
3160
+ -webkit-transition: background-color var(--wp-fast) ease, color var(--wp-fast) ease;
3161
+ transition: background-color var(--wp-fast) ease, color var(--wp-fast) ease;
3162
+ }
3163
+
3164
+ html[data-mode] section[data-sorting] > div > div.flex:has(> h3) > button:hover,
3165
+ html[data-mode] section[data-sorting] .absolute.end-14.top-2:hover,
3166
+ html[data-mode] section[data-sorting="false"] > button[data-sortable-handle]:hover {
3167
+ background: var(--wp-line-soft) !important;
3168
+ color: var(--wp-text) !important;
3169
+ }
3170
+
3171
+
3172
+ /* ==========================================================================
3173
+ [22] (EMPTY) The block that used to hide the legacy migration fields lived here.
3174
+
3175
+ On 2026-09-08, in the same session it was born, the five fields were REALLY DROPPED:
3176
+ `category`, `tags`, `author`, `image` and `image_alt` on `posts`, and `image` on
3177
+ `areas`. What was blocking the cleanup was the risk of publishing a historical revision after the
3178
+ DROP ("no such column", content.ts:1931). Solved at the root, with a backup taken first:
3179
+ UPDATE revisions SET data = json_remove(data, '$.category','$.tags','$.author',
3180
+ '$.image','$.image_alt') WHERE collection IN ('posts','areas'); -- 9 rows
3181
+ Zero revisions with a legacy key after that, so EmDash's field DELETE ran without
3182
+ leaving an armed bomb behind. Do not resurrect this block: if one of these fields reappears in the
3183
+ form, it's because it came back to the schema, and the fix belongs in the schema.
3184
+ ========================================================================== */