/* ========================================================================== * Global base styles — imported once from src/main.ts, after Element Plus. * * This file owns exactly two things: a minimal reset, and the application * shell's viewport contract. Every view inherits both, so no page has to * re-declare margins, heights or scroll behaviour. * * Why it exists * ------------- * Element Plus ships component styles only: dist/index.css contains no * `html` / `body` reset. The browser default `body { margin: 8px }` therefore * applied on top of a shell declared as `min-height: 100vh`. The document * measured one viewport PLUS 8px, which produced: * * - a permanently visible vertical scrollbar on the right edge, even on a * page whose content is far shorter than the viewport; * - every page shifted 8px away from the intended 20px gutter; * - two competing scroll containers (the window and .el-main). * * The contract now * ---------------- * html / body / #app / .app-shell exactly one viewport tall, zero margin * body never scrolls -> the window has no bar * .app-main the single scroll container, showing a * slim bar only when content really does * exceed the viewport * page gutter el-header / el-main padding (20px), * which all content stays inside * ========================================================================== */ /* --- reset ----------------------------------------------------------------- */ *, *::before, *::after { box-sizing: border-box; } html, body { height: 100%; margin: 0; padding: 0; } body { /* Element Plus declares --el-font-family but never applies it to the document, so page-level text would otherwise fall back to the browser default. */ font-family: var(--el-font-family); background-color: var(--el-bg-color-page); color: var(--el-text-color-primary); /* The shell below is exactly one viewport tall and owns all scrolling. The window itself must never scroll, so no bar appears on the right. */ overflow: hidden; } #app { height: 100%; min-width: 0; } /* Media can never be the reason a page overflows its padding. */ img, svg, video, canvas { max-width: 100%; } /* Long unbroken strings — DSNs, URLs, hashes — wrap instead of pushing the content box wider than the page gutter. */ code, kbd, samp { overflow-wrap: anywhere; } /* --- application shell ------------------------------------------------------ .app-shell, .app-body, .app-main and .app-aside are applied by src/App.vue and src/components/SectionAside.vue. They live here because the viewport contract spans html -> body -> #app -> shell -> body -> main and only holds together when it is declared in one place. The shape is: .app-shell column: header over .app-body .app-body row: .app-main over .app-aside (if present) .app-main the single scroll container .app-aside fixed-width rail, scrolls independently --------------------------------------------------------------------------- */ .app-shell { /* Fill the viewport exactly. Not `min-height: 100vh`: that grows with content and re-introduces the window scrollbar. */ height: 100%; min-height: 0; /* el-container defaults to a row; the shell stacks header over content. */ flex-direction: column; } /* The header keeps its height; only the content area scrolls. */ .app-shell > .app-header { flex: 0 0 auto; } /* The row holding the content and the right-hand menu. */ .app-body { flex: 1 1 auto; min-height: 0; /* el-container is a row by default and stays one because it holds an el-aside; stating it keeps the layout readable. */ flex-direction: row; } .app-main { /* min-height: 0 lets this flex child actually shrink, so overflow is contained here instead of escaping to the window. */ flex: 1 1 auto; min-width: 0; min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; /* Slim, low-contrast scrollbar: invisible while content fits, unobtrusive when it does not. Firefox. */ scrollbar-width: thin; scrollbar-color: var(--el-border-color-darker) transparent; } /* WebKit / Blink. */ .app-main::-webkit-scrollbar, .app-aside::-webkit-scrollbar { width: 8px; height: 8px; } .app-main::-webkit-scrollbar-track, .app-aside::-webkit-scrollbar-track { background: transparent; } .app-main::-webkit-scrollbar-thumb, .app-aside::-webkit-scrollbar-thumb { background-color: var(--el-border-color-darker); border-radius: 4px; } .app-main::-webkit-scrollbar-thumb:hover, .app-aside::-webkit-scrollbar-thumb:hover { background-color: var(--el-text-color-placeholder); } /* The second-level menu on the right. Its own width is set by the component, so only the shrink behaviour belongs here. */ .app-aside { flex: 0 0 auto; min-height: 0; overflow-x: hidden; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--el-border-color-darker) transparent; transition: width 0.2s ease; } /* --- page-level helpers ----------------------------------------------------- A view's outermost element uses .page to get a consistent vertical rhythm inside the main gutter. */ .page { display: flex; flex-direction: column; gap: 16px; min-width: 0; } /* Toolbar above a table: filters on the left, actions on the right, wrapping on narrow screens instead of overflowing the gutter. */ .toolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } .toolbar-spacer { flex: 1 1 auto; min-width: 0; } /* Right-aligned pagination under a table. */ .table-pagination { display: flex; justify-content: flex-end; margin-top: 16px; }