/* ---- App shell: sidebar + routed main content - see Shell.ts. ---- */ .appShell { display: flex; min-height: 100vh; } .sidebar { width: 220px; flex-shrink: 0; background: var(--bg-header); padding: 20px 12px; display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh; overflow-y: auto; } .sidebarBrand { font-size: 18px; font-weight: 700; padding: 0 12px 20px; } .sidebarNav { display: flex; flex-direction: column; gap: 2px; flex: 1; } .navSectionLabel { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .05em; padding: 16px 12px 6px; } .navItem { display: block; padding: 10px 12px; border-radius: 8px; color: var(--text); text-decoration: none; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .navItem:hover { background: rgba(255, 255, 255, .06); } .navItem.active { background: var(--accent); color: white; font-weight: 600; } .sidebarLogout { margin-top: 12px; padding: 8px 12px; } .mainContent { flex: 1; min-width: 0; padding: 24px clamp(16px, 3vw, 48px) 80px; } /* ==== Mobile navigation (<=720px) ==== Replaces the sidebar outright - see docs/architecture.md's "Mobile navigation" section for why a bottom sheet was chosen over reshaping the sidebar into a horizontal bar. Every rule below the base `display: none` only ever applies once the 720px media query turns it back on, so none of this affects desktop. */ .mobileTopBar, .mobileNavTrigger, .mobileNavScrim, .mobileNavSheet { display: none; } .mobileTopBar { position: sticky; top: 0; z-index: 3; padding: 14px 16px; background: var(--bg-header); } .mobileBrand { font-size: 16px; font-weight: 700; } /* position:fixed, not sticky - #musicBar below already relies on a fixed wrapper to stay docked regardless of how far #mainContent's own (whole-page, not internally-scrolled) content runs. */ .bottomStack { position: fixed; left: 0; right: 0; bottom: 0; z-index: 15; display: flex; flex-direction: column; } .mobileNavTrigger { align-items: center; justify-content: center; gap: 8px; background: var(--bg-header); border: none; border-top: 1px solid rgba(255, 255, 255, .08); color: var(--text); font-family: inherit; font-size: 13px; font-weight: 600; padding: 13px; padding-bottom: calc(13px + env(safe-area-inset-bottom, 0px)); } .mobileNavScrim { position: fixed; inset: 0; z-index: 20; background: rgba(0, 0, 0, .5); opacity: 0; pointer-events: none; transition: opacity .2s; /* A touch starting here has no scrollable content of its own to "contain" (overscroll-behavior wouldn't apply) - this is what stops the browser reading a drag over the dimmed backdrop as a page-level gesture (pull-to-refresh) instead of just dismissing the sheet via the existing tap-to-close below. */ touch-action: none; } .mobileNavScrim.open { opacity: 1; pointer-events: auto; } .mobileNavSheet { position: fixed; left: 0; right: 0; bottom: 0; z-index: 21; max-height: 75vh; overflow-y: auto; /* Stops a drag that hits the top of the sheet's OWN scroll (its library grid can be taller than 75vh) from chaining past this element to the page underneath and surfacing as pull-to-refresh - complements touch-action:none on the dedicated grabber handle below, which instead covers a drag that never scrolls anything. */ overscroll-behavior: contain; background: var(--bg-header); border-radius: 16px 16px 0 0; padding: 8px 16px calc(20px + env(safe-area-inset-bottom, 0px)); transform: translateY(100%); transition: transform .22s ease; box-shadow: 0 -8px 24px rgba(0, 0, 0, .3); } .mobileNavSheet.open { transform: translateY(0); } /* Dragging (see Shell.ts) suspends the transition above for 1:1 tracking under the finger, then this class restores it for the snap-back/close animation - same "instant while dragging, animated on release" split views/player/Player.css's own seek bar uses. */ .mobileNavSheet.dragging { transition: none; } /* Bigger drag/touch target than the pill it contains (36x4px is far below a real touch-target minimum) - see Shell.html/Shell.ts. */ .sheetGrabberHandle { display: flex; justify-content: center; padding: 14px 0 10px; touch-action: none; cursor: grab; } .sheetGrabber { width: 36px; height: 4px; border-radius: 99px; background: var(--muted); } /* Accueil and the (conditional) Listes de lecture row both use this - same visual weight, bookending the "Bibliothèques" group between them, so the one part whose size actually varies (however many libraries Emby returns) reads as visually distinct from the two fixed rows around it. */ .sheetPrimary { display: flex; align-items: center; gap: 10px; width: 100%; background: var(--panel); border: none; border-radius: 10px; color: var(--text); font-family: inherit; font-size: 14px; font-weight: 600; padding: 12px; margin: 4px 0; text-decoration: none; } .sheetPrimary.active { background: var(--accent); color: #fff; } .sheetGrid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 4px 0 10px; } .sheetItem { display: flex; align-items: center; gap: 9px; background: var(--panel); border-radius: 10px; color: var(--text); font-size: 13px; padding: 12px 10px; text-decoration: none; } .sheetItem.active { background: var(--accent); color: #fff; font-weight: 600; } .navIcon { width: 20px; height: 20px; flex-shrink: 0; display: block; } .navIcon svg { width: 100%; height: 100%; } .sheetLogout { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; margin-top: 8px; padding: 12px; border: 1px solid rgba(255, 255, 255, .15); border-radius: 10px; color: var(--muted); font-size: 13px; } @media (max-width: 720px) { .appShell { flex-direction: column; } .sidebar { display: none; } .mobileTopBar { display: flex; align-items: center; } .mobileNavTrigger { display: flex; } .mobileNavScrim { display: block; } .mobileNavSheet { display: block; } }