/* ============================================================ * ResourceList component styles * * All styles for .file-item (both grid cards and list rows), * the list header, drag ghost, per-item checkboxes, and * section-specific item modifiers (.favorite-item, .recent-item, * .trash-item). * * Page-level shell (.files-container, .selection-rect) → fileManager.css * Batch-action toolbar (.batch-selection-bar, .batch-btn …) → multiSelect.css * Section page headers (.list-header.favorites-header …) → views/*.css * ============================================================ */ /* ── Base icon container ─────────────────────────────────── */ .file-icon { width: 100px; height: 70px; border-radius: var(--radius-lg); position: relative; display: flex; align-items: center; justify-content: center; overflow: hidden; } /* Thumbnail image inside file-icon (grid and list views) */ .file-icon .file-thumb { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1; /* The thumbnail is purely decorative — it must never be a hit-test target. In grid view it fills the whole tile UNDER the overlay controls (checkbox, favorite star, kebab); a loaded full-bleed thumbnail (promoted to its own stacking context by the hover scale transform) was capturing clicks meant for those controls, so they "did nothing" (the click fell through to open-file). Making it transparent to pointer events routes corner clicks to the controls and body clicks to the .file-icon (which still opens). */ pointer-events: none; } /* ── File-type colours ─────────────────────────────────────── Each bucket sets a local --fk (the foreground hue, from the --file-kind-* tokens). The glyph takes --fk directly and the tile fill/ring derive soft tints from it with color-mix(), so the icons read as colourful tiles instead of flat monochrome. Adding a bucket is a single custom-property line; the consuming rules never change. */ .file-icon { --fk: var(--file-kind-generic); } .file-icon--folder { --fk: var(--file-kind-folder); } .file-icon--pdf { --fk: var(--file-kind-pdf); } .file-icon--doc { --fk: var(--file-kind-doc); } .file-icon--sheet { --fk: var(--file-kind-sheet); } .file-icon--slides { --fk: var(--file-kind-slides); } .file-icon--archive { --fk: var(--file-kind-archive); } .file-icon--code { --fk: var(--file-kind-code); } .file-icon--image { --fk: var(--file-kind-image); } .file-icon--video { --fk: var(--file-kind-video); } .file-icon--audio { --fk: var(--file-kind-audio); } .file-icon--text { --fk: var(--file-kind-text); } .file-icon--generic { --fk: var(--file-kind-generic); } .file-icon > i, .file-icon > svg { color: var(--fk); } /* ── Item states ─────────────────────────────────────────── */ .file-item { background-color: var(--color-item); /* Smooth the hover/selection tint (list rows used to snap). Grid cards override this with their own transform/shadow transition. */ transition: background-color var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard); } /* Brief pulse on rows that were just optimistically inserted (e.g. newly created folder, upload completion, drag-drop move into the current folder). Pure CSS so timing is deterministic — the JS adds the class, the animation auto-clears the background, and a single `animationend` listener removes the class. `scroll-margin-top` reserves space above the row for the sticky page header (`.page-sticky-header` ≈ 80 px). Without it, `scrollIntoView({ block: 'nearest' })` aligns the row's top edge against the viewport's top edge — which the sticky header is currently covering — so the user only sees the row's bottom edge. `scroll-margin-bottom` gives a touch of breathing room when the scroll happens to land the row near the viewport bottom. */ .file-item.resource-row--just-added { animation: resource-row-just-added 1.5s ease-out; scroll-margin-top: 100px; scroll-margin-bottom: 24px; } @keyframes resource-row-just-added { 0% { background-color: var(--color-success-bg); } 100% { background-color: transparent; } } /* Client-only "New" swimlane created on the fly by `addItem()` when the view is grouped. Subtler styling than a natural-group lane: the user understands the pin is temporary (it dissolves on next full reload), so we don't want the bar to dominate the list. The pinned placement at the top of the container is what makes it discoverable; the header just confirms the intent. */ .resource-list__swimlane-group--just-added > .resource-list__swimlane-header { color: var(--color-success-text); } .file-item.selected { background-color: var(--color-item-selected); } .file-item:hover { background-color: var(--color-item-hover); border-color: var(--color-border-medium); } .file-item.selected:hover { background-color: var(--color-item-hover-accent); } .file-item.dragging { opacity: 0.8; background-color: var(--color-bg-input); } .file-item.drop-target { background-color: var(--color-warning-ring); } .file-item .file-icon > i, .file-item .file-icon > svg { position: absolute; display: flex; align-items: center; justify-content: center; } /* ── Per-item checkboxes (list + grid) ───────────────────── */ .list-header-checkbox, .file-item .checkbox-cell { display: flex; align-items: center; justify-content: center; } .list-header-checkbox input[type="checkbox"], .file-item .checkbox-cell input[type="checkbox"] { /* Bumped from 17px to 20px so the row-selection checkbox reads at a similar visual weight to the 28x28 action buttons that sit at the other end of the row (Ed's 2026-07-26 UX note). 20px is the upper end of the browser-native checkbox range — beyond that platforms start rendering an oversized-and-blurry glyph. */ width: 20px; height: 20px; cursor: pointer; accent-color: var(--color-accent); border-radius: var(--radius-sm); } /* Orange outline on hover for the row-selection checkbox (Ed's UX ask 2026-07-26). `outline` (not `border`) because native checkboxes in list view honor `accent-color` but their border rendering is platform-inconsistent — `outline` is drawn OUTSIDE the box and doesn't reflow the layout. Grid view uses a custom-drawn checkbox (see `.files-grid-view … input[type="checkbox"]` below) which accepts real border styling; its hover override lives with that block and beats this via specificity. Matches the "everything hovers to accent" convention shared with row-action buttons. */ .list-header-checkbox input[type="checkbox"]:hover, .file-item .checkbox-cell input[type="checkbox"]:hover { outline: 2px solid var(--color-accent); outline-offset: 1px; } .list-header.selection-mode { grid-template-columns: 36px 1fr; background-color: var(--color-multiselect-bg); color: var(--color-multiselect-text); border-bottom-color: var(--color-multiselect-border); } .list-header.selection-mode .list-header-checkbox input[type="checkbox"] { accent-color: var(--color-accent); } /* ── List view ───────────────────────────────────────────── */ .list-header { display: grid; grid-template-columns: var(--files-list-columns); column-gap: var(--space-3); padding: 15px; font-weight: var(--weight-semibold); color: var(--color-text); background-color: var(--color-bg-subtle); border-bottom: 1px solid var(--color-border-faint); align-items: center; } /* Trash view: Name → [Path] → Size → Date → Actions * Override --files-list-columns so the trash header AND trash items align. * No checkbox (selectable=false), no owner cell visible, no type column. * * Mobile-first: the Path column is hidden by default to keep the layout * legible on narrow screens (path stays accessible via itemTooltip on hover). * From 1000px upward, Path reappears and claims roughly half the table * width via a 3fr share against Name's 1fr. */ .files-list-view.trash-list { --files-list-columns: minmax(180px, 1fr) 110px 130px 100px; } .files-list-view.trash-list .file-item .path-cell, .files-list-view.trash-list .list-header.trash-header > div:nth-child(2) { display: none; } @media (min-width: 1000px) { .files-list-view.trash-list { --files-list-columns: minmax(180px, 1fr) 3fr 110px 130px 100px; } .files-list-view.trash-list .file-item .path-cell, .files-list-view.trash-list .list-header.trash-header > div:nth-child(2) { display: block; } } .list-header > div, .files-list-view .file-item > div { min-width: 0; } /* Column alignment — targets classes on BOTH the header divs AND the value cells, so the header label always matches its column's value alignment regardless of which optional columns (path/type/owner/…) are on. The previous shape keyed off `nth-child(N)` and drifted the moment a ResourceList caller toggled a `show*` prop. */ .list-header > .size-cell, .files-list-view .file-item .size-cell { justify-self: end; text-align: right; } /* ── Sortable column headers (flat Drive-style sort) ─────────── */ .list-header-sort { display: inline-flex; align-items: center; gap: var(--space-1-5); min-width: 0; padding: 0; border: none; background: none; cursor: pointer; /* Inherit the header row's weight + colour so it reads as a header. */ font: inherit; color: inherit; transition: color var(--motion-fast) var(--ease-standard); } .list-header-sort:hover, .list-header-sort.is-active { color: var(--color-accent); } .list-header-sort__arrow { font-size: var(--text-2xs); color: var(--color-accent); flex-shrink: 0; } /* Match the value-cell alignment so header and data line up. */ .list-header-sort[data-sort-field="size"] { justify-self: end; } .list-header-sort[data-sort-field="modified_at"] { justify-self: center; } /* ── Owner column ────────────────────────────────────────── */ /* Styles applied whenever the cell is visible (hidden class absent). The .hidden utility class (display:none !important) keeps it invisible by default — it is stamped directly in the HTML templates. `display: flex` lets a nested `.user-vignette` (the common case for an owner cell — an avatar circle plus a name) become a flex child that can shrink past its intrinsic content width. The `.user-vignette__name` already declares `text-overflow: ellipsis`, but that only fires when the cascade above it ACTUALLY constrains the width. Without flex here, the vignette sized to its content and the cell clipped it flat with no ellipsis. The cell's own `text-overflow` still ellipses plain-text fallback content (cells without a vignette child). */ /* Scoped to `.file-item` so the header div — which also carries the `.owner-cell` class now (so column-alignment CSS keys off classes instead of brittle nth-child indices) — doesn't inherit the muted cell colour / cell font size. Header keeps `.list-header`'s semibold + text colour. */ .file-item .owner-cell { color: var(--color-text-secondary); font-size: var(--text-base); display: flex; align-items: center; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* List-view rows are fixed 56px (VirtualList `rowHeight={56}`), and a full UserVignette stacks avatar (32px) + name (20px) + email (15px) → ~40px block that doesn't visually fit the flex-centered cell — the email tail was cropped. Hide the email in dense-row contexts (Ed 2026-07-26); the identity signal remains (avatar + name), and the email survives in the ShareDialog / recipient pickers where UserVignette was originally scaled for. Grid view's owner cell is unaffected (it never renders a UserVignette — the sharer surfaces through the `.file-icon__badge` slot instead). */ .files-list-view .file-item .owner-cell .uv__email { display: none; } /* Expand the grid track as soon as at least one owner cell is visible. */ .files-list-view:has(.owner-cell:not(.hidden)) { --files-list-columns: 36px minmax(200px, 2fr) 120px 100px 110px 130px 200px; } .files-list-view { --files-list-columns: 36px minmax(200px, 2fr) 100px 110px 130px 200px; display: flex; flex-direction: column; width: 100%; border-radius: var(--radius-xl); overflow: hidden; background-color: var(--color-item); box-shadow: 0 1px 3px var(--color-shadow-xs); } .files-list-view .file-item { display: grid; grid-template-columns: var(--files-list-columns); column-gap: var(--space-3); padding: var(--space-3) 15px; border-bottom: 1px solid var(--color-border-xfaint); align-items: center; cursor: pointer; transition: background-color var(--motion-fast) var(--ease-standard), box-shadow var(--motion-fast) var(--ease-standard); } /* "This row" emphasis — a left accent bar on hover/selection (inset shadow, so it adds no width and never shifts the grid columns). */ .files-list-view .file-item:hover, .files-list-view .file-item.selected { box-shadow: inset 3px 0 0 var(--color-accent); } .files-list-view .file-item.drop-target { border: 1px dashed var(--color-warning-border); } .files-list-view .file-item .name-cell { color: var(--color-text); display: flex; align-items: center; gap: var(--space-3); min-width: 0; overflow: hidden; } /* Same narrowing rationale as the grid view rule below — target only the name text span, not every descendant span, so `.file-icon__badge` overlay contents (avatars, chips) aren't sized as text. */ .files-list-view .file-item .name-cell__text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Content-search snippet — fragment of the file body that matched the query. Wraps onto a second line inside the flex name-cell. */ .files-list-view .file-item .name-cell:has(.file-item__snippet) { flex-wrap: wrap; row-gap: 2px; } .files-list-view .file-item .file-item__snippet { flex-basis: 100%; /* icon width (36px) + cell gap (12px) — aligns under the name */ padding-left: 48px; font-size: 12px; color: var(--color-text-muted); } /* Grid cards are too compact for body fragments. */ .files-grid-view .file-item .file-item__snippet { display: none; } .files-list-view .file-item .file-icon { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-lg); /* Soft type-tinted tile + matching hairline ring so the row glyph reads as a colourful chip (and light previews don't bleed into the row). */ background: color-mix(in srgb, var(--fk) 12%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fk) 22%, var(--color-border)); font-size: var(--text-md); margin-bottom: 0; flex-shrink: 0; } .list-header > .date-cell, .files-list-view .file-item .date-cell { justify-self: center; text-align: center; } .files-list-view .file-item .date-cell { color: var(--color-text-muted); font-size: var(--text-base); } .files-list-view .file-item .size-cell { color: var(--color-text-muted); font-size: var(--text-base); text-align: right; font-variant-numeric: tabular-nums; } .files-list-view .file-item .type-cell { color: var(--color-text-secondary); font-weight: var(--weight-medium); font-size: var(--text-base); } .files-list-view .file-item .action-cell { /* Right-justified flex row: gives every list-view action button (shared, fav, broom, kebab, trash's restore/delete, …) a predictable horizontal cluster with a stable gap. The legacy `display: inline` on individual buttons ignored `width`/`height`, so once /recent grew a 4th button the mix of `inline` + `inline- flex` (`.btn-action`) started wrapping onto pseudo-rows that read as a broken grid. Flex flattens the mix into one line. */ display: flex; justify-content: flex-end; align-items: center; gap: var(--space-1); text-align: right; } /* List rows expose the individual action buttons inline (on hover), so the corner kebab is redundant here — it's the grid view's affordance. */ .files-list-view .file-item .action-cell .file-actions { display: none; } /* Styles for the built-in action-cell buttons (favorite-star, kebab). * `.btn-action` is excluded — it owns its own colors via the generic * `.btn-action` rule + variant modifiers (e.g. `.btn-action--delete`). */ .files-list-view .file-item .action-cell button:not(.btn-action), .files-list-view .file-item .action-cell div { display: inline; width: 28px; height: 28px; border-radius: var(--radius-lg); border: none; background: transparent; align-items: center; justify-content: center; cursor: pointer; color: var(--color-text-subtle); font-size: var(--text-md); } .files-list-view .file-item .action-cell button:not(.btn-action):hover { background: var(--color-border-subtle); /* Accent-orange hover — matches the grid view's `.file-actions:hover` and the shared `.btn-action:hover` rule. Semantic overrides on `.favorite-star:hover` / `.shared-button:hover` (gold / blue) take precedence via more-specific selectors below. */ color: var(--color-accent); } /* List-view resting/active state colors for favorite (gold) and shared (blue). Hover is intentionally NOT overridden here — the generic `.action-cell button:not(.btn-action):hover → --color-accent` rule above owns the orange hover for every button (kebab / favorite / shared) per Ed's 2026-07-26 spec. This block only paints the at-rest state on `.active` rows so the star / shared chip stays discoverable on a quiet row. */ .files-list-view .file-item .action-cell button.favorite-star.active { color: var(--color-star-text-hover); } .files-list-view .file-item .action-cell button.shared-button.active { color: var(--color-badge-blue-text); } /* Fav-star + shared-button — hidden on quiet rows, visible on row hover, and always visible when their `.active` class is set (so a favorited / shared row is discoverable without mousing over). `opacity: 0` + `pointer-events: none` (instead of `visibility: hidden`) so the hide transitions match the kebab's fade — Ed's 2026-07-26 UX note: pre-refactor kebab faded over motion-fast while star/shared snapped out instantly (visibility flips have no animatable value), producing a staggered exit when the pointer left a row. The slot still reserves layout because the button geometry is unchanged; only its paint is toggled. */ .files-list-view .file-item .action-cell button.favorite-star, .files-list-view .file-item .action-cell button.shared-button { opacity: 0; pointer-events: none; border: none; transition: opacity var(--motion-fast) var(--ease-standard); } /* Strict reveal: star + shared appear ONLY on `.active` (row is favorited / shared) or pointer hover — Ed's 2026-07-26 spec. No `:focus-within` reveal, so keyboard focus on the row-body (name / path cells) does NOT flash the actions cluster. Quiet rows stay quiet at rest. */ .files-list-view .file-item:hover .action-cell button.favorite-star, .files-list-view .file-item:hover .action-cell button.shared-button, .files-list-view .file-item .action-cell button.favorite-star.active, .files-list-view .file-item .action-cell button.shared-button.active { opacity: 1; pointer-events: auto; } /* Reveal the kebab on hover for cleaner rows — but only on hover-capable devices, so touch users (no hover) keep it always tappable. Applies to both list and grid views because both keep the kebab inside `.action-cell`. Keyboard accessibility comes from `:focus-visible` on the kebab button itself (below), NOT `:focus-within` on the row. Using `:focus-within` on the row was a lingering-visibility trap: • dragstart landed focus on the dragged descendant → row `:focus-within` stayed true after the pointer left → kebab stayed visible on an otherwise-idle row. • Opening a context-menu / ShareDialog portal moved focus outside the row (good) but if focus briefly bounced through the kebab first, the reveal could persist through the transition. Ed's 2026-07-26 report: "when starting dragging or when using the share dialog, I have the [...] button that remains visible." */ @media (hover: hover) { .files-list-view .file-item .action-cell button.file-actions, .files-grid-view .file-item .action-cell button.file-actions { opacity: 0; transition: opacity var(--motion-fast) var(--ease-standard); } .files-list-view .file-item:hover .action-cell button.file-actions, .files-grid-view .file-item:hover .action-cell button.file-actions, .files-list-view .file-item .action-cell button.file-actions:focus-visible, .files-grid-view .file-item .action-cell button.file-actions:focus-visible { opacity: 1; } } /* ── Grid view ───────────────────────────────────────────── */ .files-grid-view { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--grid-card-min), 1fr)); gap: var(--space-5); position: relative; } /* header not visible in grid mode */ .files-grid-view .list-header { display: none; } /* ── Mobile reflow (≤ --bp-sm 640px) ────────────────────────── The 6-column grid can't fit a phone, so each list row collapses to a compact flex line (icon + name … size + actions) and the column header is hidden (sort controls live in the actions bar). One block covers files / trash / favorites / recent uniformly; flex ignores the grid tracks, so there's no header-vs-row misalignment to maintain. */ @media (max-width: 640px) { /* Keep the selection-mode header (holds select-all); hide the plain one. */ .list-header:not(.selection-mode) { display: none; } .files-list-view .file-item { display: flex; align-items: center; gap: var(--space-3); } .files-list-view .file-item .type-cell, .files-list-view .file-item .date-cell, .files-list-view .file-item .owner-cell, .files-list-view .file-item .path-cell { display: none; } .files-list-view .file-item .name-cell { flex: 1; min-width: 0; } .files-list-view .file-item .checkbox-cell, .files-list-view .file-item .size-cell, .files-list-view .file-item .action-cell { flex-shrink: 0; } /* Tighter grid on phones (2 columns instead of 1). */ .files-grid-view { --grid-card-min: 140px; gap: var(--space-2); } } .files-grid-view .file-item { border-radius: var(--radius-2xl); /* Hairline at rest → accent on hover (gallery, not form). */ border: 1px solid var(--color-border); padding: var(--space-3); display: flex; flex-direction: column; align-items: center; box-shadow: 0 1px 2px var(--color-shadow-xs); cursor: pointer; width: 100%; min-height: 160px; position: relative; transition: transform var(--motion-base) var(--ease-standard), box-shadow var(--motion-base) var(--ease-standard), border-color var(--motion-base) var(--ease-standard); } .files-grid-view .file-item.dragging { opacity: 0.5; transform: scale(0.95); box-shadow: none; } .files-grid-view .file-item:hover { transform: translateY(-3px); box-shadow: 0 12px 28px -8px var(--color-shadow-md); border-color: var(--color-accent); } /* The thumbnail subtly zooms inside its clipped tile on hover — the "alive" feel of a premium gallery. */ .files-grid-view .file-item .file-icon .file-thumb { transition: transform var(--motion-moderate) var(--ease-standard); } .files-grid-view .file-item:hover .file-icon .file-thumb { transform: scale(1.04); } .files-grid-view .file-item.selected { border-color: var(--color-accent); box-shadow: 0 0 0 1px var(--color-accent-ring-dark), 0 4px 12px var(--color-accent-ring); } .files-grid-view .file-item.selected:hover { box-shadow: 0 0 0 1px var(--color-accent-ring-strong), 0 6px 18px var(--color-accent-ring-dark); } /* elements hidden in grid view — the type label ("Imagen") is redundant under a thumbnail, so we drop it and surface the file size instead. */ .files-grid-view .file-item .date-cell, .files-grid-view .file-item .type-cell, .files-grid-view .file-item .owner-cell { display: none; } /* Selection checkbox */ /* Overlay controls sit ON the thumbnail (offset by the card padding) over a frosted scrim, so they read as part of the media instead of floating on the card corner. */ .files-grid-view .file-item .checkbox-cell { position: absolute; top: calc(var(--space-3) + 8px); left: calc(var(--space-3) + 8px); /* 30x30 matches the action-cell chip pills (`.file-actions`, star, shared, `.btn-action`) so the checkbox and the corner-cluster buttons read as siblings of one visual size (Ed 2026-07-26). */ width: 30px; height: 30px; border-radius: var(--radius-md); background: var(--color-scrim-control); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); box-shadow: 0 1px 3px var(--color-shadow-sm); display: flex; align-items: center; justify-content: center; opacity: 0; z-index: 10; cursor: pointer; transition: opacity var(--motion-fast) var(--ease-standard); } /* Visible whenever the card is hovered OR already selected, so the checked state stays on screen after the pointer leaves. */ .files-grid-view .file-item:hover .checkbox-cell, .files-grid-view .file-item.selected .checkbox-cell { opacity: 1; } /* Custom-drawn checkbox: an empty white box that fills with accent and shows a crisp white tick when checked. The native 17px control was invisible once the cell turned accent on selection (accent-on-accent), and the intended tick targeted a `.checkbox-cell i` the markup never renders — so a click read as "nothing happened". This makes the checked state unmistakable. */ .files-grid-view .file-item .checkbox-cell input[type="checkbox"] { appearance: none; -webkit-appearance: none; /* Custom-drawn glyph sized proportionally to the 30x30 chip pill — bumped from 18x18 to match the list-view checkbox's 20x20. */ width: 20px; height: 20px; margin: 0; border: 2px solid var(--color-border-medium); border-radius: var(--radius-sm); background: var(--color-bg-surface); display: grid; place-content: center; cursor: pointer; transition: background-color var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard); } /* Grid view uses a custom-drawn checkbox (`appearance: none`) so we can swap the real border color on hover — cleaner than the list-view `outline` trick, and no double-ring on this variant. Specificity (0,4,1 + 0,0,1) beats the shared `input[type="checkbox"]:hover` outline rule above so the grid-view chip doesn't get both a border AND an outline. */ .files-grid-view .file-item .checkbox-cell input[type="checkbox"]:hover { border-color: var(--color-accent); outline: none; } .files-grid-view .file-item .checkbox-cell input[type="checkbox"]::after { content: ""; width: 5px; height: 9px; margin-top: -1px; border: solid var(--color-danger-text); border-width: 0 2px 2px 0; transform: rotate(45deg) scale(0); transform-origin: center; transition: transform var(--motion-fast) var(--ease-standard); } .files-grid-view .file-item .checkbox-cell input[type="checkbox"]:checked { background: var(--color-accent); border-color: var(--color-accent); } .files-grid-view .file-item .checkbox-cell input[type="checkbox"]:checked::after { transform: rotate(45deg) scale(1); } .files-grid-view .file-item .checkbox-cell input[type="checkbox"]:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; } /* ── Grid card action cluster ───────────────────────────────────── Every action a row can surface — favorite star, `.file-actions` kebab, per-section `.btn-action` icons (e.g. trash's Restore / Delete permanently) — lives in a single `.action-cell` container pinned to the top-right of the card. The container carries the position + hover-reveal + gap; its children just supply their own chip visuals (30x30 scrim pill, etc.), no more one-off absolute positioning per child. Old rules put `.file-actions` and `.favorite-star` at hand-crafted absolute coordinates and hid `.btn-action` entirely — that made trash's per-item buttons invisible in grid view. The unified container reads as one design pattern and takes whatever children the row template hands it. */ .files-grid-view .file-item .action-cell { position: absolute; top: calc(var(--space-3) + 8px); right: calc(var(--space-3) + 8px); z-index: 10; display: flex; gap: var(--space-1); } /* Full-width top action bar — only applies when the row surfaces at least one state chip (shared or favorite). Trash (no shared / no favorite) falls through to the horizontal corner cluster above, preserving its Restore / Delete / kebab layout. Layout target: ┌────────────────────────────────────────────┐ │[chk] [kebab] [btn-action…] [shared] [fav] │ ← single row │ │ │ (thumbnail) │ │ │ └────────────────────────────────────────────┘ The checkbox stays where it is (its own `.checkbox-cell` absolute at top-left); the action-cell spans horizontally next to it so the two clusters read as one row. `justify-content: space-between` + `order` splits the flex row into a left group (kebab + btn-actions) and a right group (shared + favorite) without fighting DOM order — the markup still has shared, favorite, itemActions, kebab in that sequence so list-view's inline right-aligned flow is unchanged. */ .files-grid-view .file-item .action-cell:has(.shared-button, .favorite-star) { /* Start right after the checkbox column (30px chip + inline gap) so the left group aligns visually with the checkbox row. Kept in sync with `.checkbox-cell` width above — the two share this constant. */ left: calc(var(--space-3) + 8px + 30px + var(--space-2)); right: calc(var(--space-3) + 8px); display: flex; align-items: center; justify-content: flex-start; gap: var(--space-1); } .files-grid-view .file-item .action-cell:has(.shared-button, .favorite-star) .file-actions { order: 1; } .files-grid-view .file-item .action-cell:has(.shared-button, .favorite-star) .btn-action { order: 2; } /* The first "right group" item eats all remaining horizontal space via `margin-left: auto`, which is the flexbox idiom for splitting a row into left+right clusters without wrapping the two groups in extra containers. When both buttons are wired, shared is first (`order: 3`) and takes the push; fav follows with the normal gap. When only fav is wired (`/shared-with-me` — recipient side, no share affordance), fav is the sole right-group item and takes the push instead. */ .files-grid-view .file-item .action-cell:has(.shared-button, .favorite-star) .shared-button { order: 3; margin-left: auto; } .files-grid-view .file-item .action-cell:has(.shared-button, .favorite-star) .favorite-star { order: 4; } .files-grid-view .file-item .action-cell:has(.favorite-star):not(:has(.shared-button)) .favorite-star { margin-left: auto; } /* Per-button visibility (mirrors list view): each button is independently gated by its own `.active` flag OR row-hover. This prevents a favorited row from also lighting up the shared button (and vice versa). `opacity + pointer-events` so the fade timing matches the kebab and every `.btn-action--hover` — see the list-view block above for why. */ .files-grid-view .file-item .action-cell .favorite-star, .files-grid-view .file-item .action-cell .shared-button { opacity: 0; pointer-events: none; transition: opacity var(--motion-fast) var(--ease-standard); } /* Strict reveal: same rule as the list view — `.active` OR pointer hover, no `:focus-within` (Ed's 2026-07-26 spec). */ .files-grid-view .file-item:hover .action-cell .favorite-star, .files-grid-view .file-item:hover .action-cell .shared-button, .files-grid-view .file-item .action-cell .favorite-star.active, .files-grid-view .file-item .action-cell .shared-button.active { opacity: 1; pointer-events: auto; } /* Chip visuals for anything inside the corner cluster — the kebab, the star, the shared button, any `.btn-action`. Uniform 30x30 scrim pill so they line up in the flex row. NOTE: no `opacity: 1` here. Per-button reveal rules above own the opacity (star/shared: `.active` + `:hover`; kebab + btn-action: `:hover`). Pre-refactor this block force-set `opacity: 1` on the assumption that reveal lived on the parent `.action-cell` — that assumption is gone (Ed 2026-07-26), and the leftover made star + shared appear always-visible in grid view regardless of hover / `.active` state. */ .files-grid-view .file-item .action-cell .file-actions, .files-grid-view .file-item .action-cell .favorite-star, .files-grid-view .file-item .action-cell .shared-button, .files-grid-view .file-item .action-cell .btn-action { position: static; width: 30px; height: 30px; /* `margin: 0` overrides the legacy `.files-grid-view .file-item .btn-action { margin-top: var(--space-1) }` rule further down — inside the corner cluster the parent's `gap` handles spacing and any per-child margin would misalign the pills. */ margin: 0; padding: 0; border: none; border-radius: var(--radius-full); background: var(--color-scrim-control); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); box-shadow: 0 1px 3px var(--color-shadow-sm); display: inline-flex; align-items: center; justify-content: center; color: var(--color-text); font-size: var(--text-md); cursor: pointer; } /* Unified row-action hover: every button in the grid-view corner cluster (kebab, star, shared, btn-action*) turns accent-orange on hover. Semantic states are conveyed by the `.active` class, not by hover color, so favorite = gold when starred, shared = blue when shared, both regardless of pointer position (see `.active` rules below). Ed's 2026-07-26 UX call: "orange for mouse over on all buttons; blue only for active shared." Selector specificity (0,4,1 + 0,0,1 = high) beats the chip-visual base rule at ~line 852 (`.files-grid-view .file-item .action-cell .btn-action { color: var(--color-text) }`, 0,4,0), which is why the simpler `.btn-action:hover` didn't take effect inside the corner cluster. */ .files-grid-view .file-item .action-cell .file-actions:hover, .files-grid-view .file-item .action-cell .btn-action:hover, .files-grid-view .file-item .action-cell .favorite-star:hover, .files-grid-view .file-item .action-cell .shared-button:hover { color: var(--color-accent); } .files-grid-view .file-item.drop-target { background-color: var(--color-warning-ring); border: 2px dashed var(--color-warning-border); } /* Favorite star + shared button — resting/active state colors only. HOVER color for both lives in the unified `.action-cell button:hover → --color-accent` rule above; the per-state palette here only fires when the button is NOT hovered. Convention (Ed 2026-07-26): • hover → orange (accent) — every row action • star.active (not hover) → gold • shared.active (not hover) → blue `.active` retains its color even on hover for `favorite-star` (star users expect gold-on-gold on the currently-starred item; losing the glyph mid-click reads as broken) but yields to orange for `shared` per Ed's explicit "blue only for active shared" — pointer-over on an already-shared row should still communicate "you're about to toggle something." */ .files-grid-view .file-item button.favorite-star, .files-grid-view .file-item button.shared-button { color: var(--color-text-subtle); font-size: 15px; line-height: var(--leading-none); } .files-grid-view .file-item button.favorite-star.active { color: var(--color-star-text-hover); } .files-grid-view .file-item button.shared-button.active { color: var(--color-badge-blue-text); } .files-grid-view .file-item .name-cell { font-size: var(--text-sm); font-weight: var(--weight-medium); text-align: center; margin-bottom: 2px; color: var(--color-text); /* Span the full card width. The card is a centered flex column (`align-items: center`), so without an explicit width the name-cell — which wraps BOTH the thumbnail tile and the filename — shrink-wraps to the filename's length. That made `.file-icon { width: 100% }` track the NAME width, so a long name ("CURSO TERRAFORM") rendered a big tile and a short one ("Idiomas") a small one. Forcing 100% makes every thumbnail tile identical regardless of name length. */ width: 100%; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: var(--space-1); } /* Narrowed from the pre-fix `.name-cell span` (descendant selector) to the specific name-text span. The broad rule caught EVERY span inside `.name-cell` — including the `.file-icon__badge` overlay and, inside it, ``'s `.ua` wrapper — and slapped 8 px of top/bottom padding on them, which crushed the badge's `` from 22 × 22 down to 22 × 6 (Ed 2026-07-26). Text ellipsis / padding stays on the text span alone; overlay children in `.file-icon` are unaffected. */ .files-grid-view .file-item .name-cell__text { display: block; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-top: var(--space-2); padding-bottom: var(--space-2); } .files-grid-view .file-item .name-cell svg.favorite-star-inline { display: none; } /* The grid card's secondary line is now the combined .grid-meta block ("hace 2 días · 4,31 MB"), so the standalone size-cell is hidden here. */ .files-grid-view .file-item .size-cell { display: none; } /* ── Grid metadata line — recency + size (+ owner avatar when shared) ────── Replaces the lone "4,31 MB": a top file app captions a thumbnail with WHEN it was touched (the dominant retrieval cue), not just bytes. Hidden everywhere except the grid — list view keeps its own date/size/owner columns, so a display:none here drops it cleanly out of the row grid. */ .grid-meta { display: none; } .files-grid-view .file-item .grid-meta { display: flex; align-items: center; justify-content: center; gap: var(--space-1); max-width: 100%; margin-top: 2px; font-size: var(--text-2xs); color: var(--color-text-muted); line-height: var(--leading-snug); } /* On a narrow card the relative date truncates first; the size never clips. */ .files-grid-view .file-item .grid-meta__date { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .files-grid-view .file-item .grid-meta__size { flex: 0 0 auto; color: var(--color-text-faint); } .files-grid-view .file-item .grid-meta__size::before { content: "·"; margin-right: var(--space-1); color: var(--color-text-faint); } /* Owner avatar — only rendered when the item is shared; sits at the line end. */ .files-grid-view .file-item .grid-meta .user-vignette { flex: 0 0 auto; margin-left: var(--space-1); } /* Full-width thumbnail tile with a fixed aspect ratio — the image (`.file-thumb`, object-fit:cover) fills it edge-to-edge for a uniform, rich grid instead of a small letterboxed preview. Non-image files show their type icon centred on the placeholder fill. */ .files-grid-view .file-item .file-icon { width: 100%; height: auto; aspect-ratio: 4 / 3; border-radius: var(--radius-lg); /* Soft type-tinted fill + matching hairline ring (a thumbnail, when present, covers both edge-to-edge). Gives non-preview files a modern colourful tile keyed to their type instead of a flat grey panel. */ background: color-mix(in srgb, var(--fk) 9%, var(--color-bg-input)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fk) 22%, var(--color-border)); margin: 0 0 var(--space-3); font-size: 30px; } /* Type icon for non-thumbnail files (documents, etc.) — perfectly centered in the tile. `inset: 0` + `margin: auto` + a fixed size is the absolute-centering trick; the previous `top: auto` broke the vertical half, bottom-anchoring the icon so it sat low in the tile. */ .files-grid-view .file-item .file-icon > i, .files-grid-view .file-item .file-icon > svg { inset: 0; margin: auto; width: 56px; height: 56px; } /* ── Drag ghost ──────────────────────────────────────────── */ .dragged-items { --dragged-files-list-columns: 36px minmax(200px, 1fr); flex-direction: column; background-color: transparent; overflow: hidden; border-radius: var(--radius-xl); width: 100%; } .dragged-items .file-item { grid-template-columns: var(--dragged-files-list-columns); column-gap: var(--space-3); align-items: center; background-color: var(--color-item); display: flex; padding: var(--space-1); width: 360px; height: 46px; color: var(--color-text); } /* file name text is truncated */ .dragged-items .file-item div:nth-child(2) { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dragged-items .file-item .file-icon { width: 36px; height: 36px; } .dragged-items div.fading { -webkit-mask-image: linear-gradient(to bottom, var(--color-item) 10%, transparent); mask-image: linear-gradient(to bottom, var(--color-item) 10%, transparent); } .dragged-items-badge { position: absolute; top: 0; right: 0; transform: translate(50%, -50%); background: var(--color-notification-badge); color: var(--color-danger-text); border-radius: 50%; min-width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: var(--text-xs); font-weight: var(--weight-bold); padding: var(--space-0-5); } /* ── Swimlane group header ───────────────────────────────── */ /* Spans the full grid width in list view; no-op in grid view since grid wraps it naturally. */ .resource-list__swimlane-header { grid-column: 1 / -1; padding: var(--space-1-5) var(--space-3) var(--space-1); font-size: 0.72rem; font-weight: var(--weight-semibold); text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-text-faint); border-bottom: 1px solid var(--color-border); margin-top: var(--space-2); cursor: default; user-select: none; } .resource-list__swimlane-header:first-child, .list-header + .resource-list__swimlane-header { margin-top: 0; } /* Grouped-grid container (files route): a vertical stack of (header + its own windowed card grid) per swimlane. Not a grid itself — the card grid rides each VirtualList's inner window — so it just stacks. */ .files-grouped-grid { display: flex; flex-direction: column; } /* In that flex stack the `grid-column: 1 / -1` span is inert; the header spans naturally as a block-level flex child. First header needs no top margin (there is no list-header sibling before it in the grid path). */ .files-grouped-grid > .resource-list__swimlane-header--grid { grid-column: auto; } .files-grouped-grid > .resource-list__swimlane-header--grid:first-child { margin-top: 0; } /* When the header contains a rich DOM node (e.g. a user vignette for the "owner" group-by), reset the typographic overrides that only make sense for plain-text labels, and lay the node out inline. */ .resource-list__swimlane-header--node { display: flex; align-items: center; padding: var(--space-1) var(--space-3); text-transform: none; letter-spacing: normal; font-size: inherit; font-weight: var(--weight-normal); color: inherit; } /* ── Swimlane group card (list view only) ────────────────── */ /* When swimlane groups are present, dissolve the outer container into the page background so each group card reads as its own panel. */ .files-list-view:has(.resource-list__swimlane-group) { background-color: transparent; box-shadow: none; border-radius: 0; overflow: visible; gap: var(--space-2-5); } /* Each group is a self-contained card */ .files-list-view .resource-list__swimlane-group { background-color: var(--color-item); border-radius: var(--radius-xl); box-shadow: 0 1px 3px var(--color-shadow-xs); overflow: hidden; } /* The header inside a group card is always first — no extra top margin */ .files-list-view .resource-list__swimlane-group .resource-list__swimlane-header { margin-top: 0; } /* ── Swimlane group wrapper (grid view) ──────────────────── */ /* The group wrapper must be transparent to the grid so .file-item children continue to flow in the parent's columns (subgrid mirrors the column tracks). The wrapper spans the full row width; items inside fill the columns naturally. */ .files-grid-view .resource-list__swimlane-group { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; gap: var(--space-5); } /* ── Section item modifiers ──────────────────────────────── */ /* — Favorites — */ .file-item.favorite-item { border-left: 3px solid var(--color-warning-border); [dir="rtl"] & { border-right: 3px solid var(--color-warning-border); border-left: unset; } } /* list-view column override */ .file-item.favorite-item { position: relative; grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px; } .file-item.favorite-item .favorite-indicator { position: relative; top: 0; right: 0; width: 30px; height: 30px; } /* — Recent — */ .files-grid-view .file-item.recent-item { border-left: 3px solid var(--color-recent-border); [dir="rtl"] & { border-right: 3px solid var(--color-recent-border); border-left: unset; } } /* list-view column override */ .file-item.recent-item { position: relative; grid-template-columns: 30px minmax(200px, 2fr) 1fr 1fr 120px; } .file-item.recent-item .recent-indicator { position: relative; top: 0; right: 0; width: 30px; height: 30px; } /* ── Path column (opt-in via ResourceListConfig.showPath) ────────── * Visible in list view only — grid cards hide it because they have no * dedicated column slot. itemTooltip still surfaces the path on hover. */ .files-list-view .file-item .path-cell { color: var(--color-text-secondary); font-size: var(--text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .files-grid-view .file-item .path-cell { display: none; } /* ── Custom inline actions (ResourceListConfig.customActions) ────── * Always visible in both list and grid view — used by trash for the * restore / permanent-delete buttons that must be one click away. */ .btn-action { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-lg); border: none; background: transparent; cursor: pointer; color: var(--color-text-subtle); font-size: var(--text-md); padding: 0; } .btn-action:hover { background: var(--color-border-subtle); /* Accent (orange) hover matches `.file-actions` (kebab) and the favorite / shared button semantics — the pre-refactor grey-only tint left `/recent`'s broom, `/trash`'s restore and `/search`'s "open parent" reading as inert on hover next to the accented kebab. Ed's 2026-07-26 UX ask: "all row buttons should change color on hover, not just kebab and favorite." Section-specific variants (`.btn-action--delete` in trash, `--on` in ShareDialog) still win via more-specific selectors so red / etc. semantics are preserved. */ color: var(--color-accent); } /* Opt-in modifier: hide the button until the row is hovered / focused. Used by `/recent`'s per-row broom and `/search`'s open-parent (history-management / navigational actions that shouldn't distract from the row content at rest). Trash's Restore / Delete stay on the plain `.btn-action` — those are the reason the user opened trash, and hiding them would fail Fitts' law. `opacity + pointer-events` (not `visibility`) so the fade timing matches the kebab + star + shared button — Ed's 2026-07-26 UX note: pre-refactor each reveal mechanism was different, producing a staggered exit when the pointer left a row. */ .files-list-view .file-item .action-cell .btn-action--hover, .files-grid-view .file-item .action-cell .btn-action--hover { opacity: 0; pointer-events: none; transition: opacity var(--motion-fast) var(--ease-standard); } /* Reveal on hover OR when the button itself has keyboard focus. The pre-fix `:focus-within` on the row was a lingering-visibility trap during drag / dialog transitions — see the `.file-actions` block above for the full rationale. `:focus-visible` on the button gives keyboard users the same reveal without the row-scope side effect. */ .files-list-view .file-item:hover .action-cell .btn-action--hover, .files-grid-view .file-item:hover .action-cell .btn-action--hover, .files-list-view .file-item .action-cell .btn-action--hover:focus-visible, .files-grid-view .file-item .action-cell .btn-action--hover:focus-visible { opacity: 1; pointer-events: auto; } /* Legacy: a margin-top on `.btn-action` in grid view for the era when these buttons flowed at the bottom of the card. Kept for any free-standing use outside the corner cluster; reset inside `.action-cell` (line ~745) so the broom / restore / delete pills align with the kebab and star. */ .files-grid-view .file-item .btn-action { margin-top: var(--space-1); }