PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / assets / css / desktop-files.css

desktop-files.css in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.10, at assets/css/desktop-files.css

1,236 lines 35.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * OpenStation — Files-on-the-Desktop tile + layer styles.
3 *
4 * The layer is an absolutely-positioned container inside
5 * `#os-area`; tiles inside use absolute positioning
6 * keyed off `(x, y)` coordinates persisted server-side.
7 *
8 * Tiles are intentionally close to the look of `os-icon`
9 * (the plugin-shortcut tiles) so the desktop reads as one
10 * coherent surface — but they live in their own class namespace
11 * (`os-file-tile`) so plugins can theme each rail
12 * independently.
13 *
14 * @since 0.9.0
15 */
16
17 /*
18 * Unified rail (since 0.9.0): plugin shortcuts now ride on the
19 * same files layer as folders / posts / etc — they're a
20 * `'shortcut'` file type. The legacy `.os-icons` DIV
21 * still renders for backwards compatibility (existing tests and
22 * non-files-active code paths), but when a files layer is
23 * mounted as a sibling we hide it so we don't end up with two
24 * rails competing for the same wallpaper.
25 */
26 #os-area:has( > .os-files-layer ) > .os-icons {
27 display: none;
28 }
29
30 /*
31 * Drop-target highlight — the `data-files-drop-active` attribute
32 * is set on the FilesLayer host (desktop area or folder window
33 * body) while a shortcut payload is being dragged over it.
34 *
35 * A quiet inner glow only. This used to be a dashed, animated
36 * marching-ants frame around the whole wallpaper, and on a full
37 * desktop that read as a large moving grid behind the tile in hand
38 * — loud, and design asked for it to go. The ghost's accept /
39 * reject chip and the per-cell drop preview already say where the
40 * tile will land; the glow just confirms the surface is listening.
41 */
42 [ data-files-drop-active ] {
43 position: relative;
44 box-shadow: inset 0 0 32px 4px rgba( 34, 113, 177, 0.12 );
45 transition: box-shadow 0.18s ease-out;
46 }
47
48 .os-files-layer {
49 position: absolute;
50 inset: 0;
51 z-index: 0;
52 /*
53 * Layer doesn't intercept pointer events itself — tiles and
54 * the wallpaper own that. Phase 4's wallpaper context menu
55 * binds clicks on the parent `#os-area`, which is
56 * unblocked because of this rule.
57 */
58 pointer-events: none;
59 }
60
61 /*
62 * Live drop-cell preview. A soft outline that hovers at the cell
63 * the dropped tile will land in, so the user can predict where the
64 * grid snap will place it before releasing. Positioned absolutely
65 * inside the files layer; `translate3d` updated from JS on every
66 * pointermove while a desktop-file drag hovers the canvas.
67 *
68 * The tile dimensions are baked into `grid.ts` (88 × 96 visual,
69 * 96 × 110 cell pitch). The outline matches the cell pitch — slightly
70 * larger than the tile — so the highlight reads as "this slot" rather
71 * than "this tile."
72 *
73 * @since 0.20.0
74 */
75 .os-files-drop-preview {
76 position: absolute;
77 top: 0;
78 left: 0;
79 width: 88px;
80 height: 96px;
81 pointer-events: none;
82 box-sizing: border-box;
83 border-radius: 12px;
84 background: var(
85 --os-drop-preview-bg,
86 rgba( 34, 113, 177, 0.08 )
87 );
88 border: 2px dashed
89 var(
90 --os-drop-preview-border,
91 rgba( 34, 113, 177, 0.55 )
92 );
93 transition: transform 90ms ease-out;
94 will-change: transform;
95 /* Sit BELOW tiles so a tile being dragged over the cell isn't
96 * visually clipped by the dashed outline; the outline still reads
97 * because the tile is semi-transparent during drag (`--dragging`
98 * sets `opacity: 0.4`). */
99 z-index: 0;
100 }
101
102 @media ( prefers-reduced-motion: reduce ) {
103 .os-files-drop-preview {
104 transition: none;
105 }
106 }
107
108 .os-file-tile {
109 pointer-events: auto;
110 position: absolute;
111 display: flex;
112 flex-direction: column;
113 align-items: center;
114 justify-content: flex-start;
115 gap: 6px;
116 /*
117 * `box-sizing` is load-bearing, not housekeeping. Without it the
118 * horizontal padding is ADDED to the declared width, so an 88px
119 * tile occupies 96px — which was exactly the desktop's old cell
120 * pitch, and why icons sat edge to edge with no gap at all while
121 * the maths insisted there were 8px between them.
122 *
123 * The tile is the size the grid says it is. Padding lives inside.
124 */
125 box-sizing: border-box;
126 width: var( --os-tile-w, 88px );
127 /*
128 * Fixed, not minimum. The tile box IS the selection ring, and a
129 * box that grows with its label gives a row of selected icons a
130 * ragged top edge — one height per label that happened to wrap
131 * to two lines. Every tile occupies its cell; short labels leave
132 * the slack inside the ring, where it reads as padding.
133 */
134 height: var( --os-tile-h, 104px );
135 padding: 8px 4px;
136 border: 0;
137 background: transparent;
138 /* Tile foreground driven by `--os-tile-fg` so any
139 * light-on-dark surface (folder window, My WordPress preview)
140 * can retint tiles by overriding the variable in its scope —
141 * no per-surface selectors needed here. */
142 color: var( --os-tile-fg, var( --os-fg, #fff ) );
143 cursor: pointer;
144 border-radius: 8px;
145 transition: background-color 0.15s ease, transform 0.05s linear;
146 -webkit-user-select: none;
147 user-select: none;
148 touch-action: none;
149 /* Soft fade-in on mount — each tile carries its own randomised
150 * `--enter-delay` / `--enter-duration` (set inline by the
151 * tile builder), so a freshly-painted grid reads as a cascade
152 * rather than a synchronised pop-in. Both values land in the
153 * decimal-of-a-second range — perceivable, never sluggish.
154 *
155 * `backwards` keeps the tile invisible during its delay so the
156 * cascade is honest; the animation runs once, leaves the tile
157 * at its static `opacity: 1`, and never re-fires from later
158 * state changes (drag, hover, drop-target).
159 */
160 animation: os-file-tile-enter
161 var( --os-file-tile-enter-duration, 0.4s )
162 ease-out
163 var( --os-file-tile-enter-delay, 0s )
164 backwards;
165 }
166
167 @keyframes os-file-tile-enter {
168 from {
169 opacity: 0;
170 }
171 to {
172 opacity: 1;
173 }
174 }
175
176 @media ( prefers-reduced-motion: reduce ) {
177 .os-file-tile {
178 animation: none;
179 }
180 }
181
182 .os-file-tile:hover,
183 .os-file-tile:focus-visible {
184 background: var(
185 --os-tile-hover-bg,
186 rgba( 255, 255, 255, 0.12 )
187 );
188 outline: none;
189 }
190
191 .os-file-tile:focus-visible {
192 /* Inset focus ring — outset rings extended past the tile and
193 * visually overlapped the colindant icon when the canvas cell
194 * pitch is tight (96 px cell, 88 px tile → only 4 px clearance
195 * per side). Inset keeps the ring fully inside the tile. */
196 box-shadow: inset 0 0 0 2px
197 var( --os-tile-focus-ring, var( --wp-admin-theme-color, #2271b1 ) );
198 }
199
200 /* Status ribbon sizing inside tiles. The `<os-ribbon>` defaults
201 * are tuned for cards (~90 px clipping window); 88 px-wide tiles
202 * need a tighter footprint or the ribbon dominates the icon. We
203 * override the component's CSS custom-property theming surface so
204 * the ribbon stays a small accent in the corner. Authors who want
205 * different sizing can override per-tile or per-section. */
206 .os-file-tile > os-ribbon {
207 --os-ui-ribbon-size: 48px;
208 --os-ui-ribbon-banner-width: 72px;
209 --os-ui-ribbon-banner-offset: 12px;
210 --os-ui-ribbon-banner-pull: -18px;
211 --os-ui-ribbon-padding: 1px 0;
212 --os-ui-ribbon-font: 700 7px/1.4 var( --os-font, system-ui );
213 --os-ui-ribbon-tracking: 0.05em;
214 --os-ui-ribbon-shadow: 0 1px 2px rgba( 0, 0, 0, 0.2 );
215 }
216
217 .os-file-tile--dragging {
218 /* The DragManager renders a separate ghost element that follows
219 * the pointer; the SOURCE tile stays in place but is dimmed so
220 * the user has a stable reference for "this is what I'm
221 * dragging."
222 *
223 * @since 0.18.0
224 */
225 opacity: 0.4;
226 cursor: grabbing;
227 }
228
229 /* Pinned tiles ("My WordPress", Recycle Bin) anchor to a fixed slot
230 * and DO NOT wire drag — but they look + behave identically to any
231 * other tile until the user attempts a drag (which silently fails).
232 * No upfront visual cue: feedback now happens at the moment of the
233 * gesture, not before, per @since 0.9.0 design feedback.
234 *
235 * Default `cursor: pointer` from the base `.os-file-tile`
236 * rule covers click-to-open. Touch interactions stay as
237 * `touch-action: auto` so the OS scroll gesture works on top of
238 * the tile.
239 */
240 .os-file-tile--pinned {
241 touch-action: auto;
242 }
243
244 /* Folder tile is the drop target during a cross-window shortcut
245 * drag — the user dragged a post / page / user tile out of My
246 * WordPress and is hovering over a folder. Bright outline, soft
247 * pulse, and a slight scale so the folder reads unmistakably as
248 * "yes, drop here." @since 0.8.0; pulse added 0.20.0. */
249 @keyframes os-drop-target-pulse {
250 0% {
251 box-shadow:
252 0 0 0 0 rgba( 34, 113, 177, 0.5 ),
253 inset 0 0 0 2px var( --wp-admin-theme-color, #2271b1 );
254 }
255 100% {
256 box-shadow:
257 0 0 0 8px rgba( 34, 113, 177, 0 ),
258 inset 0 0 0 2px var( --wp-admin-theme-color, #2271b1 );
259 }
260 }
261
262 .os-file-tile--drop-target {
263 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
264 outline-offset: -4px;
265 background: rgba( 34, 113, 177, 0.2 );
266 transform: scale( 1.08 );
267 z-index: 5;
268 animation: os-drop-target-pulse 1s ease-out infinite;
269 border-radius: 12px;
270 }
271
272 @media ( prefers-reduced-motion: reduce ) {
273 .os-file-tile--drop-target {
274 animation: none;
275 }
276 }
277
278 /* Drop-hint chip — floats next to the cursor during a drag and
279 * shows context-aware text ("Drop here to create shortcut",
280 * "Can't drop here", etc.). The chip text is owned by the
281 * ghost module (`src/drag/ghost.ts`); these styles control the
282 * appearance per accept/reject/neutral mode.
283 *
284 * @since 0.20.0
285 */
286 .os-drag-hint {
287 position: fixed;
288 top: 0;
289 left: 0;
290 padding: 6px 10px;
291 border-radius: 999px;
292 font-size: 12px;
293 font-weight: 600;
294 letter-spacing: 0.02em;
295 white-space: nowrap;
296 pointer-events: none;
297 user-select: none;
298 box-shadow: 0 4px 14px rgba( 0, 0, 0, 0.22 );
299 background: rgba( 30, 30, 30, 0.85 );
300 color: var( --os-ui-fg-on-accent, #fff );
301 z-index: 2147483647;
302 transition:
303 background-color 0.12s ease,
304 color 0.12s ease,
305 transform 0.06s ease-out;
306 }
307
308 .os-drag-hint--neutral {
309 background: rgba( 30, 30, 30, 0.85 );
310 color: var( --os-ui-fg-on-accent, #fff );
311 }
312
313 .os-drag-hint--accept {
314 background: #1e8449;
315 color: var( --os-ui-fg-on-accent, #fff );
316 box-shadow:
317 0 0 0 2px rgba( 30, 132, 73, 0.25 ),
318 0 6px 18px rgba( 30, 132, 73, 0.3 );
319 }
320
321 .os-drag-hint--accept::before {
322 /* Tiny checkmark chevron prefix so "Drop here" reads as an
323 * actionable affirmation rather than a passive label. The
324 * checkmark is a real character (U+2713) so screen readers
325 * skip past it via the `aria-hidden` on the host chip. */
326 content: '✓ ';
327 }
328
329 .os-drag-hint--reject {
330 background: var( --os-ui-danger-hover, #b32d2e );
331 color: var( --os-ui-fg-on-accent, #fff );
332 box-shadow:
333 0 0 0 2px rgba( 179, 45, 46, 0.25 ),
334 0 6px 18px rgba( 179, 45, 46, 0.3 );
335 }
336
337 .os-drag-hint--reject::before {
338 content: '⊘ ';
339 }
340
341 /*
342 * No body-level cursor swap during drag — the user prefers the
343 * default arrow cursor regardless of state, so the accept/reject
344 * feedback lives entirely in the hint chip + outline animation.
345 * Don't reintroduce a `cursor: grabbing/copy/no-drop` here.
346 */
347
348 /* Shortcut overlay — small arrow badge in the BOTTOM-RIGHT corner
349 * of the icon visual on any tile that's a reference to an external
350 * entity (post, attachment, comment, user, term — anything that
351 * ISN'T a folder or a plugin shortcut). Mirrors the macOS / Windows
352 * convention of an arrow decoration on alias/shortcut icons.
353 *
354 * The geometry: tile is 88px wide with `padding: 8px 4px`. The icon
355 * visual is 48×48 centered horizontally, so the icon's bottom-right
356 * corner lands at roughly (68, 56) from the tile's top-left. The
357 * 18×18 arrow circle is anchored just inside that corner so it
358 * reads as part of the icon, not floating off in space.
359 *
360 * @since 0.8.0
361 */
362 /* Only real placements get the badge — i.e. tiles built by
363 * `buildTile(placement)` on the desktop / folder windows, which
364 * are the only surface where "this is a shortcut" actually means
365 * something. Tiles in My WordPress grids (and any future content
366 * surface) are the entity, not a shortcut to it, so the arrow
367 * would just be noise. `[data-placement-id]` is the canonical
368 * placement marker. */
369 .os-file-tile[ data-placement-id ][ data-file-type="post" ]::after,
370 .os-file-tile[ data-placement-id ][ data-file-type="attachment" ]::after,
371 .os-file-tile[ data-placement-id ][ data-file-type="comment" ]::after,
372 .os-file-tile[ data-placement-id ][ data-file-type="user" ]::after,
373 .os-file-tile[ data-placement-id ][ data-file-type="term" ]::after {
374 content: "↗";
375 position: absolute;
376 right: 14px;
377 top: 44px;
378 width: 18px;
379 height: 18px;
380 border-radius: 50%;
381 background: var( --os-tile-shortcut-bg, rgba( 0, 0, 0, 0.65 ) );
382 color: var( --os-tile-shortcut-fg, #fff );
383 font-size: 11px;
384 line-height: 18px;
385 text-align: center;
386 box-shadow: var(
387 --os-tile-shortcut-shadow,
388 0 1px 3px rgba( 0, 0, 0, 0.5 )
389 );
390 pointer-events: none;
391 }
392
393 /*
394 * Missing files (the underlying entity was deleted but the
395 * placement still exists) render the tile dimmed so the user
396 * can right-click → Remove without confusing it for an active
397 * file. Phase 6's per-placement permission re-check uses the
398 * same affordance for "you can't see this" placeholders.
399 */
400 .os-file-tile--missing {
401 opacity: 0.5;
402 }
403
404 /**
405 * Access-gated tile — the recipient sees the icon (the owner
406 * shared the folder containing it) but lacks `can_read` on the
407 * underlying entity. Dim the tile, kill the pointer affordance,
408 * and overlay a lock badge so the state reads at a glance.
409 */
410 .os-file-tile--access-gated {
411 opacity: 0.65;
412 cursor: not-allowed;
413 }
414 .os-file-tile--access-gated .os-file-tile__visual,
415 .os-file-tile--access-gated .os-file-tile__preview {
416 filter: grayscale( 0.6 );
417 }
418 .os-file-tile--access-gated:hover,
419 .os-file-tile--access-gated:focus-visible {
420 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) );
421 }
422 .os-file-tile__lock {
423 position: absolute;
424 top: 4px;
425 inset-inline-end: 4px;
426 width: 20px;
427 height: 20px;
428 border-radius: 50%;
429 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.7 ) );
430 color: var( --os-ui-fg-on-accent, #fff );
431 font-size: 14px;
432 line-height: 20px;
433 text-align: center;
434 pointer-events: none;
435 box-shadow: 0 1px 3px rgba( 0, 0, 0, 0.5 );
436 }
437
438 /**
439 * Preview-pane empty state when a recipient selects a tile they
440 * can't open. Big lock ring + clear copy + secondary hint. Designed
441 * to read at a glance in the otherwise-busy preview column.
442 */
443 .os-files__access-gated {
444 display: flex;
445 flex-direction: column;
446 align-items: center;
447 justify-content: center;
448 gap: 16px;
449 padding: 32px 24px;
450 min-height: 240px;
451 text-align: center;
452 color: var( --os-fg, inherit );
453 }
454 .os-files__access-gated-ring {
455 display: flex;
456 align-items: center;
457 justify-content: center;
458 width: 96px;
459 height: 96px;
460 border-radius: 50%;
461 background: linear-gradient(
462 145deg,
463 rgba( 214, 54, 56, 0.22 ),
464 rgba( 214, 54, 56, 0.08 )
465 );
466 border: 2px solid rgba( 214, 54, 56, 0.45 );
467 box-shadow:
468 inset 0 1px 0 rgba( 255, 255, 255, 0.08 ),
469 0 8px 24px rgba( 214, 54, 56, 0.18 );
470 }
471 .os-files__access-gated-glyph {
472 font-size: 44px;
473 width: 44px;
474 height: 44px;
475 color: #ff8080;
476 filter: drop-shadow( 0 1px 2px rgba( 0, 0, 0, 0.4 ) );
477 }
478 .os-files__access-gated-title {
479 margin: 0;
480 font-size: 17px;
481 font-weight: 600;
482 letter-spacing: 0.2px;
483 }
484 .os-files__access-gated-sub {
485 margin: 0;
486 font-size: 13px;
487 line-height: 1.5;
488 max-width: 340px;
489 opacity: 0.85;
490 }
491 .os-files__access-gated-hint {
492 margin: 0;
493 font-size: 12px;
494 line-height: 1.5;
495 max-width: 340px;
496 opacity: 0.6;
497 }
498
499 .os-file-tile__visual {
500 display: inline-flex;
501 align-items: center;
502 justify-content: center;
503 width: 48px;
504 height: 48px;
505 font-size: 32px;
506 line-height: 1;
507 }
508
509 .os-file-tile__icon {
510 display: inline-flex;
511 align-items: center;
512 justify-content: center;
513 width: 48px;
514 height: 48px;
515 font-size: 32px;
516 line-height: 1;
517 }
518
519 .os-file-tile__preview {
520 width: 48px;
521 height: 48px;
522 object-fit: cover;
523 border-radius: 4px;
524 }
525
526 /* `<img>` defaults to `draggable=true`, which lets the browser
527 * start a native HTML5 image-drag from a pointerdown — that
528 * pre-empts the DragManager's pointer-event-driven tile rearrange
529 * and the tile silently refuses to move. The renderer also sets
530 * `draggable="false"` on every `<img>` it produces; this CSS rule
531 * covers anything injected later (plugin filters, decorators)
532 * without having to remember the attribute. */
533 .os-file-tile img {
534 -webkit-user-drag: none;
535 user-select: none;
536 }
537
538 /*
539 * Wallpaper + tile context menus use `<os-context-menu>` and
540 * `<os-context-menu-option>` (Phase 4 / 0.9.0). Their host-level
541 * styles live inside the component shadow DOM — see
542 * `src/ui/components/os-context-menu/os-context-menu.styles.ts`.
543 * No project CSS rules needed here; the legacy `.os-
544 * wallpaper-menu*` rules were removed when the components landed.
545 */
546
547 /*
548 * Folder native window — the body of `os-folder-<id>`
549 * contains a `FilesLayer` rendering that folder's contents.
550 * We ensure the body has positioning context so absolutely-
551 * positioned tiles inside the layer anchor correctly.
552 */
553 .desktop-mode-folder-window {
554 position: relative;
555 min-height: 200px;
556 /* Same window background recipe as My WordPress — defaults to
557 * the standard window bg (#fff) and themable per-window. */
558 background: var(
559 --os-folder-window-bg,
560 var( --os-window-bg, #fff )
561 );
562 color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
563 display: flex;
564 flex-direction: column;
565 /* Light-on-dark context: the folder window opens inside a
566 * standard window chrome whose body is white-ish. Retint the
567 * tile color tokens here so dashicons + labels read against
568 * a light background. Plugins / themes can override these
569 * same variables at any scope to retint further. */
570 --os-tile-fg: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
571 /* Chains through the palette for the same reason as
572 * `--os-tile-fg` beside it: a bare literal here outranks the
573 * palette's own value and pins every tile's secondary line to
574 * near-black, which disappears the moment the surface is dark.
575 * The literal stays as the pre-brand floor. */
576 --os-tile-fg-muted: var(
577 --os-folder-window-fg-muted,
578 var( --os-ui-fg-muted, rgba( 0, 0, 0, 0.55 ) )
579 );
580 --os-tile-hover-bg: rgba( 0, 0, 0, 0.06 );
581 /* Light-context label rendering — share the same recipe with
582 * every other light-bg tile surface (My WordPress, future
583 * windows). One set of tokens, no per-surface duplication. */
584 --os-tile-label-shadow: none;
585 --os-tile-label-weight: 500;
586 --os-tile-label-smoothing: antialiased;
587 --os-tile-label-color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
588 }
589
590 /* Folder window's bottom status bar — paint with the same light-
591 * context treatment My WordPress uses (transparent bg, soft border,
592 * muted dark text). Without this, the wallpaper-targeted defaults
593 * (`background: rgba(0,0,0,0.18); color: white`) would show as a
594 * dark band against the white window bg. */
595 .desktop-mode-folder-window .os-folder-status-bar {
596 border-top: 1px solid var( --os-ui-border, #dcdcde );
597 background: transparent;
598 color: var( --os-ui-fg-muted, #50575e );
599 }
600
601 .desktop-mode-folder-window
602 .os-folder-status-bar
603 button.os-folder-status-bar__segment:hover {
604 background: var( --os-hover, var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) ) );
605 }
606
607 /* Generic breadcrumb header — any surface that grows a navigation
608 * stack (folder window, My WordPress folder, future drill-down
609 * windows) renders one of these via `renderBreadcrumbs` from
610 * `src/desktop-files/breadcrumbs.ts`. Visual vocabulary: small
611 * icon-only Back button on the left, `›`-separated crumb buttons,
612 * current segment is bold + non-interactive.
613 *
614 * @since 0.8.0
615 */
616 .os-breadcrumbs {
617 display: flex;
618 align-items: center;
619 gap: 8px;
620 padding: 6px 10px;
621 border-bottom: 1px solid var( --os-ui-border, #dcdcde );
622 background: transparent;
623 flex: 0 0 auto;
624 }
625
626 .os-breadcrumbs__back {
627 display: inline-flex;
628 align-items: center;
629 justify-content: center;
630 width: 26px;
631 height: 26px;
632 padding: 0;
633 border: 0;
634 background: transparent;
635 border-radius: 4px;
636 cursor: pointer;
637 color: inherit;
638 flex: 0 0 auto;
639 }
640
641 .os-breadcrumbs__back:hover:not(:disabled),
642 .os-breadcrumbs__back:focus-visible:not(:disabled) {
643 background: var( --os-hover, var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) ) );
644 outline: none;
645 }
646
647 .os-breadcrumbs__back:disabled {
648 opacity: 0.35;
649 cursor: default;
650 }
651
652 .os-breadcrumbs__back .dashicons {
653 font-size: 18px;
654 width: 18px;
655 height: 18px;
656 line-height: 1;
657 }
658
659 .os-breadcrumbs__crumbs {
660 display: flex;
661 align-items: center;
662 gap: 6px;
663 font-size: 13px;
664 min-width: 0;
665 overflow: hidden;
666 text-overflow: ellipsis;
667 white-space: nowrap;
668 }
669
670 .os-breadcrumbs__crumb {
671 border: 0;
672 background: transparent;
673 color: var( --os-link, var( --os-ui-accent, #2271b1 ) );
674 cursor: pointer;
675 padding: 2px 4px;
676 border-radius: 4px;
677 font: inherit;
678 }
679
680 .os-breadcrumbs__crumb:hover {
681 background: var( --os-hover, var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) ) );
682 }
683
684 .os-breadcrumbs__crumb--current {
685 color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
686 cursor: default;
687 font-weight: 600;
688 }
689
690 .os-breadcrumbs__crumb--current:hover {
691 background: transparent;
692 }
693
694 .os-breadcrumbs__sep {
695 color: var( --os-ui-fg-muted, #787c82 );
696 }
697
698 .os-folder-window__layer {
699 position: relative;
700 flex: 1;
701 min-height: 0;
702 overflow: auto;
703 }
704
705 /* Two-pane shell — left: tile canvas, right: preview pane that
706 * reflects the currently-selected tile. Same model as the My
707 * WordPress two-pane view so the UX is unified.
708 *
709 * @since 0.8.0 */
710 .os-folder-window__split {
711 display: grid;
712 grid-template-columns: minmax( 240px, 60% ) minmax( 0, 1fr );
713 flex: 1 1 auto;
714 min-height: 0;
715 }
716
717 .os-folder-window__preview {
718 overflow-y: auto;
719 min-width: 0;
720 background: var( --os-window-bg, #fff );
721 border-inline-start: 1px solid var( --os-ui-border, #dcdcde );
722 color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
723 }
724
725 /* Selected file tile — a fill, and an inset accent ring whose width
726 * the palette decides (0 on OpenStation's, which lifts the tile with
727 * the fill alone; 1px on Legacy). Variable-driven so plugins can
728 * retune. */
729 .os-file-tile--selected,
730 .os-file-tile--selected:hover {
731 background: var(
732 --os-tile-selected-bg,
733 rgba( 34, 113, 177, 0.18 )
734 );
735 box-shadow: inset 0 0 0 var( --os-tile-selected-ring-width, 1px )
736 var( --os-tile-focus-ring, var( --wp-admin-theme-color, #2271b1 ) );
737 }
738
739 /* Inside a folder window the selected highlight should read on
740 * white. */
741 .desktop-mode-folder-window {
742 --os-tile-selected-bg: rgba( 34, 113, 177, 0.12 );
743 }
744
745 /* Type breakdown under a multi-selection's count in a folder
746 * window's preview pane. The count is the headline; this is the
747 * detail that tells you which actions the menu will offer. */
748 .os-files-preview__selection-breakdown {
749 font-size: 12px;
750 opacity: 0.75;
751 }
752
753 /* While a rubber band is live, nothing in the shell is selectable.
754 *
755 * The band starts on bare canvas, which — unlike a tile — IS
756 * selectable, so the browser begins its own text selection on the
757 * same press and keeps extending it as the pointer travels. Inside
758 * the canvas that stays invisible; drag out over another window and
759 * it starts highlighting that window's text in blue, mid-gesture.
760 *
761 * The controller also refuses `selectstart` outright while the band
762 * is up. This rule is the second half: it covers anything already
763 * selectable that the event route misses. */
764 body[ data-os-marquee ] {
765 -webkit-user-select: none;
766 user-select: none;
767 }
768
769 /* Marquee (rubber-band) selection box. Painted by the shared
770 * selection controller on any canvas that opts into it — the
771 * wallpaper, folder windows, every My WordPress list — so this one
772 * declaration dresses all of them.
773 *
774 * `pointer-events: none` is load-bearing: the box tracks under the
775 * cursor for the whole gesture, and without it every hit-test
776 * (drop targets, the controller's own tile lookup) would land on
777 * the box instead of what's beneath it. */
778 .os-selection-marquee {
779 position: absolute;
780 z-index: 5;
781 pointer-events: none;
782 border: 1px solid
783 var( --os-tile-focus-ring, var( --wp-admin-theme-color, #2271b1 ) );
784 border-radius: 2px;
785 /* A WASH, not a fill — the whole point of a rubber band is that
786 * you can see what it is about to catch. `--os-ui-accent-dim` is a
787 * solid hex (Pulse, one step back), so reaching for it directly
788 * paints an opaque rectangle over the icons; `--os-ui-accent-soft`
789 * is that same colour already taken down to a 14% wash, which is
790 * what every other ambient accent surface in the shell uses. */
791 background: var(
792 --os-selection-marquee-bg,
793 var( --os-ui-accent-soft, rgba( 34, 113, 177, 0.14 ) )
794 );
795 }
796
797 .os-folder-status-bar {
798 display: flex;
799 justify-content: space-between;
800 align-items: center;
801 gap: 12px;
802 padding: 6px 12px;
803 border-top: 1px solid rgba( 255, 255, 255, 0.08 );
804 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.18 ) );
805 color: var( --os-fg-muted, rgba( 255, 255, 255, 0.7 ) );
806 font-size: 12px;
807 flex-shrink: 0;
808 }
809
810 .os-folder-status-bar__cluster {
811 display: flex;
812 align-items: center;
813 gap: 12px;
814 }
815
816 .os-folder-status-bar__segment {
817 display: inline-flex;
818 align-items: center;
819 gap: 4px;
820 padding: 0;
821 border: 0;
822 background: transparent;
823 color: inherit;
824 font: inherit;
825 }
826
827 button.os-folder-status-bar__segment {
828 cursor: pointer;
829 border-radius: 4px;
830 padding: 2px 6px;
831 }
832
833 button.os-folder-status-bar__segment:hover {
834 background: rgba( 255, 255, 255, 0.1 );
835 }
836
837 .os-folder-status-bar__icon {
838 font-size: 14px;
839 }
840
841 /*
842 * File Associations OS Settings tab (Phase 5). One row per file
843 * type: label on the left, a `<os-select>` of compatible
844 * openers on the right. Color tokens use the OS-Settings-canonical
845 * `--os-ui-fg-muted` so the tab inherits theme colors instead of
846 * falling back to white-on-light invisibility.
847 */
848 .os-file-associations__intro {
849 display: block;
850 margin: 0 0 16px;
851 color: var( --os-ui-fg-muted, #50575e );
852 font-size: 13px;
853 line-height: 1.5;
854 }
855
856 .os-file-associations__list {
857 display: flex;
858 flex-direction: column;
859 gap: 8px;
860 }
861
862 .os-file-associations__row {
863 display: flex;
864 align-items: center;
865 justify-content: space-between;
866 gap: 16px;
867 padding: 8px 12px;
868 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.04 ) );
869 border-radius: 6px;
870 }
871
872 .os-file-associations__label {
873 font-weight: 500;
874 flex: 1;
875 }
876
877 .os-file-associations__select {
878 min-width: 220px;
879 }
880
881 .os-file-associations__none,
882 .os-file-associations__empty {
883 color: var( --os-ui-fg-muted, #50575e );
884 font-size: 12px;
885 }
886
887 /*
888 * The shell's two built-in modals — "New folder" / "Rename", and the
889 * web-link dialog, which reuses this class set for its surface. Both
890 * are light-DOM overlays that slot `<os-text-field>` +
891 * `<os-button>` for their controls.
892 *
893 * The surface reads the same `--os-ui-modal-*` family that
894 * `<os-modal>` and `<os-confirm-dialog>` do, so the three look like
895 * one dialog system and answer to a desktop theme together. Two
896 * things it must NOT do, both of which it used to:
897 *
898 * - Paint from `--os-bg`. That is the WALLPAPER token: the desk
899 * default is a gradient and the wallpaper layer overwrites it
900 * with whatever artwork is active. A dialog is a surface, not a
901 * desk. Same note as `os-modal.styles.ts`.
902 * - Style raw form controls. Core's `forms.css` reaches every
903 * `<input>` in the parent shell through `input[type="text"]` —
904 * (0,1,1), which outranks a single class of ours — so a plain
905 * input rendered as a white core-chrome box on this dark
906 * surface no matter what we declared. The controls live in
907 * shadow DOM now, where that sheet cannot follow.
908 */
909 .os-create-folder-dialog__overlay {
910 position: fixed;
911 inset: 0;
912 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.45 ) );
913 /* Desktop-theme texture slot: unset resolves to none. Mirrors
914 the scrim treatment in os-modal. */
915 background-image: var( --os-ui-scrim-image, none );
916 background-repeat: var( --os-ui-scrim-image-repeat, repeat );
917 background-size: var( --os-ui-scrim-image-size, auto );
918 background-position: var( --os-ui-scrim-image-position, center );
919 backdrop-filter: blur( 2px );
920 display: flex;
921 align-items: center;
922 justify-content: center;
923 z-index: 10000;
924 }
925
926 .os-create-folder-dialog {
927 /*
928 * Re-point the shared control tokens for a dark dialog surface,
929 * exactly as `<os-modal>` does on its host. The slotted
930 * `<os-text-field>` / `<os-button>` inherit these across the
931 * shadow boundary, so the field resolves a dark input with light
932 * ink instead of the light-admin defaults (`--os-window-bg` is
933 * `#fff` in an unthemed shell, which under a light `--os-ui-fg`
934 * is the invisible-value trap the modal styles call out).
935 *
936 * Each reads a palette-owned `--os-ui-modal-*` name first, so a
937 * desktop theme can still reach every one of them; the literals
938 * are only the floor for a shell whose stylesheet never loaded.
939 */
940 --os-ui-fg: var( --os-ui-modal-text, #f0f0f1 );
941 --os-ui-fg-muted: var( --os-ui-modal-text-muted, #a7aaad );
942 --os-ui-border: var( --os-ui-modal-border, rgba( 255, 255, 255, 0.25 ) );
943 --os-window-bg: var( --os-ui-modal-field-bg, #2c3338 );
944 --os-ui-button-bg-hover: var(
945 --os-ui-modal-button-bg-hover,
946 rgba( 255, 255, 255, 0.08 )
947 );
948
949 width: min( 420px, 92vw );
950 /* Longhand, and a literal fallback: the shorthand would reset the
951 texture slot below, and a gradient is invalid as a
952 background-color — it would leave the dialog transparent. */
953 background-color: var( --os-ui-modal-bg, #1d2327 );
954 background-image: var( --os-ui-dialog-bg-image, none );
955 background-repeat: var( --os-ui-dialog-bg-image-repeat, repeat );
956 background-size: var( --os-ui-dialog-bg-image-size, auto );
957 background-position: var( --os-ui-dialog-bg-image-position, center );
958 color: var( --os-ui-modal-fg, var( --os-fg, #fff ) );
959 border: 1px solid var( --os-ui-border, rgba( 255, 255, 255, 0.08 ) );
960 border-radius: 10px;
961 box-shadow: 0 20px 50px rgba( 0, 0, 0, 0.6 );
962 padding: 20px 22px 18px;
963 display: flex;
964 flex-direction: column;
965 gap: 10px;
966 }
967
968 .os-create-folder-dialog__title {
969 margin: 0 0 4px;
970 font-size: 16px;
971 font-weight: 600;
972 color: var( --os-ui-fg, #f0f0f1 );
973 }
974
975 .os-url-dialog__description {
976 margin: 0 0 4px;
977 font-size: 12px;
978 line-height: 1.5;
979 color: var( --os-ui-fg-muted, #a7aaad );
980 }
981
982 .os-create-folder-dialog__error {
983 margin: 0;
984 color: var( --os-ui-danger-hover, #ff8a8a );
985 font-size: 12px;
986 }
987
988 .os-create-folder-dialog__actions {
989 display: flex;
990 justify-content: flex-end;
991 gap: 8px;
992 margin-top: 6px;
993 }
994
995 /* The busy state dims the whole form area rather than the input
996 alone — the buttons carry their own disabled treatment from
997 os-button, and dimming one control of three read as a glitch. */
998 .os-create-folder-dialog--busy .os-create-folder-dialog__field {
999 opacity: 0.7;
1000 }
1001
1002 /*
1003 * Single label rule for every tile, regardless of host surface.
1004 * Visual properties read from `--os-tile-label-*`
1005 * tokens — defined in `variables.css` for the dark wallpaper and
1006 * rebound by any light-context scope (folder window, My
1007 * WordPress, …) so all surfaces share one source of truth.
1008 */
1009 .os-file-tile__label {
1010 font-size: 12px;
1011 line-height: 1.2;
1012 /* Follows the tile rather than restating its width, so a section
1013 * that re-points `--os-tile-w` (image-led sections do) gets a
1014 * label that grows with the tile instead of staying narrow. */
1015 max-width: calc( var( --os-tile-w, 88px ) - 4px );
1016 text-align: center;
1017 overflow: hidden;
1018 text-overflow: ellipsis;
1019 display: -webkit-box;
1020 -webkit-line-clamp: 2;
1021 -webkit-box-orient: vertical;
1022 color: var( --os-tile-label-color, var( --os-tile-fg, #fff ) );
1023 font-weight: var( --os-tile-label-weight, 400 );
1024 text-shadow: var( --os-tile-label-shadow, 0 1px 2px rgba( 0, 0, 0, 0.5 ) );
1025 -webkit-font-smoothing: var( --os-tile-label-smoothing, auto );
1026 -moz-osx-font-smoothing: grayscale;
1027 }
1028
1029 /*
1030 * DragManager ghost element (the visual that follows the cursor
1031 * during a drag). Cloned from the source by `mountGhost()` and
1032 * pinned to the body with `position: fixed` + transform-based
1033 * movement. `pointer-events: none` ensures the ghost itself
1034 * never obscures the drop targets underneath during hit-testing.
1035 *
1036 * The accept / reject classes are toggled on each hover transition
1037 * so the cursor matches the drop semantics: `copy` over an
1038 * accepting target, `no-drop` over rejecting (or no) target.
1039 *
1040 * @since 0.18.0
1041 */
1042 .os-drag-ghost {
1043 box-shadow: 0 6px 24px rgba( 0, 0, 0, 0.45 );
1044 border-radius: 6px;
1045 opacity: 0.95;
1046 transition: none;
1047 }
1048
1049 .os-drag-ghost--accept {
1050 cursor: copy;
1051 }
1052
1053 .os-drag-ghost--reject {
1054 cursor: no-drop;
1055 }
1056
1057 /* Multi-item drag ghost: the grabbed tile, with the rest of the set
1058 * implied by two offset cards behind it and stated by a count badge.
1059 * The stack is drawn with pseudo-elements rather than real clones —
1060 * three cloned tiles cost three subtree copies per lift, and only
1061 * the top one is ever legible. */
1062 .os-drag-stack {
1063 /* The drag manager overwrites this with `position: fixed` when it
1064 * mounts the ghost. It stays declared so the stack still composes
1065 * correctly if the helper is used outside a live drag — both
1066 * values establish the containing block the cards and the badge
1067 * are placed against. */
1068 position: relative;
1069 }
1070
1071 .os-drag-stack::before,
1072 .os-drag-stack::after {
1073 content: '';
1074 position: absolute;
1075 inset: 0;
1076 z-index: -1;
1077 border-radius: 6px;
1078 background: var( --os-ui-surface, rgba( 30, 30, 40, 0.9 ) );
1079 box-shadow: 0 4px 14px rgba( 0, 0, 0, 0.35 );
1080 }
1081
1082 .os-drag-stack::before {
1083 transform: translate( 6px, 6px );
1084 opacity: 0.7;
1085 }
1086
1087 .os-drag-stack::after {
1088 transform: translate( 12px, 12px );
1089 opacity: 0.45;
1090 }
1091
1092 .os-drag-stack__count {
1093 position: absolute;
1094 inset-block-start: -8px;
1095 inset-inline-end: -8px;
1096 z-index: 1;
1097 min-width: 20px;
1098 height: 20px;
1099 padding: 0 6px;
1100 box-sizing: border-box;
1101 display: flex;
1102 align-items: center;
1103 justify-content: center;
1104 border-radius: 10px;
1105 background: var(
1106 --os-ui-accent,
1107 var( --wp-admin-theme-color, #2271b1 )
1108 );
1109 color: #fff;
1110 font-size: 11px;
1111 font-weight: 600;
1112 line-height: 1;
1113 box-shadow: 0 2px 8px rgba( 0, 0, 0, 0.4 );
1114 }
1115
1116 /* ============================================================ *
1117 * Upload progress HUD (since 0.31.0)
1118 *
1119 * Pinned floating panel that surfaces the OS-file-drop manager's
1120 * in-flight uploads. One row per file with a `<os-progress-bar>`,
1121 * a filename + status, and a Cancel/Dismiss action. The panel
1122 * hides itself when there are no rows; it's purely reactive to
1123 * the four upload-lifecycle hooks.
1124 * ============================================================ */
1125
1126 .os-upload-hud {
1127 position: fixed;
1128 inset-block-end: 16px;
1129 inset-inline-end: 16px;
1130 width: 320px;
1131 max-width: calc( 100vw - 32px );
1132 background: var( --os-panel-bg, var( --os-ui-surface, #fff ) );
1133 color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
1134 border: 1px solid var( --os-ui-border, #dcdcde );
1135 border-radius: 10px;
1136 box-shadow: 0 8px 28px rgba( 0, 0, 0, 0.18 );
1137 z-index: 99998;
1138 font: inherit;
1139 pointer-events: auto;
1140 overflow: hidden;
1141 }
1142
1143 .os-upload-hud[ hidden ] {
1144 display: none;
1145 }
1146
1147 .os-upload-hud__header {
1148 display: flex;
1149 align-items: center;
1150 justify-content: space-between;
1151 gap: 8px;
1152 padding: 8px 12px;
1153 background: var( --os-panel-header-bg, var( --os-ui-hover, rgba( 0, 0, 0, 0.04 ) ) );
1154 border-block-end: 1px solid var( --os-ui-border, #dcdcde );
1155 font-weight: 600;
1156 font-size: 12px;
1157 }
1158
1159 .os-upload-hud__title {
1160 min-width: 0;
1161 overflow: hidden;
1162 text-overflow: ellipsis;
1163 white-space: nowrap;
1164 }
1165
1166 .os-upload-hud__close {
1167 border: 0;
1168 background: transparent;
1169 color: inherit;
1170 font-size: 16px;
1171 line-height: 1;
1172 width: 22px;
1173 height: 22px;
1174 border-radius: 4px;
1175 cursor: pointer;
1176 display: inline-flex;
1177 align-items: center;
1178 justify-content: center;
1179 }
1180 .os-upload-hud__close:hover,
1181 .os-upload-hud__close:focus-visible {
1182 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.08 ) );
1183 outline: none;
1184 }
1185
1186 .os-upload-hud__list {
1187 display: flex;
1188 flex-direction: column;
1189 gap: 10px;
1190 padding: 10px 12px;
1191 max-height: 50vh;
1192 overflow-y: auto;
1193 }
1194
1195 .os-upload-hud__row {
1196 display: grid;
1197 grid-template-columns: 1fr auto;
1198 grid-template-areas:
1199 'meta actions'
1200 'bar bar';
1201 gap: 6px 8px;
1202 align-items: center;
1203 font-size: 12px;
1204 }
1205
1206 .os-upload-hud__meta {
1207 grid-area: meta;
1208 min-width: 0;
1209 display: flex;
1210 flex-direction: column;
1211 gap: 2px;
1212 }
1213
1214 .os-upload-hud__name {
1215 overflow: hidden;
1216 text-overflow: ellipsis;
1217 white-space: nowrap;
1218 font-weight: 500;
1219 }
1220
1221 .os-upload-hud__status {
1222 font-size: 11px;
1223 opacity: 0.75;
1224 font-variant-numeric: tabular-nums;
1225 }
1226
1227 .os-upload-hud__row os-progress-bar {
1228 grid-area: bar;
1229 }
1230
1231 .os-upload-hud__actions {
1232 grid-area: actions;
1233 display: inline-flex;
1234 gap: 4px;
1235 }
1236