@layer vendor-overrides { .gridjs-container { /* Single source of truth for row rhythm — padding + line-height drive height. We do NOT set fixed `height` on th/td: that conflicts with variable content (avatars, badges, buttons) and with Grid.js' fixedHeader scroll container. */ --app-grid-cell-pad-y: 0.6rem; --app-grid-cell-pad-x: 0.9rem; --app-grid-row-line-height: 1.45; --app-grid-row-hover: color-mix( in srgb, var(--app-primary) 4%, var(--app-background-color) ); --app-grid-row-focus: color-mix( in srgb, var(--app-primary) 7%, var(--app-background-color) ); --app-grid-row-selected: color-mix( in srgb, var(--app-primary) 10%, var(--app-background-color) ); --app-grid-row-selected-hover: color-mix( in srgb, var(--app-primary) 14%, var(--app-background-color) ); color: var(--app-color); width: 100%; padding: 0; } .gridjs-container.app-grid-density-sm { --app-grid-cell-pad-y: 0.45rem; --app-grid-cell-pad-x: 0.75rem; } .gridjs-container.app-grid-density-lg { --app-grid-cell-pad-y: 0.85rem; --app-grid-cell-pad-x: 1.05rem; } .gridjs-wrapper { position: relative; border: var(--app-border-width) solid var(--app-table-border-color); border-radius: calc(var(--app-border-radius) * 1.4); box-shadow: none; background-color: var(--app-background-color); /* Horizontal scroll for wide tables; vertical scroll is managed by Grid.js (inline `height` / `max-height`) when fixedHeader is active. Do NOT clamp overflow-y here or the sticky header breaks and rows get clipped. */ overflow-x: auto; } .gridjs-container.gridjs-has-error .gridjs-wrapper { border-color: color-mix(in srgb, var(--app-danger, #b44545) 40%, var(--app-table-border-color)); } .gridjs-footer { background-color: transparent; border: 0; box-shadow: none; color: var(--app-muted-color); padding: 0.85rem 0 0; } .gridjs-table { color: var(--app-color); min-width: 100%; width: max-content; table-layout: auto; } th.gridjs-th { padding: var(--app-grid-cell-pad-y) var(--app-grid-cell-pad-x); border: 0; border-bottom: var(--app-border-width) solid var(--app-table-border-color); background-color: color-mix( in srgb, var(--app-background-color) 94%, var(--app-muted-border-color) ); color: var(--app-color); font-weight: 600; line-height: var(--app-grid-row-line-height); text-align: left; text-align: start; white-space: nowrap; vertical-align: middle; } /* Vendor uses floats (`.gridjs-th-content` float:left + `.gridjs-sort` float:right) which break vertical centering: the 24px sort icon and the text label land at different baselines. Replace floats with inline-block + vertical-align:middle so both share the same cross-axis center. Keep as table-cell (no display:flex) to preserve the browser's table width-distribution algorithm. */ th.gridjs-th .gridjs-th-content { float: none; display: inline-block; vertical-align: middle; width: auto; max-width: calc(100% - 22px); line-height: inherit; overflow: hidden; text-overflow: ellipsis; } th.gridjs-th:not(.gridjs-th-sort) .gridjs-th-content { max-width: 100%; } th.gridjs-th button.gridjs-sort { float: none; display: inline-block; vertical-align: middle; margin-inline-start: 0.35rem; } .gridjs-wrapper .gridjs-th-fixed { position: sticky; top: 0; z-index: 3; box-shadow: 0 1px 0 0 var(--app-table-border-color); } th.gridjs-th-sort:hover, th.gridjs-th-sort:focus-visible { background-color: color-mix( in srgb, var(--app-primary) 6%, var(--app-background-color) ); } td.gridjs-td { border: 0; border-bottom: var(--app-border-width) solid var(--app-table-border-color); padding: var(--app-grid-cell-pad-y) var(--app-grid-cell-pad-x); background-color: var(--app-background-color); color: var(--app-color); font-weight: var(--app-font-weight); line-height: var(--app-grid-row-line-height); white-space: nowrap; text-align: left; text-align: start; vertical-align: middle; } .gridjs-tr:last-child td.gridjs-td { border-bottom: 0; } .gridjs-tbody, td.gridjs-td { background-color: var(--app-background-color); } .gridjs-tr[data-row-open-url] { cursor: pointer; } .gridjs-tr[data-row-open-url]:hover td { background: var(--app-grid-row-hover); } .gridjs-tr:not([data-row-open-url]):hover td { background: var(--app-background-color); } .gridjs-tr[data-row-open-url]:focus td, .gridjs-tr[data-row-open-url]:focus-within td { background: var(--app-grid-row-focus); } .gridjs-tr[data-row-open-url]:focus-visible td { background: var(--app-grid-row-focus); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--app-primary) 24%, transparent); } .gridjs-tr.gridjs-tr-selected td { background: var(--app-grid-row-selected); } .gridjs-tr.gridjs-tr-selected[data-row-open-url]:hover td { background: var(--app-grid-row-selected-hover); } .gridjs-search { float: none; } .gridjs-search-input { width: min(320px, 100%); } input.gridjs-input { background-color: var(--app-background-color); border: var(--app-border-width) solid var(--app-form-element-border-color); border-radius: var(--app-border-radius); color: var(--app-color); font-size: 0.95rem; padding: 0.5rem 0.75rem; } input.gridjs-input:focus { border-color: var(--app-primary); box-shadow: 0 0 0 var(--app-outline-width) var(--app-primary-focus); } .gridjs-pagination { display: flex; flex-wrap: wrap; align-items: center; /* Robust right-alignment regardless of summary presence: summary sits left, pages pushed right by auto margin. space-between breaks when summary is absent (single flex-child collapses visually). */ justify-content: flex-start; gap: 0.65rem; color: var(--app-muted-color); } .gridjs-pagination .gridjs-summary, .gridjs-pagination .gridjs-pages { float: none; margin: 0; } .gridjs-pagination .gridjs-summary { font-size: var(--text-xs); color: var(--app-muted-color); } .gridjs-pagination .gridjs-pages { display: inline-flex; align-items: center; gap: 0.35rem; margin-inline-start: auto; } /* Pagination buttons follow the project's Pico-style theming convention: the base `button` rule in app-shell.css redefines `--app-color` and `--app-background-color` as button-local custom properties (see app-shell.css ~line 812). A rule like `color: var(--app-color)` inside the button scope resolves to `--app-primary-inverse` (white) — that is the origin of the "white-on-white" bug. We MUST theme by redefining the same custom properties, not by setting `color`/`background-color` directly, and we must use a token that isn't shadowed inside the button scope (`--app-form-element-color` fits). */ /* Align pagination buttons with the Stripe-style neutral-chip secondary outline: theme-aware surface, high-contrast text, subtle border, raised shadow. Current page stays distinct via primary fill. */ .gridjs-pagination .gridjs-pages button, .gridjs-pagination .gridjs-pages button[type="button"] { --app-background-color: var(--app-button-neutral-bg); --app-color: var(--app-button-neutral-color); --app-border-color: var(--app-button-neutral-border); box-shadow: var(--app-button-neutral-shadow); margin: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: calc(var(--app-border-radius) * 0.9); padding: 0.25rem 0.65rem; line-height: 1.3; min-width: 2rem; white-space: nowrap; font-weight: var(--font-medium); } .gridjs-pagination .gridjs-pages button:hover:not(:disabled), .gridjs-pagination .gridjs-pages button[type="button"]:hover:not(:disabled), .gridjs-pagination .gridjs-pages button:focus:not(:disabled), .gridjs-pagination .gridjs-pages button[type="button"]:focus:not(:disabled) { --app-background-color: var(--app-button-neutral-bg-hover); --app-color: var(--app-button-neutral-color-hover); --app-border-color: var(--app-button-neutral-border-hover); box-shadow: var(--app-button-neutral-shadow-hover); } .gridjs-pagination .gridjs-pages button:focus-visible { outline: none; box-shadow: var(--app-button-neutral-shadow), 0 0 0 var(--app-outline-width) var(--app-primary-focus); } .gridjs-pagination .gridjs-pages button.gridjs-currentPage, .gridjs-pagination .gridjs-pages button.gridjs-currentPage[type="button"], .gridjs-pagination .gridjs-pages button.gridjs-currentPage:hover, .gridjs-pagination .gridjs-pages button.gridjs-currentPage:focus { --app-background-color: var(--app-primary); --app-border-color: var(--app-primary); --app-color: var(--app-primary-inverse); box-shadow: var(--app-button-filled-shadow); font-weight: var(--font-semibold); } .gridjs-pagination .gridjs-pages button:disabled, .gridjs-pagination .gridjs-pages button[disabled] { --app-background-color: var(--app-button-neutral-bg); --app-color: var(--app-muted-color); --app-border-color: var(--app-button-neutral-border); box-shadow: none; cursor: default; opacity: 0.55; } .gridjs-pagination .gridjs-pages button.gridjs-spread, .gridjs-pagination .gridjs-pages button.gridjs-spread[type="button"] { --app-background-color: transparent; --app-border-color: transparent; --app-color: var(--app-muted-color); min-width: auto; padding-inline: 0.35rem; pointer-events: none; } /* ------------------------------------------------------------------- Loading Skeleton (Stripe/GitHub goldstandard) Rendered as a real by app-grid-factory.js with the same column count as the final grid. Because it uses the same wrapper/table base classes, the transition to real data is layout-stable (no jump). - Initial load: skeleton sibling replaces the empty Grid.js container (container is aria-busy while loading). - Re-fetch (gridjs-is-updating after first load): a thin top progress bar + muted tbody signals activity without destroying context. - prefers-reduced-motion: shimmer disabled, static muted bars only. ------------------------------------------------------------------- */ .app-grid-shell { position: relative; } /* Hide Grid.js' built-in loading bar — we own loading UX now. */ .gridjs-loading-bar { display: none; } /* While the skeleton is active, hide everything in the shell except the skeleton itself. Grid.js mounts its own `
` INSIDE the user's container element (which has no class of its own), so targeting `.gridjs-container` directly misses the mount wrapper. Using `:not(.app-grid-skeleton)` catches whatever DOM structure the caller provided as the mount point. `display: none` does not prevent Grid.js from constructing DOM or starting its fetch — only paint. */ .app-grid-shell-loading > :not(.app-grid-skeleton) { display: none !important; } .app-grid-skeleton { /* Skeleton root mirrors the real grid's outer layout: a bordered card wraps the table, and the footer (summary + pagination) sits outside the card — identical to how `.gridjs-wrapper` + `.gridjs-footer` are arranged. This keeps page-level layout stable across the transition. */ display: block; } .app-grid-skeleton-table { border: var(--app-border-width) solid var(--app-table-border-color); border-radius: calc(var(--app-border-radius) * 1.4); background-color: var(--app-background-color); overflow: hidden; } .app-grid-skeleton-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } .app-grid-skeleton-table { width: 100%; border-collapse: collapse; table-layout: auto; } .app-grid-skeleton-cell { padding: var(--app-grid-cell-pad-y, 0.6rem) var(--app-grid-cell-pad-x, 0.9rem); border-bottom: var(--app-border-width) solid var(--app-table-border-color); vertical-align: middle; text-align: left; } .app-grid-skeleton-cell-head { background-color: color-mix( in srgb, var(--app-background-color) 94%, var(--app-muted-border-color) ); } .app-grid-skeleton-row:last-child .app-grid-skeleton-cell { border-bottom: 0; } .app-grid-skeleton-bar { display: block; height: 0.7rem; border-radius: 999px; /* Base track + travelling highlight; the highlight is what animates. */ background-color: color-mix( in srgb, var(--app-muted-border-color) 55%, transparent ); background-image: linear-gradient( 90deg, transparent 0%, color-mix(in srgb, var(--app-primary-focus) 55%, transparent) 50%, transparent 100% ); background-size: 180px 100%; background-repeat: no-repeat; background-position: -180px 0; animation: app-grid-skeleton-shimmer 1.4s linear infinite; } .app-grid-skeleton-cell-head .app-grid-skeleton-bar { height: 0.55rem; background-color: color-mix( in srgb, var(--app-muted-border-color) 70%, transparent ); } /* Footer skeleton — visually matches the real `.gridjs-pagination`: summary (results count) left-aligned, pager buttons right-aligned. */ .app-grid-skeleton-footer { display: flex; flex-wrap: wrap; align-items: center; gap: 0.65rem; padding: 0.85rem 0 0; } .app-grid-skeleton-summary { flex: 0 0 auto; width: clamp(120px, 18%, 200px); } .app-grid-skeleton-summary .app-grid-skeleton-bar { height: 0.55rem; } .app-grid-skeleton-pager { margin-inline-start: auto; display: inline-flex; align-items: center; gap: 0.35rem; } .app-grid-skeleton-pager-button { display: inline-block; min-width: 2rem; height: 1.75rem; border-radius: calc(var(--app-border-radius) * 0.9); background-color: color-mix( in srgb, var(--app-muted-border-color) 55%, transparent ); background-image: linear-gradient( 90deg, transparent 0%, color-mix(in srgb, var(--app-primary-focus) 55%, transparent) 50%, transparent 100% ); background-size: 120px 100%; background-repeat: no-repeat; background-position: -120px 0; animation: app-grid-skeleton-shimmer 1.4s linear infinite; } @media (max-width: 768px) { .app-grid-skeleton-footer { align-items: stretch; } .app-grid-skeleton-pager { margin-inline-start: 0; flex-wrap: wrap; } } @media (prefers-reduced-motion: reduce) { .app-grid-skeleton-pager-button { animation: none; background-image: none; } } /* Re-fetch feedback: thin top progress bar + faded tbody so users see activity without losing the current data context (GitHub pattern). */ .gridjs-container.gridjs-is-updating.gridjs-has-loaded .gridjs-wrapper { position: relative; } .gridjs-container.gridjs-is-updating.gridjs-has-loaded .gridjs-wrapper::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 2px; background: linear-gradient( 90deg, transparent, var(--app-primary), transparent ); background-size: 40% 100%; background-repeat: no-repeat; animation: app-grid-progress-slide 1.1s ease-in-out infinite; z-index: 4; } .gridjs-container.gridjs-is-updating.gridjs-has-loaded .gridjs-tbody { opacity: 0.55; transition: opacity 0.15s ease; } @media (prefers-reduced-motion: reduce) { .app-grid-skeleton-bar, .gridjs-container.gridjs-is-updating.gridjs-has-loaded .gridjs-wrapper::before { animation: none; } .app-grid-skeleton-bar { background-image: none; } } .gridjs-message { padding: 1rem; color: var(--app-muted-color); } .gridjs-message.gridjs-error { color: var(--app-color); border: var(--app-border-width) solid color-mix(in srgb, var(--app-danger, #b44545) 25%, transparent); border-radius: calc(var(--app-border-radius) * 1.1); background: color-mix(in srgb, var(--app-danger, #b44545) 8%, var(--app-background-color)); } .gridjs-message.gridjs-error .app-grid-error-actions { margin-top: 0.6rem; } .gridjs-message.gridjs-error .app-grid-retry { padding: 4px 10px; font-size: 12px; line-height: 1.2; } .gridjs-message.gridjs-notfound { color: var(--app-muted-color); background: color-mix(in srgb, var(--app-muted-border-color) 18%, transparent); border-radius: calc(var(--app-border-radius) * 1.1); } .gridjs-container.gridjs-is-updating .gridjs-message.gridjs-notfound { display: none; } .grid-avatar { --grid-avatar-width: 36px; --grid-avatar-height: 36px; --grid-avatar-radius: 999px; --grid-avatar-fit: cover; --grid-avatar-padding: 0; width: var(--grid-avatar-width); height: var(--grid-avatar-height); border-radius: var(--grid-avatar-radius); border: 1px solid var(--app-muted-border-color); background: var(--app-card-background-color); object-fit: var(--grid-avatar-fit); padding: var(--grid-avatar-padding); display: inline-flex; flex-shrink: 0; box-sizing: border-box; } .grid-avatar-tenant { --grid-avatar-width: 48px; --grid-avatar-height: 28px; --grid-avatar-radius: var(--app-border-radius); --grid-avatar-fit: contain; --grid-avatar-padding: 2px; } .grid-avatar-placeholder { align-items: center; justify-content: center; font-weight: 600; color: var(--app-muted-color); font-size: 11px; background: var(--app-background-color); } .grid-avatar-tenant.grid-avatar-placeholder { font-size: 10px; } .grid-name-cell { display: inline-flex; align-items: center; gap: 0.5rem; min-width: 0; } .grid-name-cell > a:first-child, .grid-name-cell > span:first-child { display: inline-flex; flex-shrink: 0; } .grid-name-cell > :last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; } .grid-tenant-profile { display: inline-flex; align-items: center; gap: 0.75rem; min-width: 0; } .grid-tenant-profile-name-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; } /* Compound user cell: avatar + name (strong) + email subtitle (small, muted). */ .grid-user-profile { display: inline-flex; align-items: center; gap: 0.75rem; min-width: 0; } .grid-user-profile-text { display: inline-flex; flex-direction: column; min-width: 0; line-height: var(--leading-tight); } .grid-user-profile-text strong { font-weight: var(--font-medium); color: var(--app-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .grid-user-profile-text small { font-size: var(--text-xs); color: var(--app-muted-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } a.grid-user-profile.app-grid-link-cell:hover .grid-user-profile-text strong { color: var(--app-primary); text-decoration: underline; } .gridjs-container .grid-actions { display: inline-flex; align-items: center; gap: 0.35rem; white-space: nowrap; } .app-grid-link-cell { color: inherit; text-decoration: none; } .app-grid-link-cell:hover { text-decoration: underline; color: var(--app-primary); } .app-grid-link-cell:focus-visible { outline: 2px solid var(--app-primary-focus); outline-offset: 2px; border-radius: 2px; } .gridjs-table input[data-grid-select-all] { margin: 0; } .gridjs-td .badge, .gridjs-td .grid-name-cell, .gridjs-td .grid-tenant-profile, .gridjs-td .grid-user-profile, .gridjs-td .app-inline-actions { white-space: nowrap; } .gridjs-td .badge-list { align-items: center; white-space: nowrap; } /* Reset button margin inside grid cells (Pico/framework default adds margin-bottom) */ td.gridjs-td button, td.gridjs-td [role="button"] { margin: 0; } /* Inline action buttons side by side in a cell */ td.gridjs-td .app-inline-actions { display: inline-flex; gap: 0.35rem; align-items: center; } html[data-theme="dark"] button.gridjs-sort, html[data-theme="dark"] button.gridjs-sort-neutral { filter: invert(1); } button.gridjs-sort { background-color: transparent; background-position-x: center; background-repeat: no-repeat; background-size: contain; border: none; cursor: pointer; float: right; height: 24px; margin: 0; outline: none; padding: 0; width: 13px; } @media (max-width: 768px) { .gridjs-wrapper { border-radius: var(--app-border-radius); } .gridjs-pagination { align-items: stretch; } .gridjs-pagination .gridjs-pages { max-width: 100%; flex-wrap: wrap; margin-inline-start: 0; } } @keyframes app-grid-skeleton-shimmer { 0% { background-position: -180px 0; } 100% { background-position: calc(100% + 180px) 0; } } @keyframes app-grid-progress-slide { 0% { background-position: -40% 0; } 100% { background-position: 140% 0; } } }