:root{--ion-color-primary: #0054e9;--ion-color-primary-rgb: 0, 84, 233;--ion-color-primary-contrast: #fff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #004acd;--ion-color-primary-tint: #1a65eb;--ion-color-secondary: #0163aa;--ion-color-secondary-rgb: 1, 99, 170;--ion-color-secondary-contrast: #fff;--ion-color-secondary-contrast-rgb: 255, 255, 255;--ion-color-secondary-shade: #015796;--ion-color-secondary-tint: #1a73b3;--ion-color-tertiary: #6030ff;--ion-color-tertiary-rgb: 96, 48, 255;--ion-color-tertiary-contrast: #fff;--ion-color-tertiary-contrast-rgb: 255, 255, 255;--ion-color-tertiary-shade: #542ae0;--ion-color-tertiary-tint: #7045ff;--ion-color-success: #2dd55b;--ion-color-success-rgb: 45, 213, 91;--ion-color-success-contrast: #000;--ion-color-success-contrast-rgb: 0, 0, 0;--ion-color-success-shade: #28bb50;--ion-color-success-tint: #42d96b;--ion-color-warning: #ffc409;--ion-color-warning-rgb: 255, 196, 9;--ion-color-warning-contrast: #000;--ion-color-warning-contrast-rgb: 0, 0, 0;--ion-color-warning-shade: #e0ac08;--ion-color-warning-tint: #ffca22;--ion-color-danger: #c5000f;--ion-color-danger-rgb: 197, 0, 15;--ion-color-danger-contrast: #fff;--ion-color-danger-contrast-rgb: 255, 255, 255;--ion-color-danger-shade: #ad000d;--ion-color-danger-tint: #cb1a27;--ion-color-light: #f4f5f8;--ion-color-light-rgb: 244, 245, 248;--ion-color-light-contrast: #000;--ion-color-light-contrast-rgb: 0, 0, 0;--ion-color-light-shade: #d7d8da;--ion-color-light-tint: #f5f6f9;--ion-color-medium: #636469;--ion-color-medium-rgb: 99, 100, 105;--ion-color-medium-contrast: #fff;--ion-color-medium-contrast-rgb: 255, 255, 255;--ion-color-medium-shade: #57585c;--ion-color-medium-tint: #737478;--ion-color-dark: #222428;--ion-color-dark-rgb: 34, 36, 40;--ion-color-dark-contrast: #fff;--ion-color-dark-contrast-rgb: 255, 255, 255;--ion-color-dark-shade: #1e2023;--ion-color-dark-tint: #383a3e}html.ios{--ion-default-font: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Roboto", sans-serif}html.md{--ion-default-font: "Roboto", "Helvetica Neue", sans-serif}html{--ion-dynamic-font: -apple-system-body;--ion-font-family: var(--ion-default-font)}body{background:var(--ion-background-color);color:var(--ion-text-color)}body.backdrop-no-scroll{overflow:hidden}html.ios ion-modal.modal-card ion-header ion-toolbar:first-of-type,html.ios ion-modal.modal-sheet ion-header ion-toolbar:first-of-type,html.ios ion-modal ion-footer ion-toolbar:first-of-type,html.ios ion-footer.modal-footer-moving ion-toolbar:first-of-type{padding-top:6px}html.ios ion-modal.modal-card ion-header ion-toolbar:last-of-type,html.ios ion-modal.modal-sheet ion-header ion-toolbar:last-of-type{padding-bottom:6px}html.ios ion-modal ion-toolbar,html.ios .modal-footer-moving ion-toolbar{padding-right:calc(var(--ion-safe-area-right) + 8px);padding-left:calc(var(--ion-safe-area-left) + 8px)}@media screen and (min-width: 768px){html.ios ion-modal.modal-card:first-of-type{--backdrop-opacity: 0.18}}ion-modal.modal-default.show-modal~ion-modal.modal-default{--backdrop-opacity: 0;--box-shadow: none}html.ios ion-modal.modal-card .ion-page{border-top-left-radius:var(--border-radius)}.ion-color-primary{--ion-color-base: var(--ion-color-primary, #0054e9) !important;--ion-color-base-rgb: var(--ion-color-primary-rgb, 0, 84, 233) !important;--ion-color-contrast: var(--ion-color-primary-contrast, #fff) !important;--ion-color-contrast-rgb: var(--ion-color-primary-contrast-rgb, 255, 255, 255) !important;--ion-color-shade: var(--ion-color-primary-shade, #004acd) !important;--ion-color-tint: var(--ion-color-primary-tint, #1a65eb) !important}.ion-color-secondary{--ion-color-base: var(--ion-color-secondary, #0163aa) !important;--ion-color-base-rgb: var(--ion-color-secondary-rgb, 1, 99, 170) !important;--ion-color-contrast: var(--ion-color-secondary-contrast, #fff) !important;--ion-color-contrast-rgb: var(--ion-color-secondary-contrast-rgb, 255, 255, 255) !important;--ion-color-shade: var(--ion-color-secondary-shade, #015796) !important;--ion-color-tint: var(--ion-color-secondary-tint, #1a73b3) !important}.ion-color-tertiary{--ion-color-base: var(--ion-color-tertiary, #6030ff) !important;--ion-color-base-rgb: var(--ion-color-tertiary-rgb, 96, 48, 255) !important;--ion-color-contrast: var(--ion-color-tertiary-contrast, #fff) !important;--ion-color-contrast-rgb: var(--ion-color-tertiary-contrast-rgb, 255, 255, 255) !important;--ion-color-shade: var(--ion-color-tertiary-shade, #542ae0) !important;--ion-color-tint: var(--ion-color-tertiary-tint, #7045ff) !important}.ion-color-success{--ion-color-base: var(--ion-color-success, #2dd55b) !important;--ion-color-base-rgb: var(--ion-color-success-rgb, 45, 213, 91) !important;--ion-color-contrast: var(--ion-color-success-contrast, #000) !important;--ion-color-contrast-rgb: var(--ion-color-success-contrast-rgb, 0, 0, 0) !important;--ion-color-shade: var(--ion-color-success-shade, #28bb50) !important;--ion-color-tint: var(--ion-color-success-tint, #42d96b) !important}.ion-color-warning{--ion-color-base: var(--ion-color-warning, #ffc409) !important;--ion-color-base-rgb: var(--ion-color-warning-rgb, 255, 196, 9) !important;--ion-color-contrast: var(--ion-color-warning-contrast, #000) !important;--ion-color-contrast-rgb: var(--ion-color-warning-contrast-rgb, 0, 0, 0) !important;--ion-color-shade: var(--ion-color-warning-shade, #e0ac08) !important;--ion-color-tint: var(--ion-color-warning-tint, #ffca22) !important}.ion-color-danger{--ion-color-base: var(--ion-color-danger, #c5000f) !important;--ion-color-base-rgb: var(--ion-color-danger-rgb, 197, 0, 15) !important;--ion-color-contrast: var(--ion-color-danger-contrast, #fff) !important;--ion-color-contrast-rgb: var(--ion-color-danger-contrast-rgb, 255, 255, 255) !important;--ion-color-shade: var(--ion-color-danger-shade, #ad000d) !important;--ion-color-tint: var(--ion-color-danger-tint, #cb1a27) !important}.ion-color-light{--ion-color-base: var(--ion-color-light, #f4f5f8) !important;--ion-color-base-rgb: var(--ion-color-light-rgb, 244, 245, 248) !important;--ion-color-contrast: var(--ion-color-light-contrast, #000) !important;--ion-color-contrast-rgb: var(--ion-color-light-contrast-rgb, 0, 0, 0) !important;--ion-color-shade: var(--ion-color-light-shade, #d7d8da) !important;--ion-color-tint: var(--ion-color-light-tint, #f5f6f9) !important}.ion-color-medium{--ion-color-base: var(--ion-color-medium, #636469) !important;--ion-color-base-rgb: var(--ion-color-medium-rgb, 99, 100, 105) !important;--ion-color-contrast: var(--ion-color-medium-contrast, #fff) !important;--ion-color-contrast-rgb: var(--ion-color-medium-contrast-rgb, 255, 255, 255) !important;--ion-color-shade: var(--ion-color-medium-shade, #57585c) !important;--ion-color-tint: var(--ion-color-medium-tint, #737478) !important}.ion-color-dark{--ion-color-base: var(--ion-color-dark, #222428) !important;--ion-color-base-rgb: var(--ion-color-dark-rgb, 34, 36, 40) !important;--ion-color-contrast: var(--ion-color-dark-contrast, #fff) !important;--ion-color-contrast-rgb: var(--ion-color-dark-contrast-rgb, 255, 255, 255) !important;--ion-color-shade: var(--ion-color-dark-shade, #1e2023) !important;--ion-color-tint: var(--ion-color-dark-tint, #383a3e) !important}.ion-page{left:0;right:0;top:0;bottom:0;display:flex;position:absolute;flex-direction:column;justify-content:space-between;contain:layout size style;z-index:0}.ion-page.ion-page-overlay-passthrough{pointer-events:none}ion-modal>.ion-page{position:relative;contain:layout style;height:100%}.split-pane-visible>.ion-page.split-pane-main{position:relative}ion-route,ion-route-redirect,ion-router,ion-select-option,ion-nav-controller,ion-menu-controller,ion-action-sheet-controller,ion-alert-controller,ion-loading-controller,ion-modal-controller,ion-picker-controller,ion-popover-controller,ion-toast-controller,.ion-page-hidden{display:none !important}.ion-page-invisible{opacity:0}.can-go-back>ion-header ion-back-button{display:block}html.plt-ios.plt-hybrid,html.plt-ios.plt-pwa{--ion-statusbar-padding: 20px}@supports(padding-top: 20px){html{--ion-safe-area-top: var(--ion-statusbar-padding)}}@supports(padding-top: env(safe-area-inset-top)){html{--ion-safe-area-top: var(--safe-area-inset-top, env(safe-area-inset-top));--ion-safe-area-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));--ion-safe-area-left: var(--safe-area-inset-left, env(safe-area-inset-left));--ion-safe-area-right: var(--safe-area-inset-right, env(safe-area-inset-right))}}ion-card.ion-color .ion-inherit-color,ion-card-header.ion-color .ion-inherit-color{color:inherit}.menu-content{transform:translate3d(0,  0,  0)}.menu-content-open{cursor:pointer;touch-action:manipulation;pointer-events:none;overflow-y:hidden}.menu-content-open ion-content{--overflow: hidden}.menu-content-open .ion-content-scroll-host{overflow:hidden}.ios .menu-content-reveal{box-shadow:-8px 0 42px rgba(0,0,0,.08)}[dir=rtl].ios .menu-content-reveal{box-shadow:8px 0 42px rgba(0,0,0,.08)}.md .menu-content-reveal{box-shadow:4px 0px 16px rgba(0,0,0,.18)}.md .menu-content-push{box-shadow:4px 0px 16px rgba(0,0,0,.18)}ion-accordion-group.accordion-group-expand-inset>ion-accordion:first-of-type{border-top-left-radius:8px;border-top-right-radius:8px}ion-accordion-group.accordion-group-expand-inset>ion-accordion:last-of-type{border-bottom-left-radius:8px;border-bottom-right-radius:8px}ion-accordion-group>ion-accordion:last-of-type ion-item[slot=header]{--border-width: 0px}ion-accordion.accordion-animated>[slot=header] .ion-accordion-toggle-icon{transition:300ms transform cubic-bezier(0.25, 0.8, 0.5, 1)}@media(prefers-reduced-motion: reduce){ion-accordion .ion-accordion-toggle-icon{transition:none !important}}ion-accordion.accordion-expanding>[slot=header] .ion-accordion-toggle-icon,ion-accordion.accordion-expanded>[slot=header] .ion-accordion-toggle-icon{transform:rotate(180deg)}ion-accordion-group.accordion-group-expand-inset.md>ion-accordion.accordion-previous ion-item[slot=header]{--border-width: 0px;--inner-border-width: 0px}ion-accordion-group.accordion-group-expand-inset.md>ion-accordion.accordion-expanding:first-of-type,ion-accordion-group.accordion-group-expand-inset.md>ion-accordion.accordion-expanded:first-of-type{margin-top:0}ion-input input::-webkit-date-and-time-value{text-align:start}.ion-datetime-button-overlay{--width: fit-content;--height: fit-content}.ion-datetime-button-overlay ion-datetime.datetime-grid{width:320px;min-height:320px}[ion-last-focus],header[tabindex="-1"]:focus,[role=banner][tabindex="-1"]:focus,main[tabindex="-1"]:focus,[role=main][tabindex="-1"]:focus,h1[tabindex="-1"]:focus,[role=heading][aria-level="1"][tabindex="-1"]:focus{outline:none}.popover-viewport:has(>ion-content){overflow:hidden}@supports not selector(:has(> ion-content)){.popover-viewport{overflow:hidden}}/*# sourceMappingURL=core.css.map */
audio,canvas,progress,video{vertical-align:baseline}audio:not([controls]){display:none;height:0}b,strong{font-weight:bold}img{max-width:100%}hr{height:1px;border-width:0;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}label,input,select,textarea{font-family:inherit;line-height:normal}textarea{overflow:auto;height:auto;font:inherit;color:inherit}textarea::placeholder{padding-left:2px}form,input,optgroup,select{margin:0;font:inherit;color:inherit}html input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button}a,a div,a span,a ion-icon,a ion-label,button,button div,button span,button ion-icon,button ion-label,.ion-tappable,[tappable],[tappable] div,[tappable] span,[tappable] ion-icon,[tappable] ion-label,input,textarea{touch-action:manipulation}a ion-label,button ion-label{pointer-events:none}button{padding:0;border:0;border-radius:0;font-family:inherit;font-style:inherit;font-variant:inherit;line-height:1;text-transform:none;cursor:pointer;-webkit-appearance:button}[tappable]{cursor:pointer}a[disabled],button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*# sourceMappingURL=normalize.css.map */
*{box-sizing:border-box;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none}html{width:100%;height:100%;-webkit-text-size-adjust:100%;text-size-adjust:100%}html.ion-ce body{display:block}html.plt-pwa{height:100vh}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0;padding-left:0;padding-right:0;padding-top:0;padding-bottom:0;position:fixed;width:100%;max-width:100%;height:100%;max-height:100%;transform:translateZ(0);text-rendering:optimizeLegibility;overflow:hidden;touch-action:manipulation;-webkit-user-drag:none;-ms-content-zooming:none;word-wrap:break-word;overscroll-behavior-y:none;-webkit-text-size-adjust:none;text-size-adjust:none}/*# sourceMappingURL=structure.css.map */
html{font-family:var(--ion-font-family)}@supports(-webkit-touch-callout: none){html{font:var(--ion-dynamic-font, 16px var(--ion-font-family))}}a{background-color:transparent;color:var(--ion-color-primary, #0054e9)}h1,h2,h3,h4,h5,h6{margin-top:16px;margin-bottom:10px;font-weight:500;line-height:1.2}h1{margin-top:20px;font-size:1.625rem}h2{margin-top:18px;font-size:1.5rem}h3{font-size:1.375rem}h4{font-size:1.25rem}h5{font-size:1.125rem}h6{font-size:1rem}small{font-size:75%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}/*# sourceMappingURL=typography.css.map */
:root{--ion-color-primary: #4d8dff;--ion-color-primary-rgb: 77, 141, 255;--ion-color-primary-contrast: #000;--ion-color-primary-contrast-rgb: 0, 0, 0;--ion-color-primary-shade: #447ce0;--ion-color-primary-tint: #5f98ff;--ion-color-secondary: #46b1ff;--ion-color-secondary-rgb: 70, 177, 255;--ion-color-secondary-contrast: #000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #3e9ce0;--ion-color-secondary-tint: #59b9ff;--ion-color-tertiary: #8482fb;--ion-color-tertiary-rgb: 132, 130, 251;--ion-color-tertiary-contrast: #000;--ion-color-tertiary-contrast-rgb: 0, 0, 0;--ion-color-tertiary-shade: #7472dd;--ion-color-tertiary-tint: #908ffb;--ion-color-success: #2dd55b;--ion-color-success-rgb: 45, 213, 91;--ion-color-success-contrast: #000;--ion-color-success-contrast-rgb: 0, 0, 0;--ion-color-success-shade: #28bb50;--ion-color-success-tint: #42d96b;--ion-color-warning: #ffce31;--ion-color-warning-rgb: 255, 206, 49;--ion-color-warning-contrast: #000;--ion-color-warning-contrast-rgb: 0, 0, 0;--ion-color-warning-shade: #e0b52b;--ion-color-warning-tint: #ffd346;--ion-color-danger: #f24c58;--ion-color-danger-rgb: 242, 76, 88;--ion-color-danger-contrast: #000;--ion-color-danger-contrast-rgb: 0, 0, 0;--ion-color-danger-shade: #d5434d;--ion-color-danger-tint: #f35e69;--ion-color-light: #222428;--ion-color-light-rgb: 34, 36, 40;--ion-color-light-contrast: #fff;--ion-color-light-contrast-rgb: 255, 255, 255;--ion-color-light-shade: #1e2023;--ion-color-light-tint: #383a3e;--ion-color-medium: #989aa2;--ion-color-medium-rgb: 152, 154, 162;--ion-color-medium-contrast: #000;--ion-color-medium-contrast-rgb: 0, 0, 0;--ion-color-medium-shade: #86888f;--ion-color-medium-tint: #a2a4ab;--ion-color-dark: #f4f5f8;--ion-color-dark-rgb: 244, 245, 248;--ion-color-dark-contrast: #000;--ion-color-dark-contrast-rgb: 0, 0, 0;--ion-color-dark-shade: #d7d8da;--ion-color-dark-tint: #f5f6f9}:root.ios{--ion-background-color: #000000;--ion-background-color-rgb: 0, 0, 0;--ion-text-color: #ffffff;--ion-text-color-rgb: 255, 255, 255;--ion-background-color-step-50: #0d0d0d;--ion-background-color-step-100: #1a1a1a;--ion-background-color-step-150: #262626;--ion-background-color-step-200: #333333;--ion-background-color-step-250: #404040;--ion-background-color-step-300: #4d4d4d;--ion-background-color-step-350: #595959;--ion-background-color-step-400: #666666;--ion-background-color-step-450: #737373;--ion-background-color-step-500: #808080;--ion-background-color-step-550: #8c8c8c;--ion-background-color-step-600: #999999;--ion-background-color-step-650: #a6a6a6;--ion-background-color-step-700: #b3b3b3;--ion-background-color-step-750: #bfbfbf;--ion-background-color-step-800: #cccccc;--ion-background-color-step-850: #d9d9d9;--ion-background-color-step-900: #e6e6e6;--ion-background-color-step-950: #f2f2f2;--ion-text-color-step-50: #f2f2f2;--ion-text-color-step-100: #e6e6e6;--ion-text-color-step-150: #d9d9d9;--ion-text-color-step-200: #cccccc;--ion-text-color-step-250: #bfbfbf;--ion-text-color-step-300: #b3b3b3;--ion-text-color-step-350: #a6a6a6;--ion-text-color-step-400: #999999;--ion-text-color-step-450: #8c8c8c;--ion-text-color-step-500: #808080;--ion-text-color-step-550: #737373;--ion-text-color-step-600: #666666;--ion-text-color-step-650: #595959;--ion-text-color-step-700: #4d4d4d;--ion-text-color-step-750: #404040;--ion-text-color-step-800: #333333;--ion-text-color-step-850: #262626;--ion-text-color-step-900: #1a1a1a;--ion-text-color-step-950: #0d0d0d;--ion-item-background: #000000;--ion-tab-bar-background-focused: #252525;--ion-card-background: #1c1c1d}:root.ios ion-modal{--ion-background-color: var(--ion-color-step-100, var(--ion-background-color-step-100));--ion-toolbar-background: var(--ion-color-step-150, var(--ion-background-color-step-150));--ion-toolbar-border-color: var(--ion-color-step-250, var(--ion-background-color-step-250))}:root.md{--ion-background-color: #121212;--ion-background-color-rgb: 18, 18, 18;--ion-text-color: #ffffff;--ion-text-color-rgb: 255, 255, 255;--ion-background-color-step-50: #1e1e1e;--ion-background-color-step-100: #2a2a2a;--ion-background-color-step-150: #363636;--ion-background-color-step-200: #414141;--ion-background-color-step-250: #4d4d4d;--ion-background-color-step-300: #595959;--ion-background-color-step-350: #656565;--ion-background-color-step-400: #717171;--ion-background-color-step-450: #7d7d7d;--ion-background-color-step-500: #898989;--ion-background-color-step-550: #949494;--ion-background-color-step-600: #a0a0a0;--ion-background-color-step-650: #acacac;--ion-background-color-step-700: #b8b8b8;--ion-background-color-step-750: #c4c4c4;--ion-background-color-step-800: #d0d0d0;--ion-background-color-step-850: #dbdbdb;--ion-background-color-step-900: #e7e7e7;--ion-background-color-step-950: #f3f3f3;--ion-text-color-step-50: #f3f3f3;--ion-text-color-step-100: #e7e7e7;--ion-text-color-step-150: #dbdbdb;--ion-text-color-step-200: #d0d0d0;--ion-text-color-step-250: #c4c4c4;--ion-text-color-step-300: #b8b8b8;--ion-text-color-step-350: #acacac;--ion-text-color-step-400: #a0a0a0;--ion-text-color-step-450: #949494;--ion-text-color-step-500: #898989;--ion-text-color-step-550: #7d7d7d;--ion-text-color-step-600: #717171;--ion-text-color-step-650: #656565;--ion-text-color-step-700: #595959;--ion-text-color-step-750: #4d4d4d;--ion-text-color-step-800: #414141;--ion-text-color-step-850: #363636;--ion-text-color-step-900: #2a2a2a;--ion-text-color-step-950: #1e1e1e;--ion-item-background: #1e1e1e;--ion-toolbar-background: #1f1f1f;--ion-tab-bar-background: #1f1f1f;--ion-tab-bar-background-focused: #353535;--ion-card-background: #1e1e1e}/*# sourceMappingURL=dark.always.css.map */
/* Truly global styles only - everything else lives scoped inside its own
   component. See App.vue's onMounted for the ionic-components-ready class
   this file gates #topBar/#tabBarWrap visibility on. */

body, html {
    margin: 0;
    padding: 0;
    height: 100vh; /* Full viewport height */
    display: flex;
    flex-direction: column;
    /* Same tone as ReleaseList.vue's #content - covers the safe-area strip
       behind the status bar (see viewport-fit=cover/status-bar-style meta
       tags in index.html) so that area reads as part of the app, not a
       leftover white gap. */
    background: #121212;
}

/* Ionic's ion-* elements are lazy-loaded Stencil web components - each tag
   only gets its real shadow-DOM layout/styling once its own JS chunk
   fetches and customElements.define() runs for it. Until then the browser
   treats an unknown/not-yet-upgraded custom element as a plain unstyled
   inline element, so #topBar/#tabBarWrap render as broken/unstyled content
   for a flash on every load. Hidden here by default and revealed by the
   ionic-components-ready class App.vue's onMounted adds once
   customElements.whenDefined() resolves for every ion-* tag actually used
   in this page - a single clean reveal instead of a jumbled-then-fixed
   flash. #topBar/#tabBarWrap are the global ids of AppHeader.vue/
   AppTabBar.vue's own fixed-position root wrapper divs (deliberately NOT
   the inner ion-header/ion-tab-bar, which App.vue's onMounted below
   watches for Stencil's own "hydrated" class instead) - this being a
   plain global stylesheet, not scoped to either component, is what lets
   it reach into both from one place. */
#topBar, #tabBarWrap {
    visibility: hidden;
}
html.ionic-components-ready #topBar,
html.ionic-components-ready #tabBarWrap {
    visibility: visible;
}

/* Generic reusable blurred-glass surface - apply this class to any fixed
   bar that needs an iOS-native-navbar-style translucent backdrop (used by
   both #topBar and #mainTabBar, same class, no header-specific logic in
   here).
   Deliberately NOT using Ionic's own translucent="true"/--background
   mechanism: that recipe (saturate(180%) blur(20px)) lives inside
   ion-header's own shadow DOM, unreachable/untunable from here (no CSS
   custom prop or ::part for it), and on iOS 27 specifically its default
   vividness read as a harsh, oversaturated pop against the real status
   bar's own native darkening scrim directly above it (confirmed via a
   solid-red debug test - that strip is always native-dimmed regardless of
   page CSS, no known fix, see
   https://muffinman.io/blog/pwa-ios-status-bar-blur/). Applying our own
   backdrop-filter directly to the bar's own (non-Ionic, plain-div) wrapper
   instead gives full control over the saturate/blur/alpha recipe, and -
   just as importantly - paints ONE continuous surface across the bar's
   entire height (safe-area strip included) instead of two different
   mechanisms (a patch overlay + Ionic's per-toolbar blur) meeting at a
   visible seam. Tuned via CSS custom props so a future footer instance can
   override just what it needs. */
/* One frosted-glass recipe for every layered surface in the app - the
   header (#topBar), tab bar (#mainTabBar), the pinned week label, the
   warm sync banner, the PlaylistPicker header - all via these tokens so
   nothing drifts into a visible tint/blur mismatch. Matches the side
   menu's .menu-glass. */
:root {
    --blur-bg: rgba(18, 18, 18, 0.42);
    --blur-saturate: 150%;
    --blur-amount: 28px;
}
/* "Native Statusbar-Blur" ON (see useAppSettings' nativeStatusbarBlur /
   applyStatusbarMode, which toggles this class on <html>): the two bars
   drop their own surface entirely and rely solely on ion-header's /
   ion-tab-bar's own translucent="true" recipe (set alongside this class),
   i.e. whatever Ionic actually ships for iOS, unmodified - the point of
   the toggle. AppHeader.vue's .header-week already rides on ion-header's
   own blur either way; AppTabBar.vue's sync/stats rows have no Ionic
   translucency to fall back to and just go backdrop-less in this mode -
   there's no native equivalent for them, that's fine (see below for
   ion-tab-bar itself). Higher specificity than the plain class below (id +
   class + element) wins regardless of order.

   This rule, and every other custom-mode-only / native-mode-only override
   below for the tab bar and side menu, lives here as plain global CSS
   instead of in each component's own <style scoped> block on purpose:
   Vue's scoped-CSS :global() only rewrites a single wrapped selector, not
   a leading combinator in front of more selector text - a rule written in
   a component as `:global(html:not(...)) ion-tab-bar#mainTabBar` silently
   compiles to the useless `html:not(...) { ... }` (confirmed directly via
   @vue/compiler-sfc - no build error, just wrong output, the bug that
   made the whole toggle a no-op the first two times round). Plain global
   CSS has no such restriction. */
html.ionic-native-header-blur #topBar,
html.ionic-native-header-blur #tabBarWrap {
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}
/* AppTabBar.vue's ion-tab-bar#mainTabBar - custom mode only. Fully
   transparent + NO backdrop of its own - the one frosted surface is
   #tabBarWrap's .native-blur-surface above, and the tab bar just rides on
   it (same as its sync/stats rows). Anything the tab bar paints itself
   would only cover the icon-row half of the wrapper, leaving that half
   double-tinted / darker than the rows above it. Native mode leaves the
   tab bar's own :host(.tab-bar-translucent) recipe (translucent="true",
   set by applyStatusbarMode) completely unmodified. */
html:not(.ionic-native-header-blur) ion-tab-bar#mainTabBar {
    --background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    /* Ionic's own :host rule sets contain: strict - like ion-menu's
       .menu-inner below, that makes ion-tab-bar a "backdrop root", which
       cuts off #tabBarWrap's blurred backdrop hard at the tab bar's own
       top edge instead of it showing through the transparent --background
       above. Same fix: drop it. (Native mode has no wrapper backdrop to
       bleed through anymore, so Ionic's own contain: strict is left
       alone.) */
    contain: none;
}
/* AppTabBar.vue's .tab-sync-row / .tab-stats-row - native mode. Both sit
   OUTSIDE ion-tab-bar (siblings in #tabBarWrap, above it), so unlike
   AppHeader.vue's .header-week (which lives INSIDE ion-header and rides
   on its .header-background blur layer for free), there's no Ionic
   translucency covering these two rows at all - #tabBarWrap's own custom
   glass is what covered them, and that's gone in native mode. Left
   unpainted they're literally see-through to whatever's scrolled
   underneath this fixed overlay - not just "no blur", unreadable. Same
   fix as ion-menu's footer above: paint the real Ionic background token
   directly, since there's no toolbar-equivalent element here to source it
   from automatically. */
html.ionic-native-header-blur .tab-sync-row,
html.ionic-native-header-blur .tab-stats-row {
    background: var(--ion-background-color, #000000);
}

/* AppMenu.vue's ion-menu drawer - custom mode only. iOS-native translucent
   UISplitViewController-style drawer: the app blur is on the .menu-glass
   sibling layer (not ::part(container)) so it can also cover the header/
   footer's own frosted glass below without being clipped by .menu-inner's
   `contain: strict`, hence dropping that too. Native mode leaves ion-menu,
   its toolbar and its content all on Ionic's own (opaque) --background
   default - there's no Ionic-native menu translucency to switch to, this
   IS what Ionic ships for a plain ion-menu. */
html:not(.ionic-native-header-blur) ion-menu {
    --background: transparent;
}
html:not(.ionic-native-header-blur) ion-menu::part(container) {
    contain: none;
}
html:not(.ionic-native-header-blur) .menu-glass {
    background: rgba(18, 18, 18, 0.42);
    -webkit-backdrop-filter: saturate(150%) blur(28px);
    backdrop-filter: saturate(150%) blur(28px);
}
/* ion-menu's header/footer - custom mode: frosted glass, same recipe as
   the two main bars. */
html:not(.ionic-native-header-blur) ion-menu ion-header,
html:not(.ionic-native-header-blur) ion-menu ion-footer {
    background: rgba(18, 18, 18, 0.5);
    -webkit-backdrop-filter: saturate(160%) blur(22px);
    backdrop-filter: saturate(160%) blur(22px);
}
/* ion-menu's footer - native mode. Unlike ion-header (which paints solid
   via its own ion-toolbar child, using that toolbar's own --background
   default below), the footer's content here is plain <div>s with no
   ion-toolbar - and per Ionic's own footer.ios.css, a bare <ion-footer>
   carries NO background of its own (just display/position/z-index). With
   the custom frosted paint above gated to custom-mode only, this footer
   would otherwise render fully transparent in native mode, letting the
   still-scrolling list underneath show through this fixed overlay.
   Painting the real Ionic ios background token directly is the closest
   "as Ionic defines it" fix available without restructuring this into an
   actual <ion-toolbar>. */
html.ionic-native-header-blur ion-menu ion-footer {
    background: var(--ion-background-color, #000000);
}
/* ion-menu's header toolbar - custom mode: transparent, rides on the
   header's own frosted glass above, iOS-style hairline instead of Ionic's
   solid divider (reads right on a translucent surface). Native mode
   leaves the toolbar's own (opaque) --background and border alone. */
html:not(.ionic-native-header-blur) ion-menu ion-header ion-toolbar {
    --background: transparent;
    --border-width: 0 0 1px 0;
    --border-color: rgba(255, 255, 255, 0.12);
}
/* ion-menu's content - custom mode: transparent, rides on .menu-glass.
   Native mode leaves it on Ionic's own opaque --background default. */
html:not(.ionic-native-header-blur) ion-menu ion-content {
    --background: transparent;
}
.native-blur-surface {
    background: var(--blur-bg, rgba(18, 18, 18, 0.42));
    -webkit-backdrop-filter: saturate(var(--blur-saturate, 150%)) blur(var(--blur-amount, 28px));
    backdrop-filter: saturate(var(--blur-saturate, 150%)) blur(var(--blur-amount, 28px));
}

/* iOS's PWA status-bar glass blur feathers ~48px BELOW
   env(safe-area-inset-top) (measured on-device via GlassProbe, ?probe=1).
   A fixed bar whose own content must stay crisp adds var(--glass-safe-pad)
   on top of the raw inset. Kept as one CSS var so it's a single knob:
   the operator can zero it from the side menu (app_settings.glass_safe_pad
   -> html.glass-safe-pad-off, see useAppSettings/applyGlassSafePad) if a
   future iOS release renders the top edge cleanly and the extra gap is no
   longer wanted. */
:root {
    --glass-safe-pad: 48px;
}
html.glass-safe-pad-off {
    --glass-safe-pad: 0px;
}
/* App-wide hairline at the bottom edge of the extended safe-area zone
   (raw inset + --glass-safe-pad), i.e. right where real content starts.
   Without it the frosted empty band up top just looks like something
   failed to load; the line frames it as deliberate header inset. Only in
   glass-safe-pad mode (there's no extra zone to frame when it's off), and
   below z 1100 so the side menu / modals - which draw their own top
   dividers - cover it. */
html:not(.glass-safe-pad-off) body::before {
    content: '';
    position: fixed;
    top: calc(env(safe-area-inset-top) + var(--glass-safe-pad, 48px));
    left: 0;
    right: 0;
    height: 1px;
    background: rgba(255, 255, 255, 0.1);
    z-index: 1100;
    pointer-events: none;
}

/* iOS Safari auto-zooms on focus for any input/select/textarea with a
   font-size under 16px, independent of the viewport meta tag's own scale
   lock in index.html - both are needed to actually stop it. */
input, select, textarea {
    font-size: 16px;
}

/* iOS-frosted popover - the fallback rendering of the release/artist card
   "..." menu (CardMenu.vue), used wherever the real native <select> menu
   isn't a fit (non-iOS, light mode, or the operator switch off; see
   useNativeMenu). Modelled on the native iOS context menu: a translucent
   grey with a heavy blur, big corner radius, soft shadow. The
   ::part(content) box and the arrow's ::after triangle share the fill; the
   inner ion-content is made transparent so the blur shows through. */
.native-menu-popover {
    --border-radius: 15px;
    --box-shadow: 0 8px 34px rgba(0, 0, 0, 0.5);
    --backdrop-opacity: 0;
    /* hug the widest row so nothing truncates and Ionic doesn't shift the
       bubble sideways off the "…" arrow */
    --width: max-content;
    --max-width: 82vw;
}
.native-menu-popover::part(content),
.native-menu-popover::part(arrow)::after {
    background: rgba(58, 58, 60, 0.68);
    -webkit-backdrop-filter: saturate(180%) blur(30px);
    backdrop-filter: saturate(180%) blur(30px);
}
.native-menu-popover::part(content) {
    border-radius: 15px;
    border: 0.5px solid rgba(255, 255, 255, 0.08);
}
.native-menu-popover ion-content {
    --background: transparent;
}
/* The arrow triangle and the content box are two separate elements, each
   with its OWN independent backdrop-filter blur (Ionic's own TODO
   FW-4376: "ensure arrow also blurs when translucent" - it does here, but
   as a second, unrelated blur region, and often not a REAL blur at all on
   such a tiny clipped shape - just a flat fill). The real fix lives in
   lib/popover-arrow-clip.ts (called from CardMenu.vue / NativeSelect.vue
   on @did-present): it hides the arrow entirely and clip-paths the
   content box itself into a rounded rect + a triangular tab reaching up
   to the arrow's position, so there is only ONE blurred surface and no
   seam is possible. What's below is only the fallback for if that JS
   never runs (enlarge + nudge the real arrow down over the content's top
   edge via transform, not top/left - those are set inline by Ionic's own
   JS positioning every time the popover opens, see popover/animations/
   ios.enter.js, and would need !important to fight) so it physically
   overlaps and masks the seam instead of just touching it - a flat arrow,
   but not a seam. */
.native-menu-popover::part(arrow) {
    width: 26px;
    height: 16px;
    z-index: 20;
    transform: translateY(6px);
}
.native-menu-popover::part(arrow)::after {
    width: 20px;
    height: 20px;
    top: 4px;
    z-index: 21;
}

/* "Album-Cover volle Höhe" toggle (side menu, operator-persisted - see
   useAppSettings' fullHeightCovers / applyFullHeightCoverMode). Off
   (default): every row image below is a fixed square, object-fit: cover
   (crops to fit). On: each image instead becomes a square matching its own
   row's actual rendered height.

   That size comes from --cover-size, a plain pixel custom property each
   row sets on ITSELF (useFullHeightCover.ts's useFullHeightCoverSize,
   called from ReleaseCard/ArtistCard/PlaylistPicker/PlaylistManageView),
   not from any CSS stretch mechanism. Two rounds of CSS-only attempts -
   flex's align-self: stretch, then CSS grid's own stretch, both combined
   with width: auto/aspect-ratio: 1/1 - measured correctly in a desktop
   browser test each time but never reproduced on-device (iOS WebKit); the
   exact divergence was never pinned down. A row measuring its own already-
   laid-out height in JS and publishing it as a definite pixel value
   sidesteps that whole class of ambiguity - there's no stretch/intrinsic-
   ratio interaction left for an engine to disagree about.

   Global, not scoped in each component itself: Vue's scoped-CSS :global()
   can't express "class on <html>, selector on a component-scoped element"
   (see the html.ionic-native-header-blur comment above for why - same
   :global() limitation, confirmed via @vue/compiler-sfc). Specificity:
   two classes + one element beats each component's own scoped
   `.xxx-cover[data-v-xxx]` (also two classes) regardless of source order,
   so no !important needed. */
html.full-height-covers .release-cover {
    width: var(--cover-size, 56px);
    height: var(--cover-size, 56px);
    object-fit: contain;
}
html.full-height-covers .artist-cover,
html.full-height-covers .artist-cover--empty {
    width: var(--cover-size, 48px);
    height: var(--cover-size, 48px);
    object-fit: contain;
}
html.full-height-covers .pp-cover,
html.full-height-covers .pp-cover--empty {
    width: var(--cover-size, 40px);
    height: var(--cover-size, 40px);
    object-fit: contain;
}
html.full-height-covers .manage-row-cover,
html.full-height-covers .manage-row-cover--empty {
    width: var(--cover-size, 44px);
    height: var(--cover-size, 44px);
    object-fit: contain;
}

/* "Kompakte Zeilen" toggle (side menu, operator-persisted - see
   useAppSettings' compactRows / applyCompactRowsMode). zoom is a non-
   standard-but-universally-supported (Chromium, WebKit, Firefox 126+)
   property that rescales an element's rendering AND the layout space it
   occupies together - unlike transform: scale(), which only rescales the
   painted pixels and leaves the original box size (and any surrounding
   gap) untouched. Every px value inside these rows - padding, gap, font
   sizes, badge sizes, and (via useFullHeightCoverSize's own offsetHeight
   measurement, which reads the already-zoomed size) the cover too if
   "Cover & Avatare volle Höhe" is also on - shrinks together in one shot,
   instead of hand-scaling dozens of individual declarations across four
   component stylesheets. 0.75 (not 0.5 - tried first, felt too small)
   still noticeably more rows per screen without the text/badges getting
   hard to read.
   Deliberately NOT applied to PlaylistPicker.vue's .pp-row - see
   useAppSettings' compactRows comment for why (its row height is a fixed
   pixel value the virtualizer computes and positions against, not
   content-driven like these three, so zooming its content alone would
   just leave gaps between rows without also scaling that separately). */
html.compact-rows .release-item,
html.compact-rows .artist-item,
html.compact-rows .manage-row {
    zoom: 0.75;
}

/* Thin fixed overlay indicator: subtle when idle, brighter + wider while
   scrolling, brighter still while being dragged. Square corners. */
.overlay-scrollbar[data-v-9648eefb] {
    position: fixed;
    width: 3px;
    /* z-index comes from the :style binding (the zIndex prop) - static
       CSS can't win here, see the prop's own comment. */
    pointer-events: none;
    transition: width 0.16s ease;
}
.overlay-scrollbar.is-scrolling[data-v-9648eefb] {
    width: 5px;
}
.overlay-scrollbar.is-dragging[data-v-9648eefb] {
    width: 7px;
}
.overlay-scrollbar-thumb[data-v-9648eefb] {
    position: absolute;
    right: 0;
    top: 0;
    width: 100%;
    background: rgba(255, 255, 255, 0.26);
    transition: background 0.3s ease;
    /* draggable: the thumb takes pointer events (track stays pass-through)
       and blocks the browser from scrolling the page mid-drag */
    pointer-events: auto;
    touch-action: none;
}
/* Widen the touch target well past the 3-7px visible bar. */
.overlay-scrollbar-thumb[data-v-9648eefb]::before {
    content: '';
    position: absolute;
    top: -8px;
    bottom: -8px;
    right: -8px;
    left: -16px;
}
.overlay-scrollbar.is-scrolling .overlay-scrollbar-thumb[data-v-9648eefb] {
    background: rgba(255, 255, 255, 0.5);
}
.overlay-scrollbar.is-dragging .overlay-scrollbar-thumb[data-v-9648eefb] {
    background: rgba(255, 255, 255, 0.7);
}

/* Full-bleed sheet, top edge at viewport y=0 (same as the side-menu
   drawer) - not an inset iOS card. Without this the modal can present
   shorter, leaving a dead frosted band above the toolbars on top of the
   --glass-safe-pad margin. */
ion-modal[data-v-070bcf7c] {
    --height: 100%;
    --width: 100%;
    --border-radius: 0;
}
/* Ionic's global rule `ion-header ion-toolbar:first-of-type { padding-top:
   var(--ion-safe-area-top) }` adds a whole status-bar inset on top of
   .pp-header's own padding-top calc below - double-counted, that's the big
   dead band above the toolbars. .pp-header handles the inset itself, so
   zero Ionic's here (scoped selector out-specifies the global one). */
.pp-header ion-toolbar[data-v-070bcf7c]:first-of-type {
    padding-top: 0;
}
/* iOS-native frosted header: same recipe family as the side menu and the
   sticky week separator (see style.css / ReleaseList.vue). Positioned
   absolutely over a full-bleed scroll area so the playlist list shimmers
   through it. Two toolbars (title + search) ~= 104px. */
.pp-header[data-v-070bcf7c] {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 10;
    /* Ionic's first-toolbar safe-area padding is zeroed (rule below), so
       this calc is the whole top offset: the status-bar inset plus
       var(--glass-safe-pad) (style.css, default 48px, operator-zeroable),
       the measured iOS blur-feather margin. */
    padding-top: calc(env(safe-area-inset-top) + var(--glass-safe-pad, 48px));
    /* Shared blur tokens (style.css) - same frosted recipe as the app
       header / week bar / tab bar. */
    -webkit-backdrop-filter: saturate(var(--blur-saturate, 160%)) blur(var(--blur-amount, 22px));
    backdrop-filter: saturate(var(--blur-saturate, 160%)) blur(var(--blur-amount, 22px));
    background: var(--blur-bg, rgba(18, 18, 18, 0.6));
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.pp-header ion-toolbar[data-v-070bcf7c] {
    --background: transparent;
    --border-width: 0;
}
.pp-header ion-searchbar[data-v-070bcf7c] {
    --background: rgba(255, 255, 255, 0.08);
}
/* Frosted bottom bar - same blur recipe as .pp-header. Height is one
   "tab" row + the home-indicator safe area. */
.pp-footer[data-v-070bcf7c] {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: center;
    height: calc(48px + env(safe-area-inset-bottom));
    padding-bottom: env(safe-area-inset-bottom);
    -webkit-backdrop-filter: saturate(var(--blur-saturate, 160%)) blur(var(--blur-amount, 22px));
    backdrop-filter: saturate(var(--blur-saturate, 160%)) blur(var(--blur-amount, 22px));
    background: var(--blur-bg, rgba(18, 18, 18, 0.6));
    border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.pp-to-top[data-v-070bcf7c] {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 40px;
    padding: 0;
    border: 0;
    background: transparent;
    color: #e8e8e8;
    cursor: pointer;
    transition: opacity 0.2s ease;
}
.pp-to-top.is-dim[data-v-070bcf7c] {
    opacity: 0.32;
}
.pp-scroll[data-v-070bcf7c] {
    position: absolute;
    inset: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    /* Hide the native scrollbar - <OverlayScrollbar> replaces it. */
    scrollbar-width: none;
    /* clears the two toolbars (~104px) + the hint row (~40px) + the
       status-bar inset + the blur-feather margin the header now also pads
       by (see .pp-header / --glass-safe-pad) */
    padding-top: calc(146px + env(safe-area-inset-top) + var(--glass-safe-pad, 48px));
    /* clears the frosted bottom bar (.pp-footer) + home indicator */
    padding-bottom: calc(48px + env(safe-area-inset-bottom) + 8px);
    background: #121212;
}
.pp-scroll[data-v-070bcf7c]::-webkit-scrollbar {
    width: 0;
    height: 0;
    display: none;
}
.pp-hint[data-v-070bcf7c] {
    margin: 0;
    padding: 10px 16px 12px;
    font-size: 11px;
    line-height: 1.35;
    color: #8a8a8a;
}
.pp-loading[data-v-070bcf7c] {
    padding: 24px 16px;
    color: #8a8a8a;
    font-size: 14px;
    text-align: center;
}
.pp-row[data-v-070bcf7c] {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 0 16px;
    border-bottom: 1px solid #1c1c1c;
    cursor: pointer;
}
.pp-cover[data-v-070bcf7c] {
    width: 40px;
    height: 40px;
    border-radius: 4px;
    object-fit: cover;
    flex-shrink: 0;
    background: #262626;
}
.pp-cover--empty[data-v-070bcf7c] {
    background: #262626;
}
.pp-meta[data-v-070bcf7c] {
    min-width: 0;
}
.pp-name[data-v-070bcf7c] {
    font-size: 15px;
    color: #fff;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.pp-sub[data-v-070bcf7c] {
    font-size: 11px;
    color: #737373;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ion-menu is a fixed-position overlay too (see style.css's #topBar
   comment) - without an explicit z-index it sits below the header/tab bar
   in the stacking order, so its own header and first item render hidden
   underneath them while open. */
ion-menu[data-v-73806140] {
    z-index: 2000;
}
/* iOS-native translucent UISplitViewController-style drawer (custom-blur
   mode only - the ion-menu/.menu-glass/toolbar/content/footer overrides
   for this and "Native Statusbar-Blur" toggle live in style.css, not
   here: Vue's scoped-CSS :global() only rewrites a single wrapped
   selector, not a leading combinator in front of more selector text - a
   rule written here as `:global(html:not(...)) ion-menu` silently
   compiles to the useless `html:not(...) { ... }`, confirmed directly via
   @vue/compiler-sfc, no build error, just wrong output). See the
   .menu-glass comment in the template for why the app blur is on that
   sibling layer and NOT on ::part(container). */
/* App green instead of Ionic's default blue for the on-state. */
ion-toggle[data-v-73806140] {
    --track-background-checked: #1ed760;
}
.menu-glass[data-v-73806140] {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
}
.menu-edge[data-v-73806140] {
    position: absolute;
    right: 0;
    top: calc(env(safe-area-inset-top) + var(--glass-safe-pad, 48px));
    bottom: 0;
    width: 1px;
    z-index: 20;
    background: rgba(255, 255, 255, 0.12);
    pointer-events: none;
}
/* Header + footer are absolute overlays (not in the flex flow) so the
   list scrolls the full height of the drawer and stays visible behind
   their frosted glass at both edges - iOS-native. With .menu-inner's
   contain dropped, their backdrop-filter is a plain sibling filter that
   genuinely re-blurs the scrolled list + .menu-glass behind them. The
   list's matching top/bottom padding comes from --menu-head-h /
   --menu-foot-h (onMounted). Positioning stays the same in native mode too
   (still overlaid on the list, same padding math) - only the frosted-glass
   background (custom mode) / solid Ionic fallback (native mode) is set in
   style.css, see this file's ion-menu comment above for why. */
ion-menu ion-header[data-v-73806140],
ion-menu ion-footer[data-v-73806140] {
    position: absolute;
    left: 0;
    right: 0;
    z-index: 10;
}
ion-menu ion-header[data-v-73806140] {
    top: 0;
}
ion-menu ion-footer[data-v-73806140] {
    bottom: 0;
    /* Separator between the scroll area and the footer / usage bars -
       matches the header's own bottom hairline. */
    border-top: 1px solid rgba(255, 255, 255, 0.12);
}
ion-menu ion-header ion-toolbar[data-v-73806140] {
    padding-top: calc(env(safe-area-inset-top) + var(--glass-safe-pad, 48px));
}
ion-menu ion-content[data-v-73806140] {
    position: absolute;
    inset: 0;
    z-index: 1;
    --padding-top: var(--menu-head-h, 120px);
    --padding-bottom: var(--menu-foot-h, 160px);
}
/* Hide ion-content's native scrollbar - <OverlayScrollbar> replaces it. */
ion-menu ion-content[data-v-73806140]::part(scroll) {
    scrollbar-width: none;
}
ion-menu ion-content[data-v-73806140]::part(scroll)::-webkit-scrollbar {
    width: 0;
    height: 0;
    display: none;
}
/* Spotify's own account menu (side menu, list of plain action rows) uses
   tall borderless rows with generous icon/text spacing - Ionic's ios-mode
   ion-item default is denser with a divider line between items. */
ion-list[data-v-73806140] {
    padding-top: 8px;
    --background: transparent;
    --ion-item-background: transparent;
}
ion-item[data-v-73806140] {
    --min-height: 56px;
    --padding-start: 20px;
    --padding-end: 20px;
    --inner-padding-end: 0;
    --inner-border-width: 0;
    /* Rows carry no fill of their own - the frosted menu surface shows
       straight through. Only a tap flashes: a brief white wash (iOS-style
       press feedback) via --background-activated. */
    --background: transparent;
    --background-hover: transparent;
    --background-focused: transparent;
    --background-activated: #fff;
    --background-activated-opacity: 0.16;
    --ripple-color: rgba(255, 255, 255, 0.22);
    font-size: 17px;
}
ion-item ion-icon[data-v-73806140] {
    font-size: 22px;
    margin-inline-end: 20px;
}
/* Settings rows (value on the right) sit in one inset iOS-style card,
   pulled in from both edges so they read as a distinct group under the
   plain action rows above. */
.menu-group[data-v-73806140] {
    margin: 8px 14px 4px;
    border-radius: 14px;
    overflow: hidden;
    background: rgba(255, 255, 255, 0.055);
}
.menu-group ion-item[data-v-73806140] {
    --padding-start: 16px;
    --padding-end: 12px;
    --min-height: 52px;
    --background: transparent;
    font-size: 16px;
}
.menu-group ion-item ion-icon[data-v-73806140] {
    font-size: 20px;
    margin-inline-end: 16px;
}
/* Hairline between rows within the card (not after the last one). */
.menu-group ion-item[data-v-73806140]:not(:last-child) {
    --inner-border-width: 0 0 0.5px 0;
    --border-color: rgba(255, 255, 255, 0.09);
}
/* Full-width so the whole row (label + value) is the tap target, not
   just the value on the right. label-placement="start" keeps the label
   left; ion-select pushes the value + chevron to the end itself. */
.menu-group ion-select[data-v-73806140],
.menu-language ion-select[data-v-73806140] {
    --padding-end: 0;
    --padding-start: 0;
    width: 100%;
}
.menu-group ion-select[data-v-73806140]::part(label),
.menu-language ion-select[data-v-73806140]::part(label) {
    flex: 1;
    color: #fff;
}
.menu-group ion-select[data-v-73806140]::part(text),
.menu-language ion-select[data-v-73806140]::part(text) {
    color: #8a8a8a;
}
.menu-group-value[data-v-73806140] {
    font-size: 13px;
    color: #8a8a8a;
}
/* Collapsed status-bar/glass dev tools - indented under their parent row. */
.glass-submenu ion-item[data-v-73806140] {
    --padding-start: 32px;
    --background: rgba(255, 255, 255, 0.03);
    font-size: 15px;
}
/* Logo replaces the old "Menu" ion-title - same lockup asset as the main
   header (see AppHeader.vue). */
.menu-logo[data-v-73806140] {
    height: 26px;
    width: auto;
    margin-inline-end: 20px;
    vertical-align: middle;
}
/* Build number lives here now instead of the header - pinned to the bottom
   of the menu via ion-footer. (--background is dead weight here either
   way - ion-footer never reads it, see style.css's ion-menu ion-footer
   comment for the actual background source in each mode.) */
.menu-footer[data-v-73806140]::before {
    /* Kill ion-footer's default iOS-mode top hairline. */
    display: none;
}
.menu-account[data-v-73806140] {
    padding: 12px 20px 2px;
    cursor: pointer;
}
.menu-account-name[data-v-73806140] {
    font-size: 14px;
    font-weight: 600;
    color: #fff;
}
.menu-account-sub[data-v-73806140] {
    font-size: 11px;
    font-weight: 400;
    color: #737373;
    word-break: break-word;
}
.menu-userid[data-v-73806140] {
    padding: 6px 20px 2px;
    font-size: 9px;
    font-weight: 400;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    color: #737373;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    cursor: pointer;
    -webkit-user-select: text;
    user-select: text;
}
.menu-build[data-v-73806140] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 2px 20px calc(12px + env(safe-area-inset-bottom));
    font-size: 12px;
    font-weight: 400;
    color: #737373;
}
.menu-build-info[data-v-73806140] {
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.menu-build-date[data-v-73806140] {
    margin-left: 5px;
    font-size: 10px;
    opacity: 0.85;
}
/* "App aktualisieren" - green-outline pill down here next to the build
   number, with a red-dot badge; only rendered when a new build is
   waiting. Tapping applies it (SKIP_WAITING -> reload). */
.menu-update-btn[data-v-73806140] {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    flex-shrink: 0;
    padding: 4px 10px;
    border: 1px solid #1ed760;
    border-radius: 14px;
    background: transparent;
    color: #1ed760;
    font-size: 11px;
    font-weight: 700;
    font-family: inherit;
    letter-spacing: 0.2px;
    cursor: pointer;
}
.menu-update-btn ion-icon[data-v-73806140] {
    font-size: 13px;
}
.menu-update-btn[data-v-73806140]:active {
    background: rgba(30, 215, 96, 0.12);
}
/* Operator-only usage bars. */
.menu-usage[data-v-73806140] {
    width: 100%;
    box-sizing: border-box;
    padding: 8px 20px 6px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.07);
    margin-bottom: 6px;
    overflow: hidden;
}
.menu-usage-row[data-v-73806140] {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 6px;
    margin: 3px 0;
    min-width: 0;
    overflow: hidden;
}
.menu-usage-label[data-v-73806140] {
    flex: 0 0 58px;
    min-width: 0;
    font-size: 10px;
    font-weight: 600;
    color: #8a8a8a;
    letter-spacing: 0.2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.menu-usage-track[data-v-73806140] {
    flex: 1 1 0;
    min-width: 0;
    height: 4px;
    border-radius: 2px;
    background: rgba(255, 255, 255, 0.1);
    overflow: hidden;
}
.menu-usage-fill[data-v-73806140] {
    display: block;
    height: 100%;
    border-radius: 2px;
    background: #1ed760;
    transition: width .3s ease;
}
.menu-usage-fill.is-warn[data-v-73806140] { background: #ffb020;
}
.menu-usage-fill.is-crit[data-v-73806140] { background: #ff3b30;
}
/* one fixed-width block, right-aligned internally, so every row's track
   ends at the same x and the row can never overflow the menu; the suffix
   uses a smaller honest font (no transform) */
.menu-usage-pct[data-v-73806140] {
    flex: 0 0 90px;
    min-width: 0;
    display: flex;
    align-items: baseline;
    justify-content: flex-end;
    gap: 5px;
    overflow: hidden;
    font-variant-numeric: tabular-nums;
    color: #8a8a8a;
    white-space: nowrap;
}
.menu-usage-main[data-v-73806140] {
    flex: 0 0 40px;
    min-width: 0;
    text-align: right;
    font-size: 10px;
}
.menu-usage-sub[data-v-73806140] {
    flex: 0 0 44px;
    min-width: 0;
    text-align: right;
    font-size: 8px;
    color: #6b6b6b;
}
/* "Ruhe bis" has no icon of its own - it's a continuation of the "Ruhe ab"
   row above it. Indent its label to line up with the icon'd rows. */
.menu-item-quiet-end ion-select[data-v-73806140]::part(label) {
    margin-inline-start: 36px;
}
.menu-update-dot[data-v-73806140] {
    position: absolute;
    top: -3px;
    right: -3px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #ff3b30;
    box-shadow: 0 0 0 2px #121212;
}

.ns-root[data-v-5875b1ab] {
    position: relative;
    display: inline-flex;
    align-items: center;
    /* dark form-control chrome for the (hidden) <select> + its native menu */
    color-scheme: dark;
}
.ns-content[data-v-5875b1ab] {
    pointer-events: none;
    display: inline-flex;
    align-items: center;
}
/* Transparent hit target filling the pill - the <select> opens the native
   menu from here, the <button> opens the popover. */
.ns-overlay[data-v-5875b1ab] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: transparent;
    opacity: 0;
    -webkit-appearance: none;
    appearance: none;
    cursor: pointer;
}
.ns-overlay option[data-v-5875b1ab] {
    background: #1c1c1c;
    color: #fff;
}
.ns-menu[data-v-5875b1ab] {
    display: flex;
    flex-direction: column;
    overflow-x: hidden;
    padding: 4px 0;
}
.ns-menu--scroll[data-v-5875b1ab] {
    max-height: 58vh;
    overflow-y: auto;
    scrollbar-width: none;
}
.ns-menu--scroll[data-v-5875b1ab]::-webkit-scrollbar {
    width: 0;
    height: 0;
    display: none;
}
/* Extra breathing room from the overlay bar sitting near the edge -
   without this the checkmark/text runs right up against it. */
.ns-menu--scroll .ns-row[data-v-5875b1ab] {
    padding-right: 24px;
}
.ns-row[data-v-5875b1ab] {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 12px 16px;
    border: 0;
    background: transparent;
    color: #fff;
    font-size: 13px;
    text-align: left;
    cursor: pointer;
}
/* Ellipsis instead of horizontal scroll for a long value (a user-named
   playlist can run well past what --max-width leaves room for) - flex
   items default to min-width:auto (their own text's full width), which
   ignores the row's actual available space entirely; min-width:0 lets it
   actually shrink down to that before the overflow rules below kick in. */
.ns-row-label[data-v-5875b1ab] {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ns-row[data-v-5875b1ab]:active {
    background: rgba(255, 255, 255, 0.06);
}
.ns-row[data-v-5875b1ab]:disabled {
    color: #6b6b6b;
    cursor: default;
}
.ns-row.is-sel[data-v-5875b1ab] {
    color: #1ed760;
}
.ns-check[data-v-5875b1ab] {
    margin-left: auto;
    font-size: 15px;
    color: #1ed760;
    flex-shrink: 0;
}

.lf-trigger[data-v-dc70e048] {
    min-height: 26px;
    padding: 4px 8px;
    border: 1px solid #3a3a3a;
    border-radius: 14px;
    background: transparent;
    color: #b3b3b3;
    font-size: 13px;
}
.lf-trigger--active[data-v-dc70e048] {
    border-color: #1ed760;
}

#topBar[data-v-84ea5167] {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1000;
}
/* The header carries no backdrop of its own - it reads best fully
   transparent, letting the plain page tone show through (content scrolled
   right under the top edge is rare and brief). Toolbars transparent, no
   per-toolbar hairline, in every mode. */
.title-toolbar[data-v-84ea5167], #searchToolbar[data-v-84ea5167] {
    --background: transparent;
    --padding-start: 16px;
    --padding-end: 16px;
    --border-width: 0;
}
/* The pinned release-week, rendered inside <ion-header> (see template).
   No backdrop of its own - in native-blur mode Ionic's .header-background
   already blurs the whole header box including this row; the fallback
   below covers the non-native case. Text style matches
   ReleaseList.vue's .release-week-separator. */
.header-week[data-v-84ea5167] {
    /* MUST be positioned: a static block paints before ion-header's
       shadow .header-background (the translucent-mode blur layer), so its
       backdrop-filter would sample this row and blur the 11px text into
       nothing. Positioned -> painted after it -> stays crisp, same as the
       toolbars. */
    position: relative;
    z-index: 1;
    padding: 8px 16px;
    color: #737373;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    /* No own background/backdrop - this row sits inside #topBar, which
       already paints the shared frosted surface (.native-blur-surface).
       Stacking a second tint here just read as an opaque slab. */
    background: transparent;
}
.title-toolbar[data-v-84ea5167] {
    /* This toolbar is the first thing in #topBar - #topBar's fixed box only
       extends as tall as its content, so THIS padding is what actually
       grows that box (and therefore the .native-blur-surface background/
       blur painted on it) up under the status bar/notch in the first
       place. The title text itself just ends up centered lower in the now-
       taller toolbar, clear of the status bar icons. var(--glass-safe-pad)
       (style.css, default 48px, operator-zeroable via the side-menu
       toggle) adds the measured iOS status-bar blur-feather margin on top,
       so the toolbar content stays crisp below iOS's own top-edge glass. */
    padding-top: calc(env(safe-area-inset-top) + var(--glass-safe-pad, 48px));
}
/* ion-button defaults to Ionic's theme primary color (blue) unless given
   its own - every other header icon has explicit white styling somewhere,
   this one never did. */
.menu-toggle[data-v-84ea5167] {
    --color: #fff;
    position: relative;
    overflow: visible;
}
/* Red dot when a newer build is deployed and waiting (see useAppUpdate) -
   the menu itself gets an "App aktualisieren" row. */
.menu-toggle-dot[data-v-84ea5167] {
    position: absolute;
    top: 4px;
    right: 2px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: #ff3b30;
    box-shadow: 0 0 0 2px #000;
    pointer-events: none;
}
/* Logo lockup: /assets/images/logo-full.svg (icon + traced "SpotR"
   wordmark, one single source file - also used standalone/as the
   favicon's sibling asset, see logo.svg). Lives in ion-buttons slot="start"
   so it sits left-aligned; the menu toggle is in slot="end", right-aligned.
   The build number moved off the header entirely - it's shown in the side
   menu now (see AppMenu.vue). */
.brand-logo[data-v-84ea5167] {
    display: inline-block;
    height: 28px;
    width: auto;
    vertical-align: middle;
    margin-inline-start: 8px;
}
.search-row[data-v-84ea5167] {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
}
.search-row ion-searchbar[data-v-84ea5167] {
    flex: 1;
    min-width: 0;
    padding: 0;
    --background: #1c1c1c;
    --color: #fff;
    --placeholder-color: #737373;
    --icon-color: #737373;
    --clear-button-color: #737373;
    --border-radius: 8px;
}
.type-filter-toggles[data-v-84ea5167] {
    display: flex;
    gap: 4px;
    flex-shrink: 0;
    max-width: 320px;
    overflow: hidden;
    transition: max-width 0.22s ease, opacity 0.16s ease, margin-left 0.22s ease;
}
/* Searchbar focused -> cluster slides out, "…" slides in in its place. */
.search-row--focused .type-filter-toggles[data-v-84ea5167] {
    max-width: 0;
    opacity: 0;
    margin-left: -6px;
    pointer-events: none;
}
.hdr-more-btn[data-v-84ea5167] {
    flex-shrink: 0;
    width: 0;
    height: 30px;
    padding: 0;
    border: 0;
    background: transparent;
    color: #b3b3b3;
    font-size: 18px;
    overflow: hidden;
    opacity: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: width 0.22s ease, opacity 0.16s ease;
}
.search-row--focused .hdr-more-btn[data-v-84ea5167] {
    width: 30px;
    opacity: 1;
}
/* Plain buttons (not ion-button) - aria-pressed is both the a11y state and
   what this active/inactive styling keys off. */
.type-toggle-btn[data-v-84ea5167] {
    padding: 5px 10px;
    border-radius: 14px;
    border: 1px solid #3a3a3a;
    background: transparent;
    color: #737373;
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    cursor: pointer;
    white-space: nowrap;
}
/* Sort mode ("Beliebtheit") - same pill look as the type toggles, styled
   as a native <select> since it's a 2-option choice, not a boolean. */
.sort-select[data-v-84ea5167] {
    padding: 5px 8px;
    border-radius: 14px;
    border: 1px solid #3a3a3a;
    background: transparent;
    color: #737373;
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    cursor: pointer;
    /* Without this, WebKit draws the native dropdown-arrow glyph in its
       light-mode colour (near-black) regardless of the page's own dark
       styling - invisible against this dark pill. Tells it to use its
       dark-mode form-control chrome instead. */
    color-scheme: dark;
}
.sort-select option[data-v-84ea5167] {
    background: #1c1c1c;
    color: #fff;
    text-transform: none;
}
.type-toggle-btn[aria-pressed="true"][data-v-84ea5167] {
    background: rgba(30, 215, 96, 0.15);
    border-color: #1ed760;
    color: #1ed760;
}

.cm-root[data-v-02559019] {
    position: absolute;
    top: 4px;
    right: 4px;
    z-index: 2;
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.06);
    color: #b3b3b3;
    /* dark form-control chrome for the (hidden) <select> + its native menu */
    color-scheme: dark;
}
.cm-root--over-media[data-v-02559019] {
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
}
.cm-root[data-v-02559019]:active {
    background: rgba(255, 255, 255, 0.12);
}
.cm-root--over-media[data-v-02559019]:active {
    background: rgba(0, 0, 0, 0.75);
}
.cm-glyph[data-v-02559019] {
    font-size: 14px;
    pointer-events: none;
}
/* Transparent hit target filling the trigger - the <select> opens the
   native menu from here, the <button> opens the popover. */
.cm-overlay[data-v-02559019] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: transparent;
    opacity: 0;
    -webkit-appearance: none;
    appearance: none;
    cursor: pointer;
}
.cm-overlay option[data-v-02559019] {
    background: #1c1c1c;
    color: #fff;
}

/* Popover fallback rows. Scoped, but the data-v attribute travels with the
   teleported nodes so this still applies inside the overlay. */
.cm-menu[data-v-02559019] {
    display: flex;
    flex-direction: column;
    padding: 4px 0;
}
.cm-row[data-v-02559019] {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 12px 16px;
    border: 0;
    background: transparent;
    color: #fff;
    font-size: 13px;
    text-align: left;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
}
.cm-row[data-v-02559019]:active {
    background: rgba(255, 255, 255, 0.06);
}
.cm-row[data-v-02559019]:disabled {
    color: #6b6b6b;
    cursor: default;
}
.cm-row ion-icon[data-v-02559019] {
    font-size: 17px;
    flex-shrink: 0;
}
.cm-row--info[data-v-02559019] {
    cursor: default;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    margin-top: 2px;
    color: #b3b3b3;
}

.release-item[data-v-1fb9ebd0] {
    position: relative;
    display: flex;
    gap: 12px;
    align-items: center;
    background: #1c1c1c;
    border-radius: 8px;
    padding: 10px;
    margin-bottom: 10px;
    transition: background-color 0.5s ease, box-shadow 0.5s ease;
}
/* The "…" menu (top-right corner of the card) is CardMenu.vue. */
/* Jumped here from a push-notification tap - a short green pulse so it's
   obvious which card the notification was about. */
.release-item--flash[data-v-1fb9ebd0] {
    background: #24382b;
    box-shadow: 0 0 0 1px #1ed760, 0 0 16px rgba(30, 215, 96, 0.35);
}
.release-cover[data-v-1fb9ebd0] {
    width: 56px;
    height: 56px;
    border-radius: 4px;
    object-fit: cover;
    flex-shrink: 0;
    background: #2a2a2a;
}
.release-info[data-v-1fb9ebd0] {
    min-width: 0; /* lets long titles ellipsis instead of overflowing the flex row */
}
.release-info h3[data-v-1fb9ebd0] {
    margin: 0 0 2px;
    /* room for the "…" button in the card's top-right corner */
    padding-right: 22px;
    font-size: 15px;
    font-weight: 600;
    color: #fff;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.release-artist[data-v-1fb9ebd0] {
    margin: 0;
    font-size: 13px;
    color: #b3b3b3;
}
.release-date[data-v-1fb9ebd0] {
    margin: 2px 0 4px;
    font-size: 11px;
    color: #737373;
}
.release-type-badge[data-v-1fb9ebd0] {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 9px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    vertical-align: middle;
}
.release-type-album[data-v-1fb9ebd0] {
    background: rgba(94, 129, 244, 0.18);
    color: #8fa6f7;
}
.release-type-single[data-v-1fb9ebd0] {
    background: rgba(115, 115, 115, 0.22);
    color: #b3b3b3;
}
/* Which tracked source this artist comes from (Liked Songs always wins
   over a playlist, see tracked_artist_popularity in db/schema.sql) + how
   many of the user's songs are theirs (the "Beliebtheit" sort's number,
   see useReleases.ts). */
.release-meta[data-v-1fb9ebd0] {
    margin: 0 0 4px;
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}
.release-source-badge[data-v-1fb9ebd0],
.release-count-badge[data-v-1fb9ebd0] {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 9px;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 140px;
}
.release-source-badge[data-v-1fb9ebd0] {
    background: rgba(30, 215, 96, 0.14);
    color: #1ed760;
}
.release-count-badge[data-v-1fb9ebd0] {
    background: rgba(255, 255, 255, 0.08);
    color: #8a8a8a;
}
.release-info a[data-v-1fb9ebd0] {
    font-size: 12px;
    color: #1ed760;
    text-decoration: none;
    font-weight: 600;
}
.release-info a[data-v-1fb9ebd0]:hover {
    text-decoration: underline;
}

/* Hand-rolled scroll-to-top FAB - fixed, above the tab bar + home
   indicator, fades + slides in once you're well down the list. */
.to-top[data-v-c0332fec] {
    position: fixed;
    right: 14px;
    bottom: calc(56px + env(safe-area-inset-bottom) + 14px);
    z-index: 999;
    width: 42px;
    height: 42px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 50%;
    background: rgba(28, 28, 28, 0.82);
    -webkit-backdrop-filter: saturate(150%) blur(14px);
    backdrop-filter: saturate(150%) blur(14px);
    color: #e8e8e8;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
    opacity: 0;
    transform: translateY(12px);
    pointer-events: none;
    transition: opacity .2s ease, transform .2s ease;
}
.to-top.is-visible[data-v-c0332fec] {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
}
#content[data-v-c0332fec]::-webkit-scrollbar {
    width: 0;
    height: 0;
    display: none;
}
.release-empty[data-v-c0332fec] {
    margin: 0;
    padding: 24px 8px;
    color: #737373;
    font-size: 13px;
    line-height: 1.5;
    text-align: center;
}
#content[data-v-c0332fec] {
    flex-grow: 1; /* Takes remaining space */
    overflow-y: auto; /* Scrollable if needed */
    /* Hide the native scrollbar - <OverlayScrollbar> replaces it. */
    scrollbar-width: none;
    background: #121212;
    color: #fff;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    /* #topBar is fixed (App.vue publishes its live height as
       --app-content-top); ion-tab-bar is fixed too - pad the scroll area
       by both plus the iOS home-indicator safe area. */
    padding: var(--app-content-top, 128px) 16px calc(56px + env(safe-area-inset-bottom)) 16px;
}
/* flow-root instead of the default block formatting context - without it,
   a row's only child's margin (release-item's margin-bottom, the
   separator's margin-top/bottom) collapses through this wrapper into its
   own siblings instead of counting toward it, so measureElement's
   offsetHeight would come out short and rows would visually overlap. */
.virtual-row[data-v-c0332fec] {
    display: flow-root;
}
/* One per Friday release-week bucket (see groupReleasesByWeek) - the first
   one in the list sits directly under the header, no extra top margin
   needed there since #content already has its own top padding (see
   syncContentTopPadding in App.vue). */
.release-week-separator[data-v-c0332fec] {
    margin: 18px 0 10px;
    padding-top: 6px;
    border-top: 1px solid #2a2a2a;
    color: #737373;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.6px;
}
.release-week-separator.is-first[data-v-c0332fec] {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
}
/* The pinned week is drawn by AppHeader now (a row inside <ion-header>,
   sharing its one glass surface - no separate backdrop-filter box, so no
   brightness seam). Here it just becomes an invisible spacer: same box as
   any other separator (height stays stable for the virtualizer, no scroll
   jump) but not painted, so it doesn't duplicate the header's copy. */
.release-week-separator.is-pinned[data-v-c0332fec] {
    visibility: hidden;
}
/* At the very top the pinned week is the first one AND shown in the
   header - the invisible sticky spacer then just adds a dead ~23px gap
   above the first card (double the inter-card spacing). Collapse it.
   Once scrolled past week 1 the row is no longer .is-pinned and takes
   its normal height back, well above the viewport by then. */
.release-week-separator.is-pinned.is-first[data-v-c0332fec] {
    margin: 0 0 14px;
    height: 0;
    overflow: hidden;
}

.artist-item[data-v-0636cca0] {
    position: relative;
    display: flex;
    align-items: center;
    gap: 12px;
    background: #1c1c1c;
    border-radius: 8px;
    padding: 10px 12px;
    margin-bottom: 8px;
}
.artist-cover[data-v-0636cca0] {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    background: #2a2a2a;
}
.artist-cover--empty[data-v-0636cca0] {
    display: flex;
    align-items: center;
    justify-content: center;
    color: #6b6b6b;
    font-size: 22px;
}
.artist-info[data-v-0636cca0] {
    min-width: 0;
}
.artist-info h3[data-v-0636cca0] {
    margin: 0 0 3px;
    padding-right: 26px;
    font-size: 15px;
    font-weight: 600;
    color: #fff;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.artist-meta[data-v-0636cca0] {
    margin: 0 0 4px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 5px;
    font-size: 11px;
    color: #8a8a8a;
}
.am-dot[data-v-0636cca0] {
    opacity: 0.5;
}
.artist-spotify[data-v-0636cca0] {
    font-size: 12px;
    color: #1ed760;
    text-decoration: none;
    font-weight: 600;
}
.artist-spotify[data-v-0636cca0]:hover {
    text-decoration: underline;
}

#artistContent[data-v-4f14deda] {
    flex-grow: 1;
    overflow-y: auto;
    scrollbar-width: none;
    background: #121212;
    color: #fff;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    padding: var(--app-content-top, 128px) 16px calc(56px + env(safe-area-inset-bottom)) 16px;
}
#artistContent[data-v-4f14deda]::-webkit-scrollbar {
    width: 0;
    height: 0;
    display: none;
}
.artist-vrow[data-v-4f14deda] {
    display: flow-root;
}
.artist-letter-sep[data-v-4f14deda] {
    margin: 14px 0 8px;
    padding-top: 4px;
    border-top: 1px solid #2a2a2a;
    color: #737373;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.6px;
}
.artist-letter-sep.is-first[data-v-4f14deda] {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
}
/* The pinned letter is drawn by AppHeader (shares its glass surface, no
   seam - same trick as the release week). Here it's an invisible spacer. */
.artist-letter-sep.is-pinned[data-v-4f14deda] {
    visibility: hidden;
}
.artist-letter-sep.is-pinned.is-first[data-v-4f14deda] {
    margin: 0 0 12px;
    height: 0;
    overflow: hidden;
}
.artist-empty[data-v-4f14deda] {
    margin: 0;
    padding: 24px 8px;
    color: #737373;
    font-size: 13px;
    line-height: 1.5;
    text-align: center;
}

#manageContent[data-v-2768e22a] {
    flex-grow: 1;
    overflow-y: auto;
    scrollbar-width: none;
    background: #121212;
    color: #fff;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    padding: var(--app-content-top, 128px) 16px calc(56px + env(safe-area-inset-bottom)) 16px;
}
#manageContent[data-v-2768e22a]::-webkit-scrollbar {
    width: 0;
    height: 0;
    display: none;
}
.manage-toolbar[data-v-2768e22a] {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 10px;
}
.manage-segment[data-v-2768e22a] {
    display: flex;
    flex: 1;
    background: #1c1c1c;
    border-radius: 8px;
    padding: 2px;
    gap: 2px;
}
.manage-segment-btn[data-v-2768e22a] {
    flex: 1;
    padding: 7px 0;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: #8a8a8a;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
.manage-segment-btn.is-active[data-v-2768e22a] {
    background: #2f2f2f;
    color: #fff;
}
.manage-select-btn[data-v-2768e22a] {
    flex-shrink: 0;
    padding: 8px 12px;
    border: 1px solid #3a3a3a;
    border-radius: 8px;
    background: transparent;
    color: #1ed760;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
/* The source picker pill - full width, left-aligned text + a trailing
   caret so it actually reads as a dropdown (unlike the header's flag-only
   NativeSelect uses, which need no such hint), same dark chrome as
   everything else here. */
.manage-source-select[data-v-2768e22a] {
    display: flex;
    width: 100%;
    margin-bottom: 12px;
    padding: 10px 12px;
    border: 1px solid #3a3a3a;
    border-radius: 8px;
    background: #1c1c1c;
    color: #fff;
    font-size: 14px;
    font-weight: 600;
}
/* The default-slot content sits inside NativeSelect's own .ns-content
   (inline-flex, hugs its content) - needs to actually stretch across the
   pill for the label/caret to land at opposite ends. */
.manage-source-select[data-v-2768e22a] .ns-content {
    display: flex;
    align-items: center;
    width: 100%;
}
.manage-source-label[data-v-2768e22a] {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.manage-source-caret[data-v-2768e22a] {
    flex-shrink: 0;
    margin-left: 8px;
    font-size: 15px;
    color: #737373;
}
.manage-empty[data-v-2768e22a] {
    margin: 0;
    padding: 24px 8px;
    color: #737373;
    font-size: 13px;
    line-height: 1.5;
    text-align: center;
}
/* Same spinner recipe as the tab bar's sync row (AppTabBar.vue) - a plain
   "Lade …" text read as stuck/dead next to it elsewhere in the app. */
.manage-loading[data-v-2768e22a] {
    display: flex;
    justify-content: center;
    padding: 24px 8px;
}
.manage-spinner[data-v-2768e22a] {
    width: 28px;
    height: 28px;
    border: 3px solid rgba(255, 255, 255, 0.25);
    border-top-color: #1ed760;
    border-radius: 50%;
    animation: manage-spin-2768e22a 0.8s linear infinite;
}
@keyframes manage-spin-2768e22a {
to { transform: rotate(360deg);
}
}
.manage-row[data-v-2768e22a] {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 4px;
    border-bottom: 1px solid #1f1f1f;
    cursor: pointer;
}
.manage-row-check[data-v-2768e22a] {
    flex-shrink: 0;
    font-size: 22px;
    color: #4a4a4a;
}
.manage-row-check.is-sel[data-v-2768e22a] {
    color: #1ed760;
}
.manage-row-cover[data-v-2768e22a] {
    width: 44px;
    height: 44px;
    border-radius: 4px;
    object-fit: cover;
    flex-shrink: 0;
    background: #2a2a2a;
}
.manage-row-cover--empty[data-v-2768e22a] {
    background: #2a2a2a;
}
.manage-row-info[data-v-2768e22a] {
    min-width: 0;
}
.manage-row-title[data-v-2768e22a] {
    /* 15px/11px matches every other list's title/subline (ReleaseCard's
       h3/.release-artist, ArtistCard's h3/.artist-meta, PlaylistPicker's
       .pp-name/.pp-sub) - this one used to run 14px/12px, the only list
       that didn't match. */
    font-size: 15px;
    font-weight: 600;
    color: #fff;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.manage-row-subtitle[data-v-2768e22a] {
    margin-top: 2px;
    font-size: 11px;
    color: #8a8a8a;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.manage-more-btn[data-v-2768e22a] {
    display: block;
    width: 100%;
    margin-top: 8px;
    padding: 12px 0;
    border: 1px solid #3a3a3a;
    border-radius: 8px;
    background: transparent;
    color: #b3b3b3;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
/* Rendered inside AppTabBar.vue's #tabBarWrap via Teleport (see template) -
   a plain row sharing its glass surface, not a floating card, so no
   position/background/backdrop of its own here. order:1 there puts it
   above the stats row. */
.tab-action-row[data-v-2768e22a] {
    order: 1;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 16px;
    border-top: 0.5px solid rgba(255, 255, 255, 0.08);
    border-bottom: 0.5px solid rgba(255, 255, 255, 0.08);
}
.tab-action-cancel[data-v-2768e22a] {
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 50%;
    background: #2a2a2a;
    color: #fff;
    font-size: 16px;
    cursor: pointer;
}
.tab-action-btn[data-v-2768e22a] {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 10px 0;
    border: 0;
    border-radius: 10px;
    background: #1ed760;
    color: #121212;
    font-size: 13px;
    font-weight: 700;
    cursor: pointer;
}
.tab-action-btn[data-v-2768e22a]:disabled {
    opacity: 0.6;
}

/* Fixed overlay rather than living inside the release list, deliberately:
   fighting Vue's own v-for reconciliation there for a temporary loading
   node isn't worth it - a sibling overlay never touches it at all. */
.sync-indicator[data-v-9e78b6cb] {
    position: fixed;
    inset: 0;
    z-index: 900;
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    color: #fff;
    font-size: 14px;
    text-align: center;
    pointer-events: none;
    background: #121212;
}
.sync-indicator.is-visible[data-v-9e78b6cb] {
    display: flex;
}
/* Two lines (main phrase+window / percent+count) instead of one long run -
   was reading as a single overlong line next to a small spinner. */
.sync-indicator-text[data-v-9e78b6cb] {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.sync-indicator-line2[data-v-9e78b6cb] {
    opacity: 0.7;
    font-size: 0.9em;
}
.sync-spinner[data-v-9e78b6cb] {
    width: 28px;
    height: 28px;
    border: 3px solid rgba(255, 255, 255, 0.25);
    border-top-color: #1ed760;
    border-radius: 50%;
    animation: sync-spin-9e78b6cb 0.8s linear infinite;
    flex-shrink: 0;
}
.sync-spinner.is-determinate[data-v-9e78b6cb] {
    border: none;
    animation: none;
    background: conic-gradient(#1ed760 calc(var(--progress) * 1%), rgba(255, 255, 255, 0.25) 0);
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 4px));
    mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 4px));
}
@keyframes sync-spin-9e78b6cb {
to { transform: rotate(360deg);
}
}

.spotify-login-hint[data-v-eb00da6d] {
    position: fixed;
    inset: auto 0 calc(56px + env(safe-area-inset-bottom)) 0;
    z-index: 901;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    padding: 9px 16px;
    border: none;
    border-top: 1px solid rgba(255, 176, 32, 0.35);
    background: rgba(40, 32, 12, 0.94);
    color: #ffcf7a;
    font-size: 12px;
    font-family: inherit;
    text-align: center;
    cursor: pointer;
}
.spotify-login-hint[data-v-eb00da6d]:active {
    background: rgba(56, 45, 18, 0.96);
}
/* Harder error tone - this one isn't fixable by tapping, it needs a
   dashboard change; tapping just signs the dead account out. */
.spotify-login-hint--blocked[data-v-eb00da6d] {
    border-top-color: rgba(255, 59, 48, 0.4);
    background: rgba(44, 16, 14, 0.95);
    color: #ff9b91;
}
.spotify-login-hint--blocked[data-v-eb00da6d]:active {
    background: rgba(58, 22, 19, 0.97);
}
.spotify-login-hint--blocked .spotify-login-hint-dot[data-v-eb00da6d] {
    background: #ff3b30;
}
.spotify-login-hint-dot[data-v-eb00da6d] {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #ffb020;
    flex-shrink: 0;
}

.glass-probe[data-v-2c26e72e] {
    position: fixed;
    inset: 0;
    z-index: 100000;
    background: #fff;
    overflow: hidden;
}
.glass-probe-canvas[data-v-2c26e72e] {
    display: block;
}
.glass-probe-info[data-v-2c26e72e] {
    padding: 8px 10px;
    font: 10px/1.5 ui-monospace, Menlo, monospace;
    color: #000;
    border-top: 1px solid #000;
    border-bottom: 1px solid #000;
    white-space: pre-wrap;
    word-break: break-word;
}
.glass-probe-hint[data-v-2c26e72e] {
    padding: 14px 12px;
    font-size: 12px;
    line-height: 1.5;
    color: #333;
}

/* The fixed, frosted root - same recipe/pattern as AppHeader.vue's
   #topBar: a plain wrapper div carries position/z-index/.native-blur-
   surface, the actual ion-tab-bar inside is transparent and just
   normal-flow content, so the tab-sync-row above it shares this one
   glass box instead of needing (and seaming against) one of its own. */
#tabBarWrap[data-v-e2f6812e] {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1000;
    /* flex + explicit `order` below, not DOM order: PlaylistManageView.vue
       teleports its action row in here (see its .tab-action-row comment),
       always appended as the wrapper's last child regardless of where it
       sits visually - order is what actually places it above the stats
       row. */
    display: flex;
    flex-direction: column;
}
ion-tab-bar#mainTabBar[data-v-e2f6812e] {
    order: 3;
    --color: #a6a6a6;
    --color-selected: #ffffff;
    border-top: 1px solid #2a2a2a;
    padding-bottom: env(safe-area-inset-bottom);
}
/* The custom-mode-only / native-mode-only overrides for this bar's own
   blur (--background/backdrop-filter/contain) live in style.css, NOT
   here: Vue's scoped-CSS :global() only rewrites a single wrapped
   selector, not a leading combinator in front of more selector text - a
   rule written here as `:global(html:not(...)) ion-tab-bar#mainTabBar`
   silently compiles to the useless `html:not(...) { ... }` (confirmed via
   @vue/compiler-sfc directly, no build error, just wrong output). Global,
   unscoped CSS has no such restriction. */
/* Ionic's own tab selection is unusable here (see the script comment), so
   neutralise its selected colour and drive the highlight from .is-active,
   which AppTabBar sets from viewMode. */
ion-tab-button[data-v-e2f6812e] {
    --color-selected: #a6a6a6;
}
ion-tab-button.is-active[data-v-e2f6812e] {
    --color: #ffffff;
    --color-selected: #ffffff;
}
/* MUST be positioned - see AppHeader.vue's .header-week comment for why
   (a static block would paint before any translucent-mode blur layer on
   the elements around it and get blurred away). No background/backdrop
   of its own; it rides on #tabBarWrap's glass. */
.tab-sync-row[data-v-e2f6812e] {
    position: relative;
    z-index: 1;
    order: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 8px 16px;
    color: #fff;
    font-size: 12px;
    text-align: center;
    /* Hairlines both sides - top = the list/panel boundary, bottom =
       panel/tab-bar boundary, half-thickness so they read as fine rules
       not chunky dividers. */
    border-top: 0.5px solid rgba(255, 255, 255, 0.08);
    border-bottom: 0.5px solid rgba(255, 255, 255, 0.08);
}
.sync-indicator-text[data-v-e2f6812e] {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.sync-indicator-line2[data-v-e2f6812e] {
    opacity: 0.7;
    font-size: 0.9em;
}
/* Always-on per-tab count, sitting on the same glass surface directly
   above the tab bar. When the sync row above is also visible it stacks
   above this one (see the template order) - this row itself never moves. */
.tab-stats-row[data-v-e2f6812e] {
    position: relative;
    z-index: 1;
    order: 2;
    padding: 6px 16px;
    color: #a6a6a6;
    font-size: 12px;
    text-align: center;
    border-top: 0.5px solid rgba(255, 255, 255, 0.08);
    border-bottom: 0.5px solid rgba(255, 255, 255, 0.08);
}
/* Indeterminate (default) - the /me/tracks paging phase has no known total
   yet, just a plain spinning ring. Once the artist batch loop starts,
   updateSyncProgress() knows exactly how many of how many are checked and
   this switches to .is-determinate: a real percent-filled ring
   (conic-gradient wedge, sized via the --progress custom property) instead
   of pretending it's still unknown. */
.sync-spinner[data-v-e2f6812e] {
    width: 16px;
    height: 16px;
    border: 2px solid rgba(255, 255, 255, 0.25);
    border-top-color: #1ed760;
    border-radius: 50%;
    animation: sync-spin-e2f6812e 0.8s linear infinite;
    flex-shrink: 0;
}
.sync-spinner.is-determinate[data-v-e2f6812e] {
    border: none;
    animation: none;
    background: conic-gradient(#1ed760 calc(var(--progress) * 1%), rgba(255, 255, 255, 0.25) 0);
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
    mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
}
@keyframes sync-spin-e2f6812e {
to { transform: rotate(360deg);
}
}
