mitgliederverwaltung/assets/css/app.css
Simon 8303a39041 feat(overview): rework filter builder with editable chips and native date ranges
Editable filter chips, uniformly-sized value controls, working field-picker
search, and native date-range inputs with relative presets in place of the
vendored calendar — native inputs give year-jump, manual entry and keyboard
accessibility for free.
2026-07-10 16:27:15 +02:00

974 lines
34 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* See the Tailwind configuration guide for advanced usage
https://tailwindcss.com/docs/configuration */
@import "tailwindcss";
@source "../css";
@source "../js";
@source "../../lib/mv_web";
/* A Tailwind plugin that makes "hero-#{ICON}" classes available.
The heroicons installation itself is managed by your mix.exs */
@plugin "../vendor/heroicons";
/* daisyUI Tailwind Plugin. You can update this file by fetching the latest version with:
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.js
Make sure to look at the daisyUI changelog: https://daisyui.com/docs/changelog/ */
@plugin "../vendor/daisyui" {
themes: false;
}
/* daisyUI theme plugin. You can update this file by fetching the latest version with:
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.js
We ship with two themes, a light one inspired on Phoenix colors and a dark one inspired
on Elixir colors. Build your own at: https://daisyui.com/theme-generator/ */
@plugin "../vendor/daisyui-theme" {
name: "dark";
default: false;
prefersdark: false;
color-scheme: "dark";
--color-base-100: oklch(30.33% 0.016 252.42);
--color-base-200: oklch(25.26% 0.014 253.1);
--color-base-300: oklch(20.15% 0.012 254.09);
--color-base-content: oklch(97.807% 0.029 256.847);
--color-primary: oklch(58% 0.233 277.117);
--color-primary-content: oklch(96% 0.018 272.314);
--color-secondary: oklch(58% 0.233 277.117);
--color-secondary-content: oklch(96% 0.018 272.314);
--color-accent: oklch(60% 0.25 292.717);
--color-accent-content: oklch(96% 0.016 293.756);
--color-neutral: oklch(37% 0.044 257.287);
--color-neutral-content: oklch(98% 0.003 247.858);
--color-info: oklch(58% 0.158 241.966);
--color-info-content: oklch(97% 0.013 236.62);
--color-success: oklch(60% 0.118 184.704);
--color-success-content: oklch(98% 0.014 180.72);
--color-warning: oklch(66% 0.179 58.318);
--color-warning-content: oklch(98% 0.022 95.277);
--color-error: oklch(58% 0.253 17.585);
--color-error-content: oklch(96% 0.015 12.422);
--radius-selector: 0.25rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.21875rem;
--size-field: 0.21875rem;
--border: 1.5px;
--depth: 1;
--noise: 0;
}
@plugin "../vendor/daisyui-theme" {
name: "light";
default: true;
prefersdark: false;
color-scheme: "light";
--color-base-100: oklch(98% 0 0);
--color-base-200: oklch(96% 0.001 286.375);
--color-base-300: oklch(92% 0.004 286.32);
--color-base-content: oklch(21% 0.006 285.885);
--color-primary: oklch(70% 0.213 47.604);
--color-primary-content: oklch(98% 0.016 73.684);
--color-secondary: oklch(55% 0.027 264.364);
--color-secondary-content: oklch(98% 0.002 247.839);
--color-accent: oklch(0% 0 0);
--color-accent-content: oklch(100% 0 0);
--color-neutral: oklch(44% 0.017 285.786);
--color-neutral-content: oklch(98% 0 0);
--color-info: oklch(62% 0.214 259.815);
--color-info-content: oklch(97% 0.014 254.604);
--color-success: oklch(70% 0.14 182.503);
--color-success-content: oklch(98% 0.014 180.72);
--color-warning: oklch(66% 0.179 58.318);
--color-warning-content: oklch(98% 0.022 95.277);
--color-error: oklch(58% 0.253 17.585);
--color-error-content: oklch(96% 0.015 12.422);
--radius-selector: 0.25rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.21875rem;
--size-field: 0.21875rem;
--border: 1.5px;
--depth: 1;
--noise: 0;
}
/* Add variants based on LiveView classes */
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
/* Make LiveView wrapper divs transparent for layout */
[data-phx-session] { display: contents }
/* Honeypot: off-screen and minimal size so bots fill it, humans never see it (best practice) */
.join-form-helper {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.join-form-helper .join-form-helper-input {
position: absolute;
left: -9999px;
}
/* WCAG 1.4.12 Text Spacing: allow user stylesheets to adjust text spacing in popovers.
Popover content (e.g. from DaisyUI dropdown) must not rely on non-overridable inline
spacing; use inherited values so custom stylesheets can override. */
[popover] {
line-height: inherit;
letter-spacing: inherit;
word-spacing: inherit;
}
/* WCAG 2 AA: success/error/warning text. Light theme: dark tones on light bg; dark theme: light tones on dark bg. */
.text-success-aa {
color: oklch(0.35 0.12 165);
}
.text-error-aa {
color: oklch(0.45 0.2 25);
}
.text-warning-aa {
color: oklch(0.45 0.14 75);
}
[data-theme="dark"] .text-success-aa {
color: oklch(0.72 0.12 165);
}
[data-theme="dark"] .text-error-aa {
color: oklch(0.75 0.18 25);
}
[data-theme="dark"] .text-warning-aa {
color: oklch(0.78 0.14 75);
}
/* WCAG 2.2 AA: Badge contrast. DaisyUI .badge-outline uses transparent bg; we use
Core Component <.badge style="outline"> which adds .bg-base-100. This rule ensures
outline badges always have a visible background in both themes. */
[data-theme="light"] .badge.badge-outline,
[data-theme="dark"] .badge.badge-outline {
background-color: var(--color-base-100);
}
/* WCAG 2.2 AA (4.5:1 for normal text): Form labels. DaisyUI .label uses 60% opacity,
which fails contrast. Override to 85% of base-content so labels stay slightly
deemphasised vs body text but meet the minimum ratio. Match .label directly
so the override applies even when data-theme is not yet set (e.g. initial load). */
.label {
color: color-mix(in oklab, var(--color-base-content) 85%, transparent);
}
/* WCAG 2.2 AA (4.5:1 for normal text): Badge text must contrast with badge background.
Theme tokens *-content are often too light on * backgrounds in light theme, and
badge-soft uses variant as text on a light tint (low contrast). We override
--badge-fg (and for soft, color) so badge text meets 4.5:1 in both themes. */
/* Light theme: use dark text on all colored badges (solid, soft, outline). */
[data-theme="light"] .badge.badge-primary {
--badge-fg: oklch(0.25 0.08 47);
}
[data-theme="light"] .badge.badge-primary.badge-soft {
color: oklch(0.38 0.14 47);
}
[data-theme="light"] .badge.badge-success {
--badge-fg: oklch(0.26 0.06 165);
}
[data-theme="light"] .badge.badge-success.badge-soft {
color: oklch(0.35 0.10 165);
}
[data-theme="light"] .badge.badge-error {
--badge-fg: oklch(0.22 0.08 25);
}
[data-theme="light"] .badge.badge-error.badge-soft {
color: oklch(0.38 0.14 25);
}
[data-theme="light"] .badge.badge-warning {
--badge-fg: oklch(0.28 0.06 75);
}
[data-theme="light"] .badge.badge-warning.badge-soft {
color: oklch(0.42 0.12 75);
}
[data-theme="light"] .badge.badge-info {
--badge-fg: oklch(0.26 0.08 250);
}
[data-theme="light"] .badge.badge-info.badge-soft {
color: oklch(0.38 0.12 250);
}
[data-theme="light"] .badge.badge-neutral {
--badge-fg: oklch(0.22 0.01 285);
}
[data-theme="light"] .badge.badge-neutral.badge-soft {
color: oklch(0.32 0.02 285);
}
[data-theme="light"] .badge.badge-outline.badge-primary,
[data-theme="light"] .badge.badge-outline.badge-success,
[data-theme="light"] .badge.badge-outline.badge-error,
[data-theme="light"] .badge.badge-outline.badge-warning,
[data-theme="light"] .badge.badge-outline.badge-info,
[data-theme="light"] .badge.badge-outline.badge-neutral {
--badge-fg: oklch(0.25 0.02 285);
}
/* Dark theme: ensure badge backgrounds are dark enough for light content (4.5:1).
Slightly darken solid variant backgrounds so theme *-content (light) passes. */
[data-theme="dark"] .badge.badge-primary:not(.badge-soft):not(.badge-outline) {
--badge-bg: oklch(0.42 0.20 277);
--badge-fg: oklch(0.97 0.02 277);
}
[data-theme="dark"] .badge.badge-success:not(.badge-soft):not(.badge-outline) {
--badge-bg: oklch(0.42 0.10 185);
--badge-fg: oklch(0.97 0.01 185);
}
[data-theme="dark"] .badge.badge-error:not(.badge-soft):not(.badge-outline) {
--badge-bg: oklch(0.42 0.18 18);
--badge-fg: oklch(0.97 0.02 18);
}
[data-theme="dark"] .badge.badge-warning:not(.badge-soft):not(.badge-outline) {
--badge-bg: oklch(0.48 0.14 58);
--badge-fg: oklch(0.22 0.02 58);
}
[data-theme="dark"] .badge.badge-info:not(.badge-soft):not(.badge-outline) {
--badge-bg: oklch(0.45 0.14 242);
--badge-fg: oklch(0.97 0.02 242);
}
[data-theme="dark"] .badge.badge-neutral:not(.badge-soft):not(.badge-outline) {
--badge-bg: oklch(0.32 0.02 257);
--badge-fg: oklch(0.96 0.01 257);
}
[data-theme="dark"] .badge.badge-soft.badge-primary { color: oklch(0.85 0.12 277); }
[data-theme="dark"] .badge.badge-soft.badge-success { color: oklch(0.82 0.08 165); }
[data-theme="dark"] .badge.badge-soft.badge-error { color: oklch(0.82 0.14 25); }
[data-theme="dark"] .badge.badge-soft.badge-warning { color: oklch(0.88 0.10 75); }
[data-theme="dark"] .badge.badge-soft.badge-info { color: oklch(0.85 0.10 250); }
[data-theme="dark"] .badge.badge-soft.badge-neutral { color: oklch(0.90 0.01 257); }
[data-theme="dark"] .badge.badge-outline.badge-primary,
[data-theme="dark"] .badge.badge-outline.badge-success,
[data-theme="dark"] .badge.badge-outline.badge-error,
[data-theme="dark"] .badge.badge-outline.badge-warning,
[data-theme="dark"] .badge.badge-outline.badge-info,
[data-theme="dark"] .badge.badge-outline.badge-neutral {
--badge-fg: oklch(0.92 0.02 257);
}
/* WCAG 2.2 AA: Member filter join buttons (All / Paid / Unpaid, group, boolean).
Inactive state uses base-content on a light/dark surface; active state ensures
*-content on * background meets 4.5:1. */
.member-filter-dropdown .join .btn {
/* Inactive: ensure readable text (theme base-content may be low contrast on btn default) */
border-color: var(--color-base-300);
}
[data-theme="light"] .member-filter-dropdown .join .btn:not(.btn-active) {
color: oklch(0.25 0.02 285);
background-color: var(--color-base-100);
}
[data-theme="light"] .member-filter-dropdown .join .btn.btn-success.btn-active {
background-color: oklch(0.42 0.12 165);
color: oklch(0.98 0.01 165);
}
[data-theme="light"] .member-filter-dropdown .join .btn.btn-error.btn-active {
background-color: oklch(0.42 0.18 18);
color: oklch(0.98 0.02 18);
}
[data-theme="dark"] .member-filter-dropdown .join .btn:not(.btn-active) {
color: oklch(0.92 0.02 257);
background-color: var(--color-base-200);
}
[data-theme="dark"] .member-filter-dropdown .join .btn.btn-success.btn-active {
background-color: oklch(0.42 0.10 165);
color: oklch(0.97 0.01 165);
}
[data-theme="dark"] .member-filter-dropdown .join .btn.btn-error.btn-active {
background-color: oklch(0.42 0.18 18);
color: oklch(0.97 0.02 18);
}
/* ============================================
Sidebar Base Styles
============================================ */
/* Desktop Sidebar Base */
.sidebar {
@apply flex flex-col bg-base-200 min-h-screen;
@apply transition-[width] duration-300 ease-in-out;
@apply relative;
width: 16rem; /* Expanded: w-64 */
z-index: 40;
}
/* Collapsed State */
[data-sidebar-expanded="false"] .sidebar {
width: 4rem; /* Collapsed: w-16 */
}
/* ============================================
Header - Logo Centering
============================================ */
/* Header container with smooth transition for gap */
.sidebar > div:first-child {
@apply transition-all duration-300;
}
/* ============================================
Text Labels - Hide in Collapsed State
============================================ */
.menu-label {
@apply transition-all duration-200 whitespace-nowrap;
transition-delay: 0ms; /* Expanded: sofort sichtbar */
}
[data-sidebar-expanded="false"] .sidebar .menu-label {
@apply opacity-0 w-0 overflow-hidden pointer-events-none;
transition-delay: 300ms; /* Warte bis Sidebar eingeklappt ist (300ms = duration der Sidebar width transition) */
}
/* ============================================
Toggle Button Icon Swap
============================================ */
.sidebar-collapsed-icon {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar .sidebar-expanded-icon {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar .sidebar-collapsed-icon {
@apply block;
}
/* ============================================
Menu Groups - Show/Hide Based on State
============================================ */
.expanded-menu-group {
@apply block;
}
.collapsed-menu-group {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar .expanded-menu-group {
@apply hidden;
}
[data-sidebar-expanded="false"] .sidebar .collapsed-menu-group {
@apply block;
}
/* Collapsed menu group button: center icon under logo */
.sidebar .collapsed-menu-group button {
padding-left: 14px;
}
/* ============================================
Menu Groups - Disable hover and active on expanded-menu-group header
============================================ */
/* Disable all interactive effects on expanded-menu-group header (no href, not clickable)
Using [role="group"] to increase specificity and avoid !important */
.sidebar .menu > li.expanded-menu-group > div[role="group"]:not(a) {
pointer-events: none;
cursor: default;
}
/* Higher specificity selector to override DaisyUI menu hover styles
DaisyUI uses :where() which has 0 specificity, but the compiled CSS might have higher specificity
Using [role="group"] attribute selector increases specificity without !important */
.sidebar .menu > li.expanded-menu-group > div[role="group"]:not(a):hover,
.sidebar .menu > li.expanded-menu-group > div[role="group"]:not(a):active,
.sidebar .menu > li.expanded-menu-group > div[role="group"]:not(a):focus {
background-color: transparent;
box-shadow: none;
cursor: default;
color: inherit;
}
/* ============================================
Elements Only Visible in Expanded State
============================================ */
.expanded-only {
@apply block transition-opacity duration-200;
}
[data-sidebar-expanded="false"] .sidebar .expanded-only {
@apply hidden;
}
/* ============================================
Tooltip - Only Show in Collapsed State
============================================ */
.sidebar .tooltip::before,
.sidebar .tooltip::after {
@apply opacity-0 pointer-events-none;
}
[data-sidebar-expanded="false"] .sidebar .tooltip:hover::before,
[data-sidebar-expanded="false"] .sidebar .tooltip:hover::after {
@apply opacity-100;
}
/* ============================================
Menu Item Alignment - Icons Centered Under Logo
============================================ */
/* Base alignment: Icons centered under logo (32px from left edge)
- Logo center: 16px padding + 16px (half of 32px) = 32px
- Icon center should be at 32px: 22px start + 10px (half of 20px) = 32px
- Menu has p-2 (8px), so links need 14px additional padding-left */
.sidebar .menu > li > a,
.sidebar .menu > li > button,
.sidebar .menu > li.expanded-menu-group > div,
.sidebar .menu > div.collapsed-menu-group > button {
@apply transition-all duration-300;
padding-left: 14px;
}
/* Collapsed state: same padding to keep icons at same position
- Remove gap so label (which is opacity-0 w-0) doesn't create space
- Keep padding-left at 14px so icons stay centered under logo */
[data-sidebar-expanded="false"] .sidebar .menu > li > a,
[data-sidebar-expanded="false"] .sidebar .menu > li > button,
[data-sidebar-expanded="false"] .sidebar .menu > li.expanded-menu-group > div,
[data-sidebar-expanded="false"] .sidebar .menu > div.collapsed-menu-group > button {
@apply gap-0;
padding-left: 14px;
padding-right: 14px; /* Center icon horizontally in 64px sidebar */
}
/* ============================================
Footer Button Alignment - Left Aligned in Collapsed State
============================================ */
[data-sidebar-expanded="false"] .sidebar .dropdown > button {
@apply px-0;
/* Buttons stay at left position, only label disappears */
}
/* ============================================
User Menu Button - Focus Ring on Avatar
============================================ */
/* Focus ring appears on the avatar when button is focused */
.user-menu-button:focus .avatar > div {
@apply ring-2 ring-primary ring-offset-2 ring-offset-base-200;
}
/* ============================================
User Menu Button - Smooth Centering Transition
============================================ */
/* User menu button transitions smoothly to center */
.user-menu-button {
@apply transition-all duration-300;
}
/* In collapsed state, center avatar under logo
- Avatar is 32px (w-8), center it in 64px sidebar
- (64px - 32px) / 2 = 16px padding → avatar center at 32px (same as logo center) */
[data-sidebar-expanded="false"] .sidebar .user-menu-button {
@apply gap-0;
padding-left: 16px;
padding-right: 16px;
justify-content: center;
}
/* ============================================
User Menu Button - Hover Ring on Avatar
============================================ */
/* Smooth transition for avatar ring effects */
.user-menu-button .avatar > div {
@apply transition-all duration-200;
}
/* Hover ring appears on the avatar when button is hovered */
.user-menu-button:hover .avatar > div {
@apply ring-1 ring-neutral ring-offset-1 ring-offset-base-200;
}
/* ============================================
Mobile Drawer Width
============================================ */
/* Auf Mobile (< 1024px) ist die Sidebar immer w-64 (16rem) wenn geöffnet */
@media (max-width: 1023px) {
.drawer-side .sidebar {
width: 16rem; /* w-64 auch auf Mobile */
}
}
/* ============================================
Drawer Side Overflow Fix für Desktop
============================================ */
/* Im Desktop-Modus (lg:drawer-open) overflow auf visible setzen
damit Dropdowns und Tooltips über Main Content erscheinen können */
@media (min-width: 1024px) {
.drawer.lg\:drawer-open .drawer-side {
overflow: visible !important;
overflow-x: visible !important;
overflow-y: visible !important;
}
}
/* ============================================
Collapsed Sidebar: User Menu Dropdown Richtung
============================================ */
/* Bei eingeklappter Sidebar liegt der Avatar-Button am linken Rand.
dropdown-end würde das Menü nach links öffnen (off-screen).
Stattdessen nach rechts öffnen (in den Content-Bereich). */
#app-layout[data-sidebar-expanded="false"] .dropdown.dropdown-top > ul.dropdown-content {
right: auto !important;
left: 0 !important;
}
/* Sign-in: hide SSO button and "or" divider when OIDC is not configured.
Scoped to #sign-in-page to avoid hiding unrelated elements. */
#sign-in-page[data-oidc-configured="false"] [id*="oidc"] {
display: none !important;
}
#sign-in-page[data-oidc-configured="false"] a[href*="oidc"] {
display: none !important;
}
#sign-in-page[data-oidc-configured="false"] .divider {
display: none !important;
}
/* Sign-in: when OIDC-only mode is on, hide password form and "or" divider (show only SSO). */
#sign-in-page[data-oidc-configured="true"][data-oidc-only="true"] [id*="password"] {
display: none !important;
}
#sign-in-page[data-oidc-configured="true"][data-oidc-only="true"] .divider {
display: none !important;
}
/* ============================================
WCAG 1.4.3: Primary button contrast (AA)
============================================ */
/* Override DaisyUI theme --color-primary-content so text on btn-primary (brand)
meets 4.5:1. In DevTools: inspect .btn-primary, check computed --color-primary
and --color-primary-content; verify contrast at https://webaim.org/resources/contrastchecker/ */
/* Light theme: primary is orange (brand); primary-content must be dark. */
[data-theme="light"] {
--color-primary-content: oklch(0.18 0.02 47);
--color-error: oklch(55% 0.253 17.585);
--color-error-content: oklch(98% 0 0);
}
/* Dark theme: primary is purple; ensure content is light and meets 4.5:1. */
[data-theme="dark"] {
--color-error: oklch(55% 0.253 17.585);
--color-error-content: oklch(98% 0 0);
--color-primary: oklch(72% 0.17 45);
--color-primary-content: oklch(0.18 0.02 47);
--color-secondary: oklch(48% 0.233 277.117);
--color-secondary-content: oklch(98% 0 0);
}
/* ============================================
WCAG 2.2 AA: Tab list inactive tab text contrast (4.5:1)
============================================ */
#member-tablist .tab:not(.tab-active) {
color: oklch(0.35 0.02 285);
}
[data-theme="dark"] #member-tablist .tab:not(.tab-active) {
color: oklch(0.72 0.02 257);
}
/* ============================================
WCAG 2.2 AA: Link contrast - primary and accent
============================================ */
[data-theme="light"] .link.link-primary {
color: oklch(0.45 0.15 35);
}
[data-theme="light"] .link.link-primary:hover {
color: oklch(0.38 0.14 35);
}
[data-theme="dark"] .link.link-primary {
color: oklch(0.82 0.14 45);
}
[data-theme="dark"] .link.link-primary:hover {
color: oklch(0.88 0.12 45);
}
[data-theme="dark"] .link.link-accent {
color: oklch(0.82 0.18 292);
}
[data-theme="dark"] .link.link-accent:hover {
color: oklch(0.88 0.16 292);
}
/* ============================================
WCAG 2.2 AA: Danger zone heading contrast (dark theme)
============================================ */
[data-theme="dark"] #danger-zone-heading.text-error {
color: oklch(0.78 0.18 25);
}
/* ============================================
WCAG 2.2 AA: Blue link contrast in dark theme
============================================ */
[data-theme="dark"] a.text-blue-700,
[data-theme="dark"] a.text-blue-600,
[data-theme="dark"] a.hover\:text-blue-800 {
color: oklch(0.72 0.16 255);
}
[data-theme="dark"] a.text-blue-700:hover,
[data-theme="dark"] a.text-blue-600:hover {
color: oklch(0.82 0.14 255);
}
/* ============================================
WCAG 2.2 AA: Password / form label on light box in dark theme
============================================ */
[data-theme="dark"] .bg-gray-50 {
background-color: var(--color-base-200);
color: var(--color-base-content);
}
[data-theme="dark"] .bg-gray-50 .label,
[data-theme="dark"] .bg-gray-50 .mb-1.label,
[data-theme="dark"] .bg-gray-50 .text-gray-600,
[data-theme="dark"] .bg-gray-50 .text-gray-700,
[data-theme="dark"] .bg-gray-50 strong,
[data-theme="dark"] .bg-gray-50 p,
[data-theme="dark"] .bg-gray-50 li {
color: var(--color-base-content);
}
/* Dark mode: orange/red info boxes (admin note, OIDC warning) dark bg, light text */
[data-theme="dark"] .bg-orange-50 {
background-color: oklch(0.32 0.06 55);
border-color: oklch(0.42 0.08 55);
color: var(--color-base-content);
}
[data-theme="dark"] .bg-orange-50 .text-orange-800,
[data-theme="dark"] .bg-orange-50 p,
[data-theme="dark"] .bg-orange-50 strong {
color: var(--color-base-content);
}
[data-theme="dark"] .bg-red-50 {
background-color: oklch(0.32 0.08 25);
border-color: oklch(0.42 0.12 25);
color: var(--color-base-content);
}
[data-theme="dark"] .bg-red-50 .text-red-800,
[data-theme="dark"] .bg-red-50 .text-red-700,
[data-theme="dark"] .bg-red-50 p,
[data-theme="dark"] .bg-red-50 strong {
color: var(--color-base-content);
}
/* This file is for your main application CSS */
/* ============================================
Table header density scaling
============================================ */
/*
* DaisyUI table size modifiers (table-xs, table-sm, etc.) scope their font-size
* rule to ":not(thead, tfoot) tr", so the header row stays at the base 0.875 rem
* regardless of the chosen density. We extend each modifier to thead so the
* header scales together with the body.
*/
.table.table-xs thead tr { font-size: 0.6875rem; }
.table.table-sm thead tr { font-size: 0.75rem; }
.table.table-lg thead tr { font-size: 1.125rem; }
.table.table-xl thead tr { font-size: 1.375rem; }
/*
* Density-tracking badges and sort icons in the member overview.
*
* The member table renders its rows via LiveView streams, so toggling density
* only swaps the <table> size class (table-xs <-> table-md); it does NOT
* re-render the streamed rows. A per-row `size=` conditional therefore bakes a
* stale badge size into the streamed DOM. To make badges (and, for visual
* consistency, the header sort glyphs) follow the density toggle without any
* row re-render, their size is driven purely from the table size class here.
*
* Badge values mirror daisyUI's own .badge-xs / .badge-md (font-size, --size
* which the base .badge uses for height, and padding-inline). Scoped to the
* member overview via #members-keyboard so unrelated badges are unaffected.
*/
#members-keyboard .table.table-xs .badge {
--size: calc(var(--size-selector, 0.25rem) * 4);
font-size: 0.625rem;
padding-inline: calc(0.25rem * 2 - var(--border));
}
#members-keyboard .table.table-md .badge {
--size: calc(var(--size-selector, 0.25rem) * 6);
font-size: 0.875rem;
padding-inline: calc(0.25rem * 3 - var(--border));
}
/* Sort glyphs scale with the header text (11px compact / 14px comfortable). */
#members-keyboard .table.table-md thead .sort-icon {
width: 1.5rem;
height: 1.5rem;
}
#members-keyboard .table.table-xs thead .sort-icon {
width: 1.125rem;
height: 1.125rem;
}
/*
* Infinite-scroll loading row.
*
* The loading indicator must read like a full, accented data row rather than a
* narrow pinned bar. The accent (base-200 background + a top separator) and the
* padding live on the full-width footer cell, which spans every column via
* colspan, so the accent is visible edge-to-edge even while the table is scrolled
* horizontally. The spinner + label inside stay pinned to the visible viewport
* width (StickyViewportWidth hook) and transparent, so they remain centered on
* horizontal scroll while the cell accent shows through behind them.
*
* The row height tracks the active density (table-xs compact / table-md
* comfortable) so it matches a normal data row in each mode.
*/
#members-footer > tr > td {
background-color: var(--color-base-200);
border-top: 1px solid var(--color-base-300);
padding: 0;
}
.table.table-xs #members-loading-bar { min-height: 3.375rem; }
.table.table-md #members-loading-bar { min-height: 4.1875rem; }
/* ============================================
SortableList: drag-and-drop table rows
============================================ */
/* Ghost row: placeholder showing where the dragged item will be dropped.
Background fills the gap; text invisible so layout matches original row. */
.sortable-ghost {
background-color: var(--color-base-300) !important;
opacity: 0.5;
}
.sortable-ghost td {
border-color: transparent !important;
}
/* Chosen row: the row being actively dragged (follows the cursor). */
.sortable-chosen {
background-color: var(--color-base-200);
box-shadow: 0 4px 16px -2px oklch(0 0 0 / 0.18);
cursor: grabbing !important;
}
/* Drag handle button: only grab cursor, no hover effect for mouse users.
Keyboard outline is handled via JS outline style. */
[data-sortable-handle] button {
cursor: grab;
}
[data-sortable-handle] button:hover {
background-color: transparent !important;
color: inherit;
}
/*
* Default interactive table rows: neutral hover/focus-visible fill for clickable rows.
* Uses :has(:focus-visible) so keyboard navigation highlights the row without sticky mouse-focus artifacts.
*/
.table.table-zebra tbody tr[data-row-interactive="true"]:is(:hover, :has(:focus-visible)) > td {
background-color: var(--color-base-300);
}
/*
* Sticky first column in zebra tables: opaque backgrounds per row.
* Use nth-child (not HEEx row index) so LiveStream rows stay iterable only via :for (Phoenix LV requirement).
*/
[data-sticky-first-col-rows="true"] .table.table-zebra tbody tr:nth-child(odd) > td.sticky-first-col-cell {
background-color: var(--color-base-100);
}
[data-sticky-first-col-rows="true"] .table.table-zebra tbody tr:nth-child(even) > td.sticky-first-col-cell {
background-color: var(--color-base-200);
}
/*
* Checkbox-selected rows: keep zebra backgrounds; only accent the sticky checkbox column.
*/
[data-sticky-first-col-rows="true"]
.table.table-zebra
tbody
tr[data-selected="true"]
> td.sticky-first-col-cell {
box-shadow: inset 2px 0 0 var(--color-primary);
}
[data-sticky-first-col-rows="true"]
.table.table-zebra
tbody
tr[data-row-interactive="true"]:is(:hover, :has(:focus-visible))
> td.sticky-first-col-cell {
background-color: var(--color-base-300);
/* Left accent only; keep the familiar orange primary accent. */
box-shadow: inset 2px 0 0 var(--color-primary);
}
/*
* Sticky member selection table: drop mouse-only focus outlines that read like a thin frame around the row;
* keyboard :focus-visible keeps DaisyUI control outlines (checkbox / tabindex cell).
*/
[data-sticky-first-col-rows="true"] .table.table-zebra tbody tr {
outline: none;
}
[data-sticky-first-col-rows="true"]
.table.table-zebra
tbody
tr[data-row-interactive="true"]:is(:hover, :has(:focus-visible)):not(:last-child) {
/* DaisyUI draws a bottom border on each row; hiding it while highlighted avoids a boxy “frame”. */
border-bottom-color: transparent;
}
[data-sticky-first-col-rows="true"] .table.table-zebra tbody tr td:focus:not(:focus-visible) {
outline: none;
}
[data-sticky-first-col-rows="true"] .table.table-zebra tbody tr input.checkbox:focus:not(:focus-visible) {
outline: none;
}
/*
* Sticky checkbox column: border-collapse: separate avoids the Chromium artifact
* where a collapsed row border is repainted (a spurious 1px line) under a
* position: sticky cell. The row separators are moved onto the cells so they
* still render in the separate model (matching DaisyUI's faint divider color).
*/
[data-sticky-first-col-rows="true"] .table.table-zebra {
border-collapse: separate;
border-spacing: 0;
}
[data-sticky-first-col-rows="true"]
.table.table-zebra
:where(thead tr, tbody tr:not(:last-child))
> :where(th, td) {
border-bottom: var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000);
}
/*
* Horizontal-scroll fade for the pinned checkbox column: instead of hiding the
* scrolled-away content behind a hard opaque block, a gradient on the right edge
* of the sticky cell fades the incoming content, signalling there is more behind
* it. overflow: visible lets the ::after gradient extend past the cell; the cell
* keeps its own (zebra) background so the checkbox stays legible.
*/
[data-sticky-first-col-rows="true"] .table.table-zebra td.sticky-first-col-cell {
overflow: visible;
}
[data-sticky-first-col-rows="true"] .table.table-zebra td.sticky-first-col-cell::after,
[data-sticky-first-col-rows="true"] .table.table-zebra thead th.sticky-first-col-th::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 100%;
width: 1.25rem;
pointer-events: none;
}
[data-sticky-first-col-rows="true"]
.table.table-zebra
tbody
tr:nth-child(odd)
> td.sticky-first-col-cell::after {
background: linear-gradient(to right, var(--color-base-100), transparent);
}
[data-sticky-first-col-rows="true"]
.table.table-zebra
tbody
tr:nth-child(even)
> td.sticky-first-col-cell::after {
background: linear-gradient(to right, var(--color-base-200), transparent);
}
[data-sticky-first-col-rows="true"]
.table.table-zebra
tbody
tr[data-row-interactive="true"]:is(:hover, :has(:focus-visible))
> td.sticky-first-col-cell::after {
background: linear-gradient(to right, var(--color-base-300), transparent);
}
[data-sticky-first-col-rows="true"] .table.table-zebra thead th.sticky-first-col-th::after {
background: linear-gradient(to right, var(--color-base-100), transparent);
}
/* Compact density: narrower fade so the gradient does not visually reach into the
checkbox column's own content area in the tighter table-xs cell. */
[data-sticky-first-col-rows="true"] .table.table-zebra.table-xs td.sticky-first-col-cell::after,
[data-sticky-first-col-rows="true"] .table.table-zebra.table-xs thead th.sticky-first-col-th::after {
width: 0.5rem;
}
/*
* Sort-header tooltip. Rendered via the native Popover API (see the SortTooltip
* JS hook), so it lives in the browser TOP LAYER: it is not clipped by the
* members table's overflow (overflow-x:auto forces overflow-y:auto, which used
* to clip a CSS pseudo-tooltip below the header) and paints above the sticky
* header, neighbor cells, and the pinned checkbox column's fade — without any
* z-index hacks. This block only resets the popover UA defaults and gives it a
* small daisyUI-flavoured tooltip look; the JS hook sets top/left each show.
*/
.sort-tooltip {
position: fixed;
inset: auto;
margin: 0;
border: 0;
padding: 0.25rem 0.5rem;
width: max-content;
max-width: 16rem;
overflow: visible;
background: var(--color-neutral, oklch(0.2 0 0));
color: var(--color-neutral-content, oklch(0.98 0 0));
font-size: 0.75rem;
line-height: 1rem;
border-radius: 0.25rem;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.3);
pointer-events: none;
}
/*
* Payment-badge tooltip. A native Popover-API element (opened by the
* HoverPopover hook), so it lives in the browser TOP LAYER and is not clipped by
* the members table's overflow. Placement is pure CSS anchor positioning: the
* badge carries `anchor-name`, this popover carries `position-anchor`, and
* `position-area` drops it just below the badge, left-aligned. A `position-try`
* fallback flips it above the badge when there is no room below. No JS
* coordinate math is involved (unlike the older SortTooltip).
*/
.payment-tip {
margin: 0;
position-area: bottom span-right;
position-try-fallbacks: flip-block;
}
/*
* Vertically center the row checkbox in the sticky first column using the
* table-native vertical-align: middle. This keeps display: table-cell intact so
* the cell background fills the full row height (no white gap at the bottom).
* The fieldset margin-bottom reset removes the mb-2 from the shared input
* component so no residual margin shifts the checkbox off-center.
*/
[data-sticky-first-col-rows="true"] .table.table-zebra td.sticky-first-col-cell,
[data-sticky-first-col-rows="true"] .table.table-zebra thead th.sticky-first-col-th {
vertical-align: middle;
}
[data-sticky-first-col-rows="true"] .table.table-zebra td.sticky-first-col-cell fieldset,
[data-sticky-first-col-rows="true"] .table.table-zebra thead th.sticky-first-col-th fieldset {
margin-bottom: 0;
}