Bootstrap 5.3.8 (CSS, JS bundle) and Bootstrap Icons 1.13.2 are served by Nessie from frontend/vendor/ (PR #42), in folders named for their version and sent as immutable; the page asks no third party for anything, and the e2e tests fail if it does (no_cdn). To upgrade: a new folder, the pages' links and integrity, and VENDOR_FILES in sw.js. The UI uses its grid, buttons, dropdowns, modals and offcanvas panels. The sidebar becomes a drawer below the lg breakpoint.
themes.css defines each theme as a Bootstrap colour mode ([data-bs-theme="dark"], light, synthwave-neon). It sets Bootstrap's own --bs-* variables plus a few --nessie-* ones. Its second half points Bootstrap's hard-coded parts (.btn-primary, focus rings) at those variables. Off-state controls use the theme's text colour at 50%: an empty checkbox's border and a switch's knob. That gives at least 3:1 against the page and cards in every theme. The knob is an SVG data URL, which can't read CSS variables, so each theme has its own copy (including a primary-coloured focused knob in place of Bootstrap's blue). .progress-bar-striped stripes are --nessie-on-primary at 35% instead of white at 15%.
layout.css: the body is a three-row grid (header, body, player bar) as tall as the screen. Only <main id="view"> and the sidebar scroll, which is why paging scrolls #view and not window. Safe-area insets keep things clear of the notch when the app is installed on a phone. Every full-height panel on a phone starts its header below the status bar with env(safe-area-inset-top): the queue and lyrics panels, and the sidebar drawer below lg (PR #40). A new one needs the same, or its title sits under the clock in the apps. From md up, the header is a three-column grid (1fr, search, 1fr), so the search box is centred whatever sits beside it. Each part is placed by column, because the search box can be hidden. The AI button (#ai-toggle, .app-search-ai) fills the search box's right end, full height and 2.75rem wide, and an ::after reaches a little above and below the box, so it's a 44 px target. The box gets padding-right while it shows, so the browser's own clear "×" lands just before it. On touch screens (pointer: coarse) the icon is bigger and the padding wider, keeping that "×" further away; the hover colour is only for hover: hover, so a tap doesn't leave it lit. While the box is the AI prompt, the header has ai-prompting: ✨ moves to the box's start (tinted), the button becomes × "Close AI prompt" (phones have no Escape), and the input gets enterkeyhint="send". On phones the prompt then takes a row of its own. The header's right side holds Offline, the bell and the profile menu.
components.css holds everything else, grouped by component.