
    /* Design tokens (colours, spacing, type, radius, shadow, z-index, motion) live in tokens.css,
       loaded before this file - see specs/ and specs/tokens/token-reference.md (ML-198). */

    /* ML-262: Bravura (SMuFL, SIL OFL - public/fonts/Bravura-LICENSE.txt), self-hosted so notation never
       falls back to a phone font. font-display: block - a note in the wrong font is worse than a
       brief blank. Everything drawn with it comes from public/notation.js. */
    @font-face { font-family: 'Bravura'; src: url('/fonts/bravura.woff2') format('woff2'); font-display: block; }

    /* ML-430: the text font (Inter), the music symbols (Noto Music) and the icons (Material Symbols) are
       hosted here too, so no visitor's address goes to Google Fonts. The files are Google Fonts' own,
       unchanged, with the same character ranges; licences in public/fonts/*-LICENSE.txt and the third-party
       register. The icon font is font-display: block - an icon's name as words is worse than a brief blank. */
    @font-face { font-family: 'Inter'; src: url('/fonts/inter-latin.woff2') format('woff2'); font-weight: 100 900; /* token-audit-ignore: an @font-face descriptor - CSS allows no var() here */ font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
    @font-face { font-family: 'Inter'; src: url('/fonts/inter-latin-ext.woff2') format('woff2'); font-weight: 100 900; /* token-audit-ignore: an @font-face descriptor - CSS allows no var() here */ font-display: swap; unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
    @font-face { font-family: 'Noto Music'; src: url('/fonts/noto-music.woff2') format('woff2'); font-display: swap; unicode-range: U+25CC, U+2669-266F, U+1D000-1D0F5, U+1D100-1D126, U+1D129-1D1EA, U+1D200-1D245; }
    @font-face { font-family: 'Noto Music'; src: url('/fonts/noto-music-latin.woff2') format('woff2'); font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
    @font-face { font-family: 'Noto Music'; src: url('/fonts/noto-music-latin-ext.woff2') format('woff2'); font-display: swap; unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
    @font-face { font-family: 'Material Symbols Outlined'; src: url('/fonts/material-symbols-outlined.woff2') format('woff2'); font-weight: 100 700; /* token-audit-ignore: an @font-face descriptor - CSS allows no var() here */ font-display: block; }
    .notation-glyph { font-family: var(--font-notation); }
    .notation-text { font-family: var(--font-notation-text); }
    .notation-text-italic { font-style: italic; font-weight: var(--font-weight-bold); }
    .notation-text-bold { font-weight: var(--font-weight-bold); }

    /* ML-356: the reading fonts, self-hosted (SIL OFL - public/fonts/*-LICENSE.txt, listed in
       docs/third-party-providers.md). Only downloaded once someone chooses one (Settings -> Display and
       reading). Lexend is one variable font for every weight, split Latin / Latin extended like Google's. */
    @font-face { font-family: 'Lexend'; src: url('/fonts/lexend-latin.woff2') format('woff2'); font-weight: 100 900; /* token-audit-ignore: an @font-face descriptor - CSS allows no custom properties here */ font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
    @font-face { font-family: 'Lexend'; src: url('/fonts/lexend-latin-ext.woff2') format('woff2'); font-weight: 100 900; /* token-audit-ignore: an @font-face descriptor - CSS allows no custom properties here */ font-display: swap; unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
    @font-face { font-family: 'OpenDyslexic'; src: url('/fonts/opendyslexic-regular.woff2') format('woff2'); font-weight: 400; /* token-audit-ignore: an @font-face descriptor - CSS allows no custom properties here */ font-display: swap; }
    @font-face { font-family: 'OpenDyslexic'; src: url('/fonts/opendyslexic-bold.woff2') format('woff2'); font-weight: 600 700; /* token-audit-ignore: an @font-face descriptor - CSS allows no custom properties here */ font-display: swap; }

    /* ML-356: Text size scales everything sized in rem (the whole type scale); dyslexia-friendly reading
       adds letter and word spacing and turns italics off. Icon and notation fonts keep their own
       spacing - a ligature icon font breaks with letter-spacing. */
    html { font-size: var(--text-scale); }
    body { letter-spacing: var(--reading-letter-spacing); word-spacing: var(--reading-word-spacing); }
    .material-symbols-outlined, svg, .notation-glyph, .notation-text { letter-spacing: normal; word-spacing: normal; }
    em, i, cite { font-style: var(--reading-em-style); }
    /* Buttons don't inherit the page's font on their own (the browser gives them its own) - with a
       reading font chosen, every button follows it too. The standard look is left exactly as it was. */
    html[data-font] button { font-family: inherit; }
    /* With a wider reading font or larger text, a home tool tile's label shrinks to fit its (fixed) tile
       rather than being cut off - "Metronome" is the longest. */
    html[data-font] .tool-icon-btn, html[data-text] .tool-icon-btn { container-type: inline-size; }
    html[data-font] .tool-icon-btn .tool-icon-label, html[data-text] .tool-icon-btn .tool-icon-label { font-size: min(var(--font-sm), var(--tile-label-fit)); }
    /* Settings -> Display and reading: each font choice is written in its own font, each text size at its
       size, each background tint tile filled with its colour (specs/components/display-and-reading.md). */
    .font-preview-standard { font-family: var(--font-preview-standard); }
    .font-preview-lexend { font-family: var(--font-preview-lexend); }
    .font-preview-opendyslexic { font-family: var(--font-preview-opendyslexic); }
    /* ML-359: the swatch fills its whole tint tile; the name and tick sit above it */
    .bg-swatch { position: absolute; inset: 0; border-radius: inherit; }
    .bg-tile-name { position: relative; z-index: var(--z-base); }
    .bg-swatch-standard { background: var(--swatch-standard); }
    .bg-swatch-cream { background: var(--swatch-cream); }
    .bg-swatch-blue { background: var(--swatch-blue); }
    .bg-swatch-green { background: var(--swatch-green); }
    .bg-swatch-yellow { background: var(--swatch-yellow); }
    .bg-swatch-peach { background: var(--swatch-peach); }
    .radio-group.compact label.text-size-preview-large { font-size: var(--font-md); }
    .radio-group.compact label.text-size-preview-larger { font-size: var(--font-lg); }
    /* ML-359 screen: the live preview (a line of music and a sentence), two cards */
    /* Compact and pinned under the top bar (--header-h, measured by app.js) so the effect of every choice
       stays in view while you scroll the settings; the shadow lifts it off the cards passing under it. */
    .flow-card.display-preview { position: sticky; top: var(--header-h, var(--space-0)); z-index: var(--z-sticky); padding: var(--space-2) var(--space-4) var(--space-3); box-shadow: var(--shadow-md); }
    .display-preview-label { display: block; font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--label-color); margin-bottom: var(--space-1); }
    .display-preview-staff svg { display: block; width: auto; max-width: 100%; height: var(--space-10); margin: 0 auto var(--space-2); }
    .display-preview-text { margin: 0; text-align: center; }
    .display-card > :last-child { margin-bottom: 0; }
    .display-card-title { display: flex; align-items: center; gap: var(--space-2); font-size: var(--font-lg); margin: 0 0 var(--space-4); }
    .display-toggle-row { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); font-size: var(--font-md); }
    .display-toggle-help { display: block; font-size: var(--font-sm); color: var(--label-color); }

    /* Anything you can click shows the hand on a desktop - the default for every enabled button, so a
       new component can't forget it. A component's own cursor (grab, not-allowed...) still wins: this is
       an element selector, so any class rule overrides it. */
    button:not(:disabled), [role="button"]:not([aria-disabled="true"]) { cursor: pointer; }

    input, select { color-scheme: light; }
    body.dark-mode input, body.dark-mode select { color-scheme: dark; }

    input[type=number]::-webkit-inner-spin-button, 
    input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
    input[type=number] { -moz-appearance: textfield; }

    body { font-family: var(--font-sans); background: var(--bg-color); color: var(--text-color); margin: 0; padding: 0; display: flex; flex-direction: column; align-items: center; }

    /* ML-197: stops a downward drag from triggering the browser/OS's native pull-to-refresh, which
       silently reloaded the page (and lost whatever was running - most noticeably the practice
       timer) when scrolled past the top by accident. Applied only in the installed/standalone app
       (.installed-app, set on <html> in app.js once display-mode: standalone or iOS's
       navigator.standalone is detected) - a normal browser tab keeps pull-to-refresh, where it's
       expected rather than accidental. "contain" (not "none") still allows the page's own natural
       rubber-band/scroll feel, it just stops that scroll gesture from chaining up to the browser
       shell's own refresh action. Set on both html and body since either can end up being the
       actual scrolling box depending on content height/browser. */
    html.installed-app, html.installed-app body { overscroll-behavior-y: contain; }

    /* #mainContainer is body's other flex child alongside .top-bar-sticky-group (ML-53) - it needs
       the exact same explicit width:100%/max-width pairing that group already has, or its cross-axis
       size under body's align-items:center falls back to shrink-to-fit over whichever single view is
       currently display:block (every other view is display:none, so it isn't part of the calculation).
       That made the whole app's width silently vary page to page on desktop - narrower on views whose
       content doesn't need the full 500px (e.g. mainView), full 500px on wider ones (e.g. timerView) -
       invisible on mobile only because the sub-768px breakpoint already strips max-width everywhere. */
    #mainContainer { width: 100%; max-width: var(--app-max-width); }

    .top-bar-sticky-group { width: 100%; display: flex; flex-direction: column; align-items: center; position: sticky; top: 0; z-index: var(--z-header); }
    .top-bar { width: 100%; max-width: var(--app-max-width); background: var(--container-bg); padding: var(--space-2) var(--space-5); box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; box-shadow: var(--shadow-md); }
    /* ML-271: left-aligned - centred, it drifted off-centre whenever the tuner/timer icons showed on the right. */
    .top-bar-title { font-size: var(--font-md); font-weight: var(--font-weight-bold); flex-grow: 1; text-align: left; margin: 0; } /* an <h1> since ML-210 - the page heading focus moves to on view change */
    /* ML-270: the ☰ character sits 2px below the middle of its box, so it read as lower than the timer
       pill and tuner toggle next to it. Nudged up so its three lines centre on the bar like they do. */
    .top-btn-burger { transform: translateY(calc(-1 * var(--space-0-5))); }
    .top-btn { background: none; border: none; font-size: var(--font-xl); cursor: pointer; color: var(--text-color); padding: var(--space-1) var(--space-3); font-weight: var(--font-weight-bold); }
    /* Plain icon button, same visual weight as the burger menu next to it (no filled-circle "active"
       treatment) - it only ever offers to open the tuner (it disappears entirely once open, see
       renderTopTunerToggleState), so there's no on/off state left to show here either. Fixed 40px
       (not tied to the bar's own padding) - still the tallest element in a tightened bar, so this is
       the floor the rest of the row's paddings were brought down to meet, not shrunk further. */
    .top-tuner-toggle { width: 40px; height: 40px; min-width: 40px; border-radius: var(--radius-circle); display: flex; align-items: center; justify-content: center; padding: 0; }
    .top-tuner-toggle-icon { height: 20px; width: auto; }
    .top-btn-back { background: transparent; border: none; color: var(--text-color); padding: var(--space-2) var(--space-5) var(--space-2) var(--space-1); font-size: var(--font-xl); font-weight: var(--font-weight-bold); cursor: pointer; display: flex; align-items: center; }
    .hidden-btn { visibility: hidden; pointer-events: none; }
    /* The back button no longer holds its place to keep a centred title centred (ML-271): with nothing to go
       back to (Home), it's gone, and the left-aligned title starts at the edge. */
    .top-btn-back.hidden-btn { display: none; }

    .dropdown-menu { display: none; position: absolute; top: 100%; right: 20px; background: var(--container-bg); border: 1px solid var(--input-border); border-radius: var(--radius-sm); box-shadow: var(--shadow-xl); z-index: var(--z-dropdown); min-width: 200px; overflow: hidden; }
    .dropdown-menu.show { display: flex; flex-direction: column; }
    .dropdown-menu.align-edge { right: 0; }
    /* ML-291: the ☰ menu's backdrop. Always catches a tap outside the open menu (closing it, not pressing
       what's underneath); on a phone it also dims the page so the long menu keeps the focus. */
    .nav-menu-backdrop { position: fixed; inset: 0; z-index: var(--z-menu-backdrop); background: transparent; opacity: 0; visibility: hidden; transition: opacity var(--duration-fast) ease, visibility var(--duration-fast); }
    body:has(#burgerDropdown.show) .nav-menu-backdrop { opacity: 1; visibility: visible; } /* flush with its anchor's right edge instead of the default 20px in */
    /* ML-371: while the ☰ menu or a pop-up is open the page behind stays still - a swipe on the backdrop
       doesn't scroll it, and scrolling to the end of the menu or pop-up doesn't carry on into it
       (overscroll-behavior on .nav-menu / .modal-content). The gutter keeps a desktop page from shifting
       sideways as its scrollbar goes - only when there's a scrollbar to lose (--scroll-lock-gutter, set by
       a11y.js): on a page without one, a kept gutter would push it sideways instead and leave an undimmed strip. */
    html:has(#burgerDropdown.show), html:has(.modal.show) { overflow: hidden; scrollbar-gutter: var(--scroll-lock-gutter, auto); }
    .nav-menu-backdrop, .modal { overscroll-behavior: contain; }
    /* ML-201: notification centre - red dot on ☰, count badge in the menu, the Notifications list */
    #navBurgerMenuBtn { position: relative; }
    .notif-dot { position: absolute; top: 4px; right: 4px; width: 10px; height: 10px; border-radius: var(--radius-circle); background: var(--danger-color); box-shadow: var(--shadow-ring-surface); }
    .notif-count { display: inline-block; min-width: 20px; padding: 0 var(--space-2); margin-left: var(--space-2); border-radius: var(--radius-md); background: var(--danger-color); color: var(--text-on-accent); font-size: var(--font-sm); line-height: 20px; text-align: center; }
    .notifications-toolbar { display: flex; justify-content: flex-end; margin-bottom: var(--space-4); }
    .notifications-toolbar .btn-nav { width: auto; }
    .notification-item { display: block; width: 100%; box-sizing: border-box; text-align: left; font: inherit; color: var(--text-color); background: var(--input-bg); border: 1px solid var(--control-border); border-radius: var(--radius-md); padding: var(--space-4); margin-bottom: var(--space-3); cursor: pointer; }
    .notification-item.unread { border-color: var(--primary-action-strong); }
    .notification-head { display: flex; align-items: center; gap: var(--space-2); }
    .notification-unread-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: var(--radius-circle); background: var(--danger-color); }
    .notification-date { font-size: var(--font-sm); color: var(--label-color); margin-top: var(--space-0-5); }
    .notification-body { margin: var(--space-2) 0 0; white-space: pre-wrap; line-height: var(--line-height-base); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
    .notification-item.expanded .notification-body, .notification-update .notification-body { display: block; -webkit-line-clamp: unset; }
    /* ML-167: an urgent notification - the pop-up shows the whole message; the tag also marks it in the list */
    .notification-urgent .notification-body, .notification-important .notification-body { display: block; -webkit-line-clamp: unset; } /* a pop-up shows the whole message (.notification-important: the "Before you continue..." notice, ML-463) */
    .notification-urgent-tag { display: inline-block; font-size: var(--font-xs); font-weight: var(--font-weight-bold); padding: var(--space-0-5) var(--space-2); border-radius: var(--radius-pill); background: var(--status-amber-bg); color: var(--status-amber-fg); }
    .notification-head .notification-urgent-tag { margin-left: var(--space-2); }
    .notification-update { cursor: default; border-color: var(--primary-action-strong); }
    .notification-update .btn-submit { margin-top: var(--space-4); }
    .notification-release { margin-top: var(--space-3); font-size: var(--font-sm); }
    /* ML-377: avatar - a gold circle holding your initials or a drawing (public/avatars.js). The drawing's
       lines take the circle's colour; its .is-solid parts are filled with the circle's own tint so they
       hide what's behind them; .is-dot parts are filled dots. A button on the home screen, a span elsewhere. */
    .avatar { flex: none; display: inline-flex; align-items: center; justify-content: center; width: var(--touch-target); height: var(--touch-target); padding: 0; box-sizing: border-box; border-radius: var(--radius-circle); border: 2px solid var(--primary-action-strong); background: var(--primary-action-tint); color: var(--primary-action-strong); font: inherit; }
    .avatar svg { width: 80%; height: 80%; fill: none; stroke: currentColor; }
    .avatar .is-solid { fill: var(--primary-action-tint); }
    .avatar .is-dot { fill: currentColor; stroke: none; }
    .avatar-initials { font-size: var(--font-sm); font-weight: var(--font-weight-bold); }
    /* The home greeting: avatar, then the greeting with one line of encouragement under it */
    .home-greeting { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-6); } /* a clear break before Start a practice session */
    .home-greeting-text { min-width: 0; }
    .home-greeting-hello { margin: 0; font-size: var(--font-md); font-weight: var(--font-weight-bold); }
    .home-greeting-line { margin: 0; font-size: var(--font-sm); color: var(--label-color); }
    .notifications-empty { text-align: center; margin-top: var(--space-6); }
    /* The empty state's line drawing (someone relaxing with a euphonium): lines in the label colour, the
       instrument filled with the page colour so it hides the lines behind it - both follow the theme/tint. */
    .notifications-empty-art { display: block; width: 70%; height: auto; margin: 0 auto var(--space-5); fill: none; stroke: var(--label-color); }
    .notifications-empty-art .is-solid { fill: var(--container-bg); }
    .notifications-empty-title { font-size: var(--font-lg); margin: 0 0 var(--space-2); }
    .about-running-note { background: var(--input-bg); border: 1px solid var(--primary-action-strong); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4); font-size: var(--font-sm); }
    /* Menu items are <button>s (ML-210) - reset the native button look so they match the old rows. */
    .dropdown-item { width: 100%; box-sizing: border-box; text-align: left; background: none; border: none; font-family: inherit; font-size: inherit; display: block; padding: var(--space-3) var(--space-4); cursor: pointer; font-weight: var(--font-weight-bold); color: var(--text-color); text-decoration: none; }
    /* A separator belongs only between two rows that are both actually showing - :last-child alone
       isn't enough once some items are conditionally hidden via .hidden-group (e.g. qpBarMenu's Move
       up/down/Delete, hidden rather than disabled when they don't apply): the true DOM last-child can
       still be a hidden row, which left the last VISIBLE row (wrongly) keeping its own border-bottom,
       reading as a stray extra line under a menu that only has one item showing. This instead adds the
       border only when a later sibling is also visible, so whichever row is last on screen never gets
       one, regardless of how many hidden rows follow it in the DOM. */
    .dropdown-item:not(.hidden-group):has(~ .dropdown-item:not(.hidden-group)) { border-bottom: 1px solid var(--input-border); }
    .dropdown-item:hover { background: var(--input-bg); }
    /* ML-285: every ⋮ menu item has an icon before its label, like the ☰ menu - a Material Symbol, or
       one of the custom SVGs from public/icons/menu/ inlined so it takes currentColor. Grey like the
       ☰ menu's icons; a delete item's icon is red with its text. */
    .dropdown-item:has(> .dropdown-item-icon) { display: flex; align-items: center; gap: var(--space-3); }
    .dropdown-item-icon { flex: 0 0 auto; width: var(--icon-md); height: var(--icon-md); font-size: var(--icon-md); color: var(--label-color); }
    .metroBlk-tile-menu-delete .dropdown-item-icon, .account-band-menu-delete .dropdown-item-icon { color: inherit; }
    .dropdown-item-text { flex: 1 1 auto; min-width: 0; }
    /* ML-259 (also ML-222): the ☰ menu - grouped, an icon on every row, lines between groups (not
       between every row), the tools as a row of tiles, the current screen marked. See
       specs/components/dropdown-menu.md ("Navigation menu"). The ID keeps these rules above the
       generic per-row separator above. */
    /* ML-291: right edge flush with the ☰ button, i.e. the top bar's own right margin. */
    .nav-menu { right: 0; min-width: 320px; max-width: calc(100vw - var(--space-6)); max-height: calc(100dvh - var(--touch-target) - var(--space-6)); overflow-y: auto; overscroll-behavior: contain; padding: var(--space-1) 0; }
    #burgerDropdown .dropdown-item { border-bottom: none; }
    .nav-item { display: flex; align-items: center; gap: var(--space-3); min-height: var(--touch-target); font-weight: var(--font-weight-semibold); }
    .nav-item-icon { font-size: var(--icon-md); color: var(--label-color); flex: 0 0 auto; }
    .nav-item-text { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
    .nav-item-meta { flex: 0 0 auto; font-size: var(--font-sm); font-weight: var(--font-weight-normal); color: var(--label-color); }
    .nav-item-sub { font-size: var(--font-xs); font-weight: var(--font-weight-normal); color: var(--label-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .nav-item[aria-current="page"], .nav-tool[aria-current="page"] { background: var(--primary-action-tint); color: var(--primary-action-strong); }
    .nav-item[aria-current="page"] .nav-item-icon { color: var(--primary-action-strong); }
    .nav-divider { border-top: 1px solid var(--input-border); margin: var(--space-1) 0; }
    .nav-section-title { font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--label-color); padding: var(--space-1) var(--space-4) 0; }
    .nav-tools { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-1); padding: var(--space-1) var(--space-2) var(--space-2); }
    .nav-tool { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-1); min-height: var(--touch-target); padding: var(--space-2) 0; background: var(--input-bg); color: var(--text-color); border: none; border-radius: var(--radius-md); font: inherit; cursor: pointer; }
    .nav-tool .tool-icon-svg { width: var(--icon-lg); height: var(--icon-lg); }
    .nav-tool .material-symbols-outlined { font-size: var(--icon-lg); }
    .nav-tool-label { font-size: var(--font-xs); font-weight: var(--font-weight-semibold); }
    /* ML-326: Tools is its own panel inside the menu - it slides in from the right over the main one. */
    .nav-item-chevron { flex: 0 0 auto; font-size: var(--icon-md); color: var(--label-color); }
    .nav-panel-sub { animation: nav-panel-in var(--duration-base) ease; }
    @keyframes nav-panel-in { from { transform: translateX(var(--space-6)); opacity: 0; } to { transform: none; opacity: 1; } }
    /* One shared floating menu, repositioned via JS against whichever row's own "more" button opened
       it (same pattern as .metroBlk-tile-menu) - overrides .dropdown-menu's own absolute/top:100%/
       right:20px (built for the single fixed-position burger menu) so JS can freely set left/top. */
    .account-band-menu { position: fixed; top: auto; right: auto; left: auto; min-width: 160px; }
    .account-band-menu-delete { color: var(--danger-text); border-top: 1px solid var(--input-border); }

    .container { width: 100%; max-width: var(--app-max-width); background: var(--container-bg); padding: var(--space-5); box-sizing: border-box; min-height: calc(100vh - 60px); }

    /* min-height: the small-phone padding (below) left these at 41px - under the 44px touch target (ML-210) */
    .btn-large, .btn-submit, .btn-nav { display: block; width: 100%; min-height: var(--touch-target); color: var(--text-on-accent); border: none; padding: var(--space-4); font-size: var(--font-md); font-weight: var(--font-weight-bold); border-radius: var(--radius-md); cursor: pointer; margin-bottom: var(--space-4); box-sizing: border-box; text-align: center; }
    /* A plain ">" at the same weight/size language as the top bar's own "<" back button (.top-btn-back)
       - stronger than a "&rarr;" glyph, and visually pairs with it as forward's counterpart to back. */
    .btn-nav-arrow { font-size: 1.2em; font-weight: var(--font-weight-bold); }
    .btn-large { background: var(--primary-action); color: var(--primary-action-text); font-size: var(--font-lg); padding: var(--space-5); }
    .btn-submit { background: var(--primary-action); color: var(--primary-action-text); }
    .btn-submit:disabled { opacity: 0.5; cursor: not-allowed; }
    .btn-nav { background: var(--nav-action); color: var(--nav-action-text); }
    /* The confirm pop-up's action button when something is deleted for good: outlined in red, not a red slab
       (owner, 5 Oct 2026 - the filled red was more severe than the job needs). A "sure?" that deletes nothing
       (Stop round, Cancel invite) is the ordinary gold button - showConfirmModal's isDanger false. */
    .btn-submit.is-danger { background: var(--container-bg); color: var(--danger-text); border: 2px solid var(--danger-text); }
    /* Design system: three button levels, one corner radius.
       1. Primary (.btn-submit/.btn-large) - filled, var(--primary-action).
       2. Secondary/cancel (.btn-cancel, paired with .btn-nav for sizing) - hollow, same border
          colour as the primary action, so "this is the other choice next to the primary one" reads
          consistently everywhere a Cancel sits next to a Save/Submit.
       3. Tertiary (.btn-text / .btn-text-danger) - text only, underlined, no fill or border at all.
          For a low-emphasis or destructive action that shouldn't sit right next to a primary
          button where it could be hit by mistake (e.g. Delete moved into its own "Other actions"
          area below Cancel/Save, not beside them).
       Every one of these (plus .btn-edit/.btn-delete below and every icon/tile button elsewhere in
       this file) uses var(--radius-md) - this app used to mix that with fully-rounded pill buttons
       (.btn-large/.btn-submit/.btn-nav/.btn-filter, and .btn-edit/.btn-delete at a large enough
       radius to read as a pill on their short height) sitting right next to slightly-rounded ones
       (tool icons, stat cards) on the very same screen. One radius for every button, site-wide - the
       "several fully rounded, the rest slightly rounded" inconsistency was never a deliberate two-tier
       design, just drift. */
    .btn-cancel { background: transparent; border: 2px solid var(--primary-action-strong); color: var(--primary-action-strong); }
    /* No explicit color previously - a plain <button> falls back to the browser's own default text
       color (near-black) rather than inheriting anything, which reads fine in light mode but is very
       low-contrast in dark mode. .btn-text-danger already overrode this for its own case; every other
       plain .btn-text use was silently affected. */
    .btn-text { display: block; width: 100%; background: none; border: none; padding: var(--space-3); min-height: 48px; font-size: var(--font-base); font-weight: var(--font-weight-bold); text-decoration: underline; cursor: pointer; text-align: center; box-sizing: border-box; color: var(--primary-action-strong); }
    .btn-text-danger { color: var(--danger-text); }
    .btn-text:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; } /* button spec: Disabled */

    .tools-title { margin-top: 0; }
    /* A 4-column grid (ML-260): a 5th tool starts a second row of the same columns, so every tile still
       lines up with the Progress grid below. */
    /* Tool groups: Everyday / My routine / Practise / Learn - a small label over each group's row, under the Tools heading. */
    .tool-group-title { font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--label-color); margin: var(--space-3) 0 var(--space-2); }
    .tool-icon-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-3); margin-bottom: var(--space-4); }
    /* min-width: 0 + the narrow side padding keep all four tiles exactly equal on a phone - otherwise
       "Metronome" (the longest label) props its tile a few px wider than the rest and the row stops
       lining up with the Progress .dashboard-grid below (see specs/components/stat-card.md). */
    .tool-icon-btn { flex: 1 1 0; min-width: 0; aspect-ratio: 1 / 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-1); background: var(--nav-action); color: var(--nav-action-text); border: none; border-radius: var(--radius-md); cursor: pointer; box-sizing: border-box; padding: var(--space-2) var(--space-1); }
    .tool-icon-btn .material-symbols-outlined { font-size: var(--icon-xl); }
    .tool-icon-btn .tool-icon-label { font-size: var(--font-sm); font-weight: var(--font-weight-bold); }
    /* ML-378: All tools (.tools-page) - a filled ★ in the corner of each tool that's on Home. While choosing
       (.is-editing) every tile shows one (outline = not on Home), and the ones on Home take the selected look.
       The star is the tile's own text colour (it passes on the tile in both themes; gold on silver doesn't in dark). */
    .tools-page .tool-icon-btn { position: relative; }
    .tool-icon-btn .tool-fav-star { display: none; position: absolute; top: var(--space-1); right: var(--space-1); font-size: var(--icon-sm); color: inherit; }
    .tool-icon-btn.is-fav .tool-fav-star { display: block; font-variation-settings: 'FILL' 1; }
    .tools-page.is-editing .tool-icon-btn .tool-fav-star { display: block; }
    .tools-page.is-editing .tool-icon-btn.is-fav { background: var(--primary-action-tint); color: var(--primary-action-strong); }
    /* While choosing, "My Home screen" stays pinned under the top bar (--header-h, as the Display and reading
       preview) and Done stays pinned to the bottom, so both are in reach however far you scroll. */
    .tools-home-card { position: sticky; top: var(--header-h, var(--space-0)); z-index: var(--z-sticky); box-shadow: var(--shadow-md); padding: var(--space-2) var(--space-4) var(--space-3); }
    /* ⋮ in the corner of each tile in My Home screen - it has options (tap anywhere on the tile: Move earlier /
       later, Take off Home), as the app's other ⋮ menus; same place and size as the ★ on the tiles below */
    .tool-icon-btn .tool-move-icon { position: absolute; top: var(--space-1); right: var(--space-1); font-size: var(--icon-sm); color: inherit; }
    .tools-done-bar { margin-top: var(--space-4); }
    .tools-page.is-editing .tools-done-bar, .stats-page.is-editing .tools-done-bar { position: sticky; bottom: 0; z-index: var(--z-sticky); background: var(--container-bg); padding: var(--space-3) 0; }
    /* ML-387: the Stats page (.stats-page) works the same way for Home stats - a filled ★ in the corner of each
       card on Home; while choosing every card shows one and the ones on Home take the selected look (tint and
       a strong border, the text keeps its own colours). The star is the card's text colour, as on the tools. */
    .stats-page .stat-card.clickable { position: relative; }
    .stat-card .stat-fav-star { display: none; position: absolute; top: var(--space-1); right: var(--space-1); font-size: var(--icon-sm); color: var(--text-color); }
    .stat-card.is-fav .stat-fav-star { display: block; font-variation-settings: 'FILL' 1; }
    .stats-page.is-editing .stat-card .stat-fav-star { display: block; }
    .stats-page.is-editing .stat-card.clickable.is-fav { background: var(--primary-action-tint); border-color: var(--primary-action-strong); }
    /* Home's My stats sit between the greeting and Start a practice session: the greeting's break moves below them. */
    .home-greeting.has-stats { margin-bottom: var(--space-3); }
    .dashboard-grid.home-stats { margin-bottom: var(--space-6); }
    /* Custom front-page tool glyphs (Quick play, Flow) - inlined SVGs (not <img>, so `fill:
       currentColor` can pick up the button's own themed text color like the material icons already
       do) with a JS-added viewBox and preserveAspectRatio so any source aspect ratio scales to fit
       this same 32px box the material icons use, rather than stretching or overflowing the button. */
    .tool-icon-svg { width: 32px; height: 32px; fill: currentColor; }

    /* ML-260 Theory practice (specs/components/theory-quiz.md). Notation itself is Notation's SVG in
       currentColor; these only lay it out. */
    .theory-intro { color: var(--label-color); font-size: var(--font-sm); margin: 0 0 var(--space-3); }
    .theory-quiz-row { gap: var(--space-3); justify-content: flex-start; }
    .theory-quiz-row .history-details { flex: 1 1 auto; }
    .theory-quiz-icon { flex: 0 0 var(--icon-xl); display: flex; justify-content: center; align-items: center; }
    .theory-quiz-icon svg { max-width: var(--icon-xl); max-height: var(--icon-xl); width: auto; height: auto; }
    .theory-grade { display: inline-flex; gap: var(--space-1); flex: 0 0 auto; }
    .theory-grade-dot { width: var(--space-2); height: var(--space-2); border-radius: var(--radius-circle); border: 2px solid var(--control-border); box-sizing: border-box; }
    .theory-grade-dot-on { background: var(--primary-action-strong); border-color: var(--primary-action-strong); }
    .theory-grade-lg { gap: var(--space-2); }
    .theory-grade-lg .theory-grade-dot { width: var(--space-5); height: var(--space-5); }
    .theory-best-line { color: var(--label-color); font-size: var(--font-sm); text-align: center; margin: var(--space-2) 0 var(--space-3); }
    .theory-status { display: flex; justify-content: space-between; font-size: var(--font-md); font-weight: var(--font-weight-bold); font-variant-numeric: tabular-nums; margin-bottom: var(--space-2); }
    .theory-countdown { height: var(--space-1); background: var(--input-border); border-radius: var(--radius-circle); overflow: hidden; }
    .theory-countdown-fill { transform: scaleX(var(--remaining)); height: 100%; background: var(--primary-action-strong); transform-origin: left center; transition: transform var(--duration-fast) linear; }
    .theory-question { text-align: center; color: var(--label-color); font-size: var(--font-base); margin: var(--space-3) 0 var(--space-2); }
    .theory-prompt { display: flex; justify-content: center; align-items: center; color: var(--text-color); min-height: calc(var(--touch-target) * 2); }
    .theory-prompt svg { max-width: 100%; height: auto; }
    .theory-meaning { font-size: var(--font-lg); font-weight: var(--font-weight-bold); text-align: center; margin: 0; }
    /* Always takes its line, so the answer buttons don't jump down when "Not quite" appears. */
    .theory-feedback { min-height: calc(var(--font-base) * 1.5); text-align: center; color: var(--danger-text); font-weight: var(--font-weight-bold); margin: var(--space-2) 0; }
    .theory-answers { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); }
    /* ML-292: note names are 7 columns, C to B, on the 8-column grid's column width and centred (the
       way 3 tiles sit on a 4-column grid) - (full width - 7 gaps) x 7/8 + 6 gaps. The tighter gap keeps
       each button 44px+ wide on a phone. With sharps and flats on (-keyboard) it's the keyboard: sharps
       above the naturals and flats below, each in its black key's column, gaps where there isn't one.
       .note-keyboard (ML-9, the Scales key-note picker) uses the same placement. */
    .theory-answers-notes, .theory-answers-keyboard { --theory-note-gap: var(--space-1); gap: var(--theory-note-gap); grid-template-columns: repeat(7, minmax(0, 1fr)); width: calc((100% - 7 * var(--theory-note-gap)) * 7 / 8 + 6 * var(--theory-note-gap)); margin-inline: auto; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="C#"] { grid-area: 1 / 1; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="D#"] { grid-area: 1 / 2; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="F#"] { grid-area: 1 / 4; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="G#"] { grid-area: 1 / 5; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="A#"] { grid-area: 1 / 6; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="C"] { grid-area: 2 / 1; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="D"] { grid-area: 2 / 2; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="E"] { grid-area: 2 / 3; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="F"] { grid-area: 2 / 4; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="G"] { grid-area: 2 / 5; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="A"] { grid-area: 2 / 6; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="B"] { grid-area: 2 / 7; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="Db"] { grid-area: 3 / 2; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="Eb"] { grid-area: 3 / 3; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="Gb"] { grid-area: 3 / 5; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="Ab"] { grid-area: 3 / 6; }
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="Bb"] { grid-area: 3 / 7; }
    /* ML-438: the Keys quiz answers on this keyboard too; C flat (seven flats) takes the free place under C */
    :is(.theory-answers-keyboard, .note-keyboard) [data-id="Cb"] { grid-area: 3 / 1; }
    .theory-answer { min-height: var(--touch-target); display: flex; align-items: center; justify-content: center; gap: var(--space-1); padding: var(--space-2); background: var(--input-bg); color: var(--text-color); border: 2px solid var(--control-border); border-radius: var(--radius-md); font: inherit; font-size: var(--font-md); font-weight: var(--font-weight-bold); cursor: pointer; }
    .theory-answer .material-symbols-outlined { font-size: var(--icon-md); }
    /* ML-438: on the Keys keyboard a note that is no key in this round is still in its place, greyed out and not tappable */
    .theory-answer:disabled { opacity: 0.4; cursor: not-allowed; }
    .theory-answers-symbols .theory-answer { min-height: calc(var(--touch-target) * 2); }
    .theory-answers-symbols .theory-answer svg { max-width: 100%; max-height: calc(var(--touch-target) * 1.5); width: auto; height: auto; }
    /* Right/wrong is never colour alone: a tick or cross icon goes in too, and "Not quite: it's X" is announced. */
    /* ML-438: the half of a two-tap answer that is picked so far (a scale's note, or Major / Minor) - the app's selected look */
    .theory-answer[aria-pressed="true"] { border-color: var(--primary-action-strong); background: var(--primary-action-tint); color: var(--primary-action-strong); }
    .theory-answer-right { border-color: var(--success-text); color: var(--success-text); }
    .theory-answer-wrong { border-color: var(--danger-text); color: var(--danger-text); }
    .theory-results-options { color: var(--label-color); font-size: var(--font-sm); text-align: center; margin: 0 0 var(--space-3); }
    /* ML-396: a round's result is a Level 1-5 in the practice Level colours (.lv-N). Size as well as colour:
       .theory-lv-hN is N+1 units tall, the unit set by what holds it (the result's steps, the smaller
       last-rounds bars). The number sits at the foot of its bar. */
    .theory-lv-h1 { height: calc(var(--theory-lv-unit) * 2); }
    .theory-lv-h2 { height: calc(var(--theory-lv-unit) * 3); }
    .theory-lv-h3 { height: calc(var(--theory-lv-unit) * 4); }
    .theory-lv-h4 { height: calc(var(--theory-lv-unit) * 5); }
    .theory-lv-h5 { height: calc(var(--theory-lv-unit) * 6); }
    /* The result: five steps, yours ringed (.is-got), the ones above it dashed outlines (.lv-0). */
    .theory-steps { --theory-lv-unit: var(--space-5); display: flex; align-items: flex-end; justify-content: center; gap: var(--space-3); margin: var(--space-2) 0 var(--space-3); }
    .theory-step, .theory-level-bar { display: flex; align-items: flex-end; justify-content: center; padding-bottom: var(--space-1); border-radius: var(--radius-xs); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); box-sizing: border-box; font-variant-numeric: tabular-nums; }
    .theory-step { width: var(--touch-target); font-size: var(--font-md); }
    .theory-step.lv-0 { border-width: 2px; }
    .theory-step.is-got, .theory-level-slot.is-now .theory-level-bar { box-shadow: var(--focus-ring); }
    /* The last rounds: eight fixed slots (so one round is one bar-width), the date under each bar. */
    .theory-level-trend { padding-top: var(--space-2); margin-bottom: var(--space-4); }
    .theory-level-bars { --theory-lv-unit: var(--space-3); display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: var(--space-1); align-items: end; }
    .theory-level-slot { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
    .theory-level-bar { font-size: var(--font-sm); }
    .theory-level-when { font-size: var(--font-xs); color: var(--label-color); text-align: center; white-space: nowrap; }
    .theory-level-slot.is-now .theory-level-when { color: var(--text-color); font-weight: var(--font-weight-bold); }
    /* ML-396 SmartLearn's strip on the options screen: sparkle, "SmartLearn applied", Learn more. Quiet
       outline where the account doesn't have it; the gold wash and outline (.is-on) where it does. */
    .smartlearn-note { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-1) var(--space-1) var(--space-3); margin: var(--space-2) 0 var(--space-3); border: 1px solid var(--control-border); border-radius: var(--radius-md); font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--text-color); }
    .smartlearn-note.is-on { background: var(--primary-action-tint); border-color: var(--primary-action-strong); }
    .smartlearn-note .material-symbols-outlined { font-size: var(--icon-md); color: var(--primary-action-strong); }
    .smartlearn-note-text { flex: 1 1 auto; min-width: 0; }
    .smartlearn-more { flex: 0 0 auto; min-height: var(--touch-target); padding: 0 var(--space-2); background: none; border: none; font: inherit; color: var(--link-color); text-decoration: underline; cursor: pointer; }
    /* The three ways out under Again: Change options / Another quiz / Home. */
    .theory-exits { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-2); margin-top: var(--space-2); }
    /* The set pop-up: the range on a staff per clef, the lines of what's asked, what each Level takes. */
    .theory-included-staffs { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-3); color: var(--text-color); margin-bottom: var(--space-2); }
    .theory-included-staffs svg { max-width: 100%; height: auto; }
    .theory-included { margin: 0 0 var(--space-3); padding-left: var(--space-5); font-size: var(--font-sm); line-height: var(--line-height-base); }
    .theory-level-key { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--space-1); margin-bottom: var(--space-3); }
    .theory-level-key > span { display: flex; flex-direction: column; align-items: center; gap: var(--space-1); font-size: var(--font-xs); color: var(--label-color); white-space: nowrap; }
    .theory-grade-block { display: flex; flex-direction: column; align-items: center; margin-bottom: var(--space-3); }
    /* The bar labels (.chart-x-label) hang below the bars absolutely, so reserve their line here, then the usual gap before the buttons. */
    .theory-trend { padding: var(--space-2) 0 var(--space-5); margin-bottom: var(--space-4); }
    /* Eight fixed slots (the last 8 rounds), so one round is one bar-width, not the whole row. */
    .theory-trend-bars { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: var(--space-2); align-items: end; height: calc(var(--touch-target) * 2); }
    .theory-trend-bars .chart-bar-container { margin-right: 0; min-width: 0; cursor: default; }

    /* ML-176: inline filter strip (design system component - see styleguide.html "Filter strip"),
       replacing the old full-width .btn-filter button + blocking checkbox modal on both the Detailed
       stats and Session history screens. Leading icon (decorative - "All" already covers "clear
       filters", see the History/Stats renderHistoryFilterStrip/renderStatsBoxes comments in app.js)
       plus a horizontal, always-scrollable-if-needed pill track - one shared row per screen that
       needs a category filter in the future too. */
    .filter-strip { display: flex; align-items: center; gap: var(--space-3); margin: var(--space-4) 0 var(--space-5) 0; }
    /* ML-200: purely decorative (not a button), so no outline circle or tappable fill - just the icon. */
    .filter-strip-icon { position: relative; flex-shrink: 0; width: 40px; height: 40px; background: transparent; color: var(--label-color); display: flex; align-items: center; justify-content: center; }
    /* Active-count badge - only shown once a subset (not all 4) of categories is selected, same
       hidden-group toggle every other conditional badge/section in the app already uses. */
    .filter-strip-badge { position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px; padding: 0 var(--space-1); border-radius: var(--radius-pill); background: var(--primary-action); color: var(--primary-action-text); font-size: var(--font-xs); font-weight: var(--font-weight-bold); display: flex; align-items: center; justify-content: center; box-sizing: border-box; }
    .filter-strip-pills { display: flex; gap: var(--space-2); overflow-x: auto; scrollbar-width: none; cursor: grab; }
    .filter-strip-pills::-webkit-scrollbar { display: none; }
    /* Click-and-drag panning (enableDragScroll in app.js) - a mouse has no swipe/two-finger-scroll
       equivalent, and the scrollbar above is hidden, so without this a desktop user had no way to
       reach an overflowing pill at all. user-select:none while actually dragging stops the browser's
       own text-selection drag from fighting it (a plain click still works normally - see that
       function's own comment). */
    .filter-strip-pills.dragging { cursor: grabbing; user-select: none; }
    /* Unselected: plain hollow outline, same language as every other unselected chip/toggle in the
       app. Selected: tinted with that category's own colour (colorMap in app.js, via the
       --filter-pill-accent custom property set inline per pill) rather than one flat colour for
       every category, so the strip doubles as a quick-glance legend - "All" has no category colour
       of its own, so it falls back to the primary action gold instead. */
    .filter-pill { flex-shrink: 0; display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-4); border-radius: var(--radius-pill); border: 2px solid var(--control-border); background: var(--input-bg); color: var(--text-color); font-size: var(--font-sm); font-weight: var(--font-weight-bold); cursor: pointer; white-space: nowrap; }
    .filter-pill.active { border-color: var(--filter-pill-accent-text, var(--primary-action-strong)); background: color-mix(in srgb, var(--filter-pill-accent, var(--primary-action)) 15%, var(--input-bg)); color: var(--text-color); } /* ML-210: text stays --text-color - accent text on its own 15% wash failed contrast (axe); the category shows in the outline + wash */
    .filter-pill-count { font-weight: var(--font-weight-normal); opacity: 0.75; }

    .form-group { margin-bottom: var(--space-5); }
    .hidden-group { display: none !important; }
    label { display: block; margin-bottom: var(--space-2); font-weight: var(--font-weight-semibold); }
    input, select, textarea { width: 100%; padding: var(--space-3) var(--space-4); border: 2px solid var(--control-border); border-radius: var(--radius-lg); font-size: var(--font-base); font-family: inherit; box-sizing: border-box; outline: none; background: var(--input-bg); color: var(--text-color); }
    textarea { resize: vertical; }

    /* ML-190 fix: flex-shrink:0 so a wide sibling (e.g. a long toggle label in a flex row) never
       squeezes this below its fixed 50x28 box - the knob's own translateX(22px) is a fixed pixel
       offset, not relative to the pill's actual rendered width, so a squashed pill makes it poke
       out past the now-narrower right edge instead of scaling down with it. */
    .toggle-switch { position: relative; display: inline-block; flex-shrink: 0; width: 50px; height: 28px; margin: 0; }
    .toggle-switch input { opacity: 0; width: 0; height: 0; margin: 0; }
    .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--control-off-bg); transition: var(--duration-base); border-radius: var(--radius-pill); }
    .toggle-slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 4px; background-color: var(--control-knob); transition: var(--duration-base); border-radius: var(--radius-circle); box-shadow: var(--shadow-sm); }
    input:checked + .toggle-slider { background-color: var(--selection-text); }
    input:checked + .toggle-slider:before { transform: translateX(22px); }

    .radio-group { display: flex; gap: var(--space-2); flex-wrap: wrap; position: relative; }
    /* ML-260: the checkbox version is the multi-select variant (Theory clefs) - same look, several on. */
    /* The real input is hidden from sight but not from the keyboard: Tab lands on the group, the arrow keys move
       the choice, and the ring shows on its label (the :focus-visible rule further down). It used to be
       display: none, which took every Yes/No and choice row in the app out of Tab's reach. */
    .radio-group input[type="radio"], .radio-group input[type="checkbox"] { position: absolute; width: 1px; height: 1px; margin: -1px; /* token-audit-ignore: standard screen-reader-only clip pattern */ padding: 0; overflow: hidden; clip: rect(0 0 0 0); border: 0; opacity: 0; pointer-events: none; }
    .radio-group label { flex: 1 1 40%; text-align: center; padding: var(--space-3); border: 2px solid var(--control-border); border-radius: var(--radius-md); background: var(--input-bg); cursor: pointer; font-weight: var(--font-weight-bold); transition: var(--duration-base); }
    /* ML-218: the app's one "selected" look (strong-gold outline + gold tint + strong-gold text), same as
       every selectable tile - the old blue fill was a second, competing selected style. */
    .radio-group input[type="radio"]:checked + label, .radio-group input[type="checkbox"]:checked + label { border-color: var(--primary-action-strong); background: var(--primary-action-tint); color: var(--primary-action-strong); }
    .radio-group.compact label { flex: 1 1 calc(25% - 6px); padding: var(--space-3) var(--space-1); font-size: var(--font-base); }
    /* ML-359: .is-centred - words centred up and down, for options drawn at different sizes (Text size) */
    .radio-group.is-centred label { display: flex; align-items: center; justify-content: center; }
    /* ML-359: .is-fit - each option as wide as its words (a long one takes its own row, never breaks) */
    .radio-group.compact.is-fit label { flex: 1 1 auto; padding-inline: var(--space-3); white-space: nowrap; }
    /* ML-359: .is-segmented - two or three options in one inset bar (Light / Dark) */
    .radio-group.is-segmented { flex-wrap: nowrap; gap: var(--space-0-5); background: var(--input-bg); border: 1px solid var(--input-border); border-radius: var(--radius-lg); padding: var(--space-1); }
    /* margin-bottom: 0 - the page-wide label margin pushed the options up off centre in the bar */
    .radio-group.is-segmented label { flex: 1; display: flex; align-items: center; justify-content: center; gap: var(--space-2); min-height: var(--touch-target); padding: var(--space-2); margin-bottom: 0; border-color: transparent; background: transparent; color: var(--label-color); }
    .radio-group.is-segmented input[type="radio"]:checked + label { background: var(--container-bg); color: var(--text-color); box-shadow: var(--shadow-sm); }
    /* ML-359: .is-tiles - colour tiles, three to a row: the colour fills the tile (.bg-swatch), the chosen
       one gets the strong outline and a tick; its words stay the normal text colour, readable on every tint */
    .radio-group.is-tiles { display: grid; grid-template-columns: repeat(3, 1fr); }
    .radio-group.is-tiles label { position: relative; display: flex; align-items: flex-end; min-height: calc(var(--touch-target) + var(--space-4)); padding: var(--space-2); text-align: left; font-size: var(--font-sm); color: var(--text-color); }
    .radio-group.is-tiles input[type="radio"]:checked + label { color: var(--text-color); box-shadow: var(--selected-ring-inset); }
    /* (.material-symbols-outlined comes later in this file and sets display, so the tick names both classes to stay
       hidden until its tile is picked - every tile showed a tick from 0.41.0 until this was fixed) */
    .material-symbols-outlined.bg-tile-tick { position: absolute; z-index: var(--z-base); top: var(--space-1); right: var(--space-1); font-size: var(--font-lg); color: var(--primary-action-strong); display: none; }
    .radio-group.is-tiles input[type="radio"]:checked + label .bg-tile-tick { display: block; }

    /* One grid per set of cards, never one grid per row: grid-auto-rows: 1fr makes every row the
       height of the tallest card, and the single gap (row + column) is --space-3, the same gutter as
       .tool-icon-row. That's what keeps the front page lined up - the middle gap of the 2-column
       Progress grid falls exactly on the Flow/Tuner gap of the 4-column tool row, and the space
       between the two Progress rows matches it. Stacking two grids instead gives a --space-4 row gap
       (the margin) and lets each row size itself. */
    .dashboard-grid { display: grid; grid-template-columns: 1fr 1fr; grid-auto-rows: 1fr; gap: var(--space-3); margin-bottom: var(--space-4); }
    .dashboard-grid.single-col { grid-template-columns: 1fr; }
    /* ML-200: a stat card is a display by default (--container-bg, same as the page) - only a
       .clickable one gets the tappable --input-bg surface (color.md: --input-bg means "you can tap this"). */
    /* ML-203: number first, label underneath - same order as every value/selection tile (value over
       caption). Done with flex order, not by reordering the markup, so a screen reader still reads
       "Total time, 12h 30m" label-first. Detail line (.sess-count, e.g. a streak's end date) stays
       directly under its number. */
    .stat-card { display: flex; flex-direction: column; background: var(--container-bg); padding: var(--space-2) var(--space-4); border-radius: var(--radius-md); border: 1px solid var(--input-border); text-align: center; }
    /* Detailed stats: a thick left edge in the category's colour (the total card uses the text colour). */
    .stat-card.edge-total { border-left: 4px solid var(--text-color); }
    .stat-card.edge-practise { border-left: 4px solid var(--cat-practise); }
    .stat-card.edge-rehearsal { border-left: 4px solid var(--cat-rehearsal); }
    .stat-card.edge-performance { border-left: 4px solid var(--cat-performance); }
    .stat-card.edge-lesson { border-left: 4px solid var(--cat-lesson); }
    .stat-card > .value { order: 1; }
    .stat-card > .sess-count { order: 2; }
    .stat-card > .label { order: 3; }
    button.stat-card { width: 100%; font: inherit; color: var(--text-color); } /* the home cards are <button>s (ML-210) */
    .stat-card.clickable { background: var(--input-bg); border-color: var(--control-border); cursor: pointer; transition: transform var(--duration-instant) ease; }
    .stat-card.clickable:active { transform: scale(0.98); }
    /* ML-134: fixed to fit two lines regardless of how long a given card's label is - without
       this, a row of short one-line labels (e.g. "Total time") renders shorter than a row of
       longer labels that wrap (e.g. "Current practice streak"), so the two dashboard-grid rows on
       the front page didn't line up height-to-height. flex centering keeps a short one-line label
       vertically centered in that same reserved space rather than sitting flush at the top. */
    /* Sentence case (just the first letter capitalised) is written directly into each label's HTML,
       not enforced here - a text-transform: lowercase + ::first-letter: uppercase pair used to do this
       instead, but ::first-letter never applies inside a flex container (this is one, for the vertical
       centering above), so every label silently rendered fully lowercase. */
    .stat-card .label { font-size: var(--font-sm); color: var(--label-color); margin-top: var(--space-1); font-weight: var(--font-weight-bold); min-height: 2.4em; display: flex; align-items: center; justify-content: center; }
    .stat-card .value { font-size: var(--font-md); font-weight: var(--font-weight-bold); color: var(--text-color); display: flex; justify-content: center; align-items: baseline; gap: var(--space-1); font-variant-numeric: tabular-nums; }
    .stat-card .sess-count { font-size: var(--font-sm); color: var(--label-color); font-weight: var(--font-weight-normal); }

    .section-title { font-size: var(--font-md); font-weight: var(--font-weight-bold); margin: var(--space-7) 0 var(--space-2) 0; color: var(--text-color); border-bottom: 2px solid var(--input-border); padding-bottom: var(--space-1); display:flex; justify-content:space-between; align-items:center; }
    .section-title::first-letter { text-transform: uppercase; }

    /* A <button> since ML-210 (tap it for Move up / Move down) - reset the native button look. */
    .drag-handle { background: none; border: none; font-family: inherit; min-width: var(--touch-target); min-height: var(--touch-target); justify-content: center; cursor: grab; padding: 0 var(--space-3); color: var(--label-color); font-size: var(--font-md); display:flex; align-items:center; }
    .drag-handle:active { cursor: grabbing; }
    .draggable-item { transition: box-shadow var(--duration-base); display:flex; width:100%; }
    .draggable-item.dragging { opacity: 0.5; box-shadow: var(--shadow-lg); }

    .scroll-wrapper { position: relative; width: 100%; display: flex; align-items: center; margin-bottom: var(--space-1); }
    .scroll-btn { position: absolute; z-index: var(--z-float); background: var(--input-bg); border: 2px solid var(--control-border); border-radius: var(--radius-circle); width: 30px; height: 30px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text-color); font-weight: var(--font-weight-bold); box-shadow: var(--shadow-sm); }
    .scroll-btn.left { left: 45px; } .scroll-btn.right { right: 5px; } .heatmap-scroll-wrapper .scroll-btn.left { left: 5px; }
    @media (hover: none) { .scroll-btn { display: none !important; } }
    @media (hover: hover) { .scroll-btn { opacity: 0; pointer-events: none; transition: opacity var(--duration-base); } .scroll-wrapper:hover .scroll-btn { opacity: 0.8; pointer-events: auto; } .scroll-wrapper:hover .scroll-btn:hover { opacity: 1; } }

    .heatmap-wrapper { overflow-x: auto; padding-top: var(--space-6); padding-bottom: var(--space-2); width: 100%; -ms-overflow-style: none; scrollbar-width: none; }
    .heatmap-wrapper::-webkit-scrollbar { display: none; }
    .heatmap-container { display: flex; align-items: stretch; gap: var(--space-0-5); width: max-content; }
    .heat-col { display: flex; flex-direction: column; gap: var(--space-0-5); position: relative; }
    .heat-cell { width: 10px; height: 10px; border-radius: var(--radius-2xs); cursor: pointer; }
    .heat-cell.blank { background: transparent !important; pointer-events: none; }
    .h-time-0 { background: var(--heat-time-0); } .h-time-1 { background: var(--heat-time-1); } .h-time-2 { background: var(--heat-time-2); } .h-time-3 { background: var(--heat-time-3); } .h-time-4 { background: var(--heat-time-4); }
    .h-sess-0 { background: var(--heat-sess-0); } .h-sess-1 { background: var(--heat-sess-1); } .h-sess-2 { background: var(--heat-sess-2); } .h-sess-3 { background: var(--heat-sess-3); } .h-sess-4 { background: var(--heat-sess-4); }
    .month-spacer { width: 6px; } .year-spacer { width: 20px; display: flex; align-items: center; justify-content: center; position: relative; } .year-spacer span { transform: rotate(-90deg); font-weight: var(--font-weight-bold); font-size: var(--font-sm); color: var(--text-color); position: absolute; } .month-label { position: absolute; top: -18px; left: 0; font-size: var(--font-xs); color: var(--label-color); font-weight: var(--font-weight-bold); white-space: nowrap; }
    .heatmap-daylabels { display: flex; flex-direction: column; gap: var(--space-0-5); flex-shrink: 0; margin-left: var(--space-1); padding-top: var(--space-6); padding-bottom: var(--space-2); }
    .heatmap-daylabels span { width: 12px; height: 10px; line-height: 10px; font-size: var(--font-2xs); font-weight: var(--font-weight-bold); color: var(--label-color); text-align: center; }
    .heatmap-legend { display: flex; justify-content: flex-end; align-items: center; gap: var(--space-1); font-size: var(--font-sm); color: var(--label-color); margin-bottom: var(--space-5); } .heatmap-legend .heat-cell { width: 10px; height: 10px; pointer-events: none; }

    .chart-wrapper { display: flex; height: 190px; width: 100%; position: relative; overflow: hidden; background: transparent; margin-bottom: var(--space-2); }
    .chart-grid-lines { position: absolute; top: 10px; bottom: 36px; left: 42px; right: 0; display: flex; flex-direction: column; justify-content: space-between; pointer-events: none; z-index: var(--z-base); }
    /* ML-288: chart geometry arrives as custom properties set by the renderer - --line-pos (a grid
       line / y-axis label's height up the chart) and --bar-h (a bar's height) - and colour as a
       .series-* class (sets --series). Never an inline height or background. */
    .grid-line { position: absolute; width: 100%; border-bottom: 1px dashed var(--input-border); opacity: 0.5; left: 0; bottom: var(--line-pos); }
    .grid-line.is-baseline { opacity: 0; } /* the 0 line - the bars' own bottom edge already marks it */
    .series-hours { --series: var(--chart-hours); }
    .series-days { --series: var(--chart-days); }
    .series-sessions { --series: var(--chart-sessions); }
    .series-streak { --series: var(--chart-streak); }
    .chart-y-axis { width: 42px; min-width: 42px; position: sticky; left: 0; z-index: var(--z-sticky); background: transparent; }
    .chart-y-axis-container { position: absolute; top: 10px; bottom: 36px; left: 0; right: 6px; } .chart-y-axis-container span { font-size: var(--font-sm); color: var(--label-color); }
    .chart-y-label { position: absolute; bottom: var(--line-pos); right: 0; transform: translateY(50%); }
    .chart-scroll-area { display: flex; overflow-x: auto; overflow-y: hidden; flex-grow: 1; align-items: flex-end; padding: var(--space-3) var(--space-3) var(--space-8) var(--space-3); height: 100%; box-sizing: border-box; -ms-overflow-style: none; scrollbar-width: none; z-index: var(--z-raised); position: relative; }
    .chart-scroll-area::-webkit-scrollbar { display: none; }
    .chart-bar-container { display: flex; flex-direction: column; justify-content: flex-end; align-items: center; height: 100%; min-width: 14px; margin-right: var(--space-0-5); position: relative; cursor: pointer; }
    .chart-bar { width: 100%; height: var(--bar-h); background: var(--series); border-radius: var(--radius-xs) var(--radius-xs) 0 0; transition: height var(--duration-base) ease; pointer-events: none; }
    /* ML-186: the current month's "at this rate" projection - a hollow bar, outline only, in the chart's
       own colour (the same .series-* class as the fill), stacked on top
       of the real bar. No bottom edge: it continues straight on from the filled bar, whose own top
       corners square off to meet it. */
    .chart-bar-projection { width: 100%; height: var(--bar-h); box-sizing: border-box; border: 2px solid; border-bottom: none; border-color: var(--series); border-radius: var(--radius-xs) var(--radius-xs) 0 0; background: transparent; transition: height var(--duration-base) ease; pointer-events: none; }
    .chart-bar-under-projection { border-radius: 0; }
    /* ML-186: key under each monthly chart while it shows a projection - solid swatch "Actual", outline
       swatch "Projected", both in that chart's colour (a .series-* class in index.html). Same right-
       aligned, small label-colour line as .heatmap-legend. */
    .chart-legend { display: flex; justify-content: flex-end; align-items: center; gap: var(--space-4); font-size: var(--font-sm); color: var(--label-color); margin-bottom: var(--space-2); }
    .chart-legend-item { display: inline-flex; align-items: center; gap: var(--space-1); }
    .chart-legend-swatch { background: var(--series); width: var(--icon-sm); height: var(--icon-sm); box-sizing: border-box; border-radius: var(--radius-xs) var(--radius-xs) 0 0; }
    .chart-legend-swatch-projected { border: 2px solid var(--series); background: transparent; } /* full outline - in the key it isn't sitting on a solid bar */
    .chart-x-label { position: absolute; top: 100%; margin-top: var(--space-1); font-size: var(--font-xs); color: var(--label-color); text-align: center; width: 40px; left: 50%; transform: translateX(-50%); line-height: var(--line-height-snug); white-space: normal; pointer-events: none; }
    /* ML-308: a chart with a label under every bar (a category per bar, not months) - each bar is as
       wide as its label, so the labels never overlap; the row scrolls sideways as usual. */
    .chart-labelled .chart-bar-container { min-width: var(--touch-target); margin-right: var(--space-1); }
    .chart-x-year { font-size: var(--font-2xs); } /* the year under a January (or first) month label - full --label-color, no dimming (it failed 4.5:1 at 0.8 opacity, axe, 0.30.0) */

    .month-nav { display: flex; justify-content: space-between; align-items: center; background: var(--secondary-color); padding: var(--space-2); border-radius: var(--radius-md); margin-bottom: var(--space-4); }
    .month-nav button { background: none; border: none; font-size: var(--font-md); cursor: pointer; color: var(--text-color); font-weight: var(--font-weight-bold); }
    /* ML-200: a list row is a display by default (page surface + hairline outline); only the controls
       inside it are tappable. A row that is itself tappable is a <button> or carries .clickable. */
    .history-item { background: var(--container-bg); border: 1px solid var(--input-border); padding: var(--space-3); border-radius: var(--radius-md); margin-bottom: var(--space-2); display: flex; justify-content: space-between; align-items: center; border-left: 6px solid var(--control-off-bg); width:100%; box-sizing:border-box;}
    button.history-item { color: var(--text-color); font: inherit; text-align: left; }
    button.history-item, .history-item.clickable { background: var(--input-bg); border-color: var(--control-border); cursor: pointer; }
    .text-muted { color: var(--label-color); } /* ML-210: was --text-color at 60% opacity, which faded below 4.5:1 - see color.md "Don't" */
    .history-details { font-size: var(--font-base); flex-grow:1;} .history-details strong { display: block; font-size: var(--font-base); margin-bottom: var(--space-1); }
    .btn-edit { background: var(--selection-color); color: var(--text-on-accent); border: none; padding: var(--space-2) var(--space-4); min-height: 48px; border-radius: var(--radius-md); font-weight: var(--font-weight-bold); cursor: pointer; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; text-decoration: none; }
    .btn-delete { background: var(--danger-color); color: var(--text-on-accent); border: none; padding: var(--space-2) var(--space-3); min-height: 48px; border-radius: var(--radius-md); font-weight: var(--font-weight-bold); cursor: pointer; font-size: var(--font-base); box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; }
    .material-symbols-outlined { font-family: var(--font-icons); font-weight: var(--font-weight-normal); font-style: normal; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; font-feature-settings: 'liga'; -webkit-font-smoothing: antialiased; }
    .material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; font-size: var(--icon-md); line-height: var(--line-height-tight); vertical-align: middle; }
    /* 48x48 touch target minimum - was 36x36. */
    /* Icon colour matches its own border (not the hardcoded white every other button on this page
       uses) because this is the one button style with a transparent fill - every other white icon/text
       (.btn-edit, .toast, .toggle-slider) sits on a solid colour background, so white always
       contrasts. Here, "background" is whatever's actually behind the button (the page/card), which is
       white in light mode - a white pencil on that is invisible. Affects every list that uses this
       shared class (admin Features/Accounts/Bands, Manage lists, the band menu's "more" button,
       session history). */
    .btn-icon-edit { padding: 0; background: transparent; color: var(--selection-text); border: 2px solid var(--selection-text); width: 48px; height: 48px; border-radius: var(--radius-circle); cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
    /* Centred under the content above it (the list-item pop-up's Remove button). */
    .btn-icon-delete.centered-action { margin: var(--space-2) auto 0 auto; }
    .btn-icon-delete { padding: 0; background: var(--danger-color); color: var(--text-on-accent); border: none; width: 48px; height: 48px; border-radius: var(--radius-circle); cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
    .btn-icon-copy { padding: 0; background: transparent; color: var(--text-color); border: 2px solid var(--control-border); width: 48px; height: 48px; border-radius: var(--radius-circle); cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }

    .play-card { background: var(--container-bg); padding: var(--space-6) var(--space-5); border-radius: var(--radius-lg); border: 2px solid var(--primary-action-strong); text-align: center; margin-bottom: var(--space-6); transition: transform var(--duration-base) ease, opacity var(--duration-base) ease; }
    .play-card.slide-out-left, .play-card.slide-in-left { transform: translateX(-40px); opacity: 0; }
    .play-card.slide-out-right, .play-card.slide-in-right { transform: translateX(40px); opacity: 0; }
    .play-piece { font-size: var(--font-xl); font-weight: var(--font-weight-bold); margin-bottom: var(--space-1); color: var(--primary-action-strong); }
    .play-ref { font-size: var(--font-md); font-weight: var(--font-weight-bold); margin-bottom: var(--space-4); }
    .play-meta { font-size: var(--font-base); color: var(--label-color); display: flex; justify-content: center; gap: var(--space-4); margin-bottom: var(--space-3); }
    .play-stats { font-size: var(--font-sm); color: var(--label-color); background: var(--container-bg); padding: var(--space-1) var(--space-3); border-radius: var(--radius-sm); display: inline-block; }

    /* background: var(--container-bg) not --input-bg - this card isn't clickable, and --input-bg is
       reserved for tappable surfaces (the Settings boxes below, the needle track, etc.) so dark mode
       can tell "you can tap this" apart from "this is just a display" at a glance. Border is a
       3-state read on the live pitch (set by renderTunerPitch/renderTunerIdle, app.js), not a fixed
       gold outline - gold there would read as "this is selected", which isn't what this card is:
       neutral gray by default (idle/silence - .out-of-tune below overrides while a note's actually
       playing but off, .in-tune overrides again once it locks in). */
    .tuner-card { position: relative; background: var(--container-bg); padding: var(--space-6) var(--space-5); border-radius: var(--radius-lg); border: 2px solid var(--input-border); margin-bottom: var(--space-6); transition: background var(--duration-fast) linear, border-color var(--duration-fast) linear; }
    .tuner-card.out-of-tune { border-color: var(--primary-action-strong); }
    /* Opens #tunerSettingsModal (every Tuner setting) - same corner-badge placement/sizing as
       .modal-close-x, just a settings gear instead of a close X, and living, so it reads as "more
       options for this card" rather than "dismiss this card". */
    .tuner-card-settings-btn { position: absolute; top: var(--space-3); right: var(--space-3); width: 36px; height: 36px; border-radius: var(--radius-circle); border: none; background: var(--input-bg); color: var(--label-color); display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
    .tuner-card-settings-btn .material-symbols-outlined { font-size: var(--icon-md); }
    .tuner-card.in-tune { border-color: var(--tuner-in-tune-text); background: var(--tuner-in-tune-tint); }
    .tuner-pitch-row { display: flex; margin-bottom: var(--space-5); }
    .tuner-pitch-col { flex: 1; text-align: center; }
    .tuner-pitch-label { font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--label-color); margin-bottom: var(--space-2); }
    .tuner-note { font-size: var(--font-3xl); font-weight: var(--font-weight-bold); color: var(--primary-action-strong); font-variant-numeric: tabular-nums; line-height: var(--line-height-tight); }
    /* Octave (tunerShowOctave) sits right after the big letter, not below it - align-items:flex-end
       bottom-aligns it against the letter's own box instead of centring/top-aligning, and
       justify-content:center keeps the pair centred as a unit now that it's two inline elements
       instead of .tuner-note being the only thing in this column. */
    .tuner-note-row { display: flex; align-items: flex-end; justify-content: center; gap: var(--space-0-5); }
    .tuner-note-octave { font-size: var(--font-md); font-weight: var(--font-weight-bold); color: var(--primary-action-strong); line-height: var(--line-height-tight); padding-bottom: var(--space-1); }
    /* Hz (tunerShowHz) - its own small line under the letter+octave row. */
    .tuner-note-hz { font-size: var(--font-xs); color: var(--label-color); margin-top: var(--space-1); font-variant-numeric: tabular-nums; }

    .tuner-bar-wrap { margin: 0 0 var(--space-4) 0; }
    .tuner-bar-track { position: relative; height: 28px; background: var(--input-bg); border: 1px solid var(--input-border); border-radius: var(--radius-md); overflow: hidden; }
    .tuner-bar-zone { position: absolute; top: 0; bottom: 0; left: 35%; right: 35%; background: var(--tuner-in-tune-zone); }
    .tuner-bar-center-mark { position: absolute; top: 0; bottom: 0; left: 50%; width: 2px; background: var(--input-border); transform: translateX(-50%); }
    .tuner-bar-needle { position: absolute; top: 2px; bottom: 2px; left: var(--needle-pos, 50%); width: 4px; border-radius: var(--radius-2xs); background: var(--danger-color); transform: translateX(-50%); transition: left var(--duration-instant) linear, background var(--duration-instant) linear; }
    .tuner-bar-needle.in-tune { background: var(--tuner-in-tune-green); }
    .tuner-bar-scale { display: flex; justify-content: space-between; font-size: var(--font-xs); color: var(--label-color); margin-top: var(--space-1); }

    /* Real messages only (requesting mic access / access denied) - genuinely appears/disappears since
       those are real state changes, unlike the old "Listening..." toggle this used to also carry (a
       separate "still listening" indicator icon briefly replaced that toggle, then was removed
       entirely - the History card's own Pitch/Dynamics strips right below already show live activity,
       so a second one up here was redundant). No min-height reservation needed: .hidden-group
       (display:none) when there's nothing to say, rather than an empty text node holding a blank
       line's worth of space open. */
    .tuner-status { text-align: center; font-size: var(--font-base); color: var(--label-color); margin-bottom: var(--space-4); }

    /* Rounded-rect pause/play toggle (not the circular icon-button shape used elsewhere, e.g.
       .modal-close-x - a rectangle reads more like a distinct control here). Sits directly beside the
       rewind slider (.tuner-rewind-row) rather than up in the section-title, since the two are one
       control - pausing is what makes the slider mean anything. Pausing freezes the pitch+dynamics
       history strips together (setTunerPaused, app.js) so a phrase can be scrubbed back through and
       reviewed rather than only ever seen live - the "right now" card above (#tunerCard) keeps
       updating regardless, since pausing only concerns the history box. flex-shrink:0 so the slider
       (.tuner-rewind-row .slider-wrap's own flex:1) is what gives up width to this fixed 36x36, not
       the other way round. */
    .tuner-pause-btn { width: 36px; height: 36px; flex-shrink: 0; border: none; border-radius: var(--radius-md); background: var(--nav-action); color: var(--nav-action-text); display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
    .tuner-pause-btn .material-symbols-outlined { font-size: var(--icon-md); }

    .tuner-rewind-label { font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--label-color); margin: var(--space-4) 0 var(--space-2) 0; }
    /* ML-290: the pause button lines up with the slider's track line, not with the track + scale labels
       as a block (which put it visibly below the line). Top-aligned, then pushed down so its middle
       meets the track's middle: track margin-top (--space-5) + half the track (4px) - half the button
       (18px) = 6px = --space-1 + --space-0-5. */
    .tuner-rewind-row { display: flex; align-items: flex-start; gap: var(--space-3); }
    .tuner-rewind-row .tuner-pause-btn { margin-top: calc(var(--space-1) + var(--space-0-5)); }
    .tuner-rewind-row .slider-wrap { flex: 1; min-width: 0; }
    /* No border-color override needed here (unlike .metroBlk-bpm-box .slider-thumb, which sits on
       --input-bg and overrides accordingly) - .slider-thumb's own default border already targets
       var(--container-bg), which is exactly what .tuner-graph-card's background is below, so the ring
       already matches without one. An earlier override pinned this to --input-bg on the assumption
       the card used that background - once the card moved to --container-bg (same "not clickable, so
       not --input-bg" reasoning as .tuner-card above), that override became the mismatch it was meant
       to prevent. */
    /* Always visible (not hidden while live) so this card's height never changes on pause/resume -
       just non-interactive and dimmed until setTunerPaused(true) toggles this off. */
    #tunerScrubSliderWrap.disabled { opacity: 0.4; }
    #tunerScrubSliderWrap.disabled .slider-track, #tunerScrubSliderWrap.disabled .slider-thumb { pointer-events: none; }

    /* ML-181: history graphs (renderTunerHistoryGraphs, app.js) - pitch and dynamics share this one
       card (always the same time-aligned window, see tunerHistory) rather than each having their own,
       with one shared pause/rewind control between them. Same card language as .tuner-card (radius-lg)
       but a plain 1px border, not the gold "hero" treatment, so it reads as secondary/supporting
       relative to the "right now" card above it. background: var(--container-bg), not --input-bg -
       same "not a clickable surface" reasoning as .tuner-card. */
    .tuner-graph-card { background: var(--container-bg); border: 1px solid var(--input-border); border-radius: var(--radius-lg); padding: var(--space-4); margin-bottom: var(--space-5); }

    /* Each graph pairs a vertical axis (what the bar height actually encodes - cents, or loudness)
       with the graph itself, so the axis labels always read top/middle/bottom against the values they
       describe, rather than being listed left-to-right below the graph as if they were a horizontal
       scale (that used to be the case here, and was wrong - +50c/0c/-50c and pp/mf/ff are both
       vertical scales). align-items: stretch (the default) is what makes .tuner-graph-yaxis's height
       match its graph sibling, so justify-content: space-between lines its 3 labels up with the
       graph's own top/middle/bottom. */
    .tuner-graph-row { display: flex; gap: var(--space-2); }
    .tuner-graph-yaxis { display: flex; flex-direction: column; justify-content: space-between; width: 28px; flex-shrink: 0; text-align: right; font-size: var(--font-xs); color: var(--label-color); padding: var(--space-0-5) 0; box-sizing: border-box; }

    /* "Pitch"/"Dynamics" over each chart - needed now that the card's own section-title is just
       "History" and no longer names what either strip actually is. First one has no extra top margin
       (the card's own padding already provides that gap); the second's margin-top is what used to be
       .tuner-graph-row's own "+ .tuner-graph-row" spacing, before a subtitle sat between the two rows. */
    .tuner-graph-subtitle { font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--label-color); margin-bottom: var(--space-2); }
    .tuner-graph-row + .tuner-graph-subtitle { margin-top: var(--space-4); }

    /* The one genuine horizontal axis in the history card - time, shared by both graphs since they're
       always the same TUNER_HISTORY_VIEW_MS-wide window (kept updated by renderTunerHistoryGraphs,
       app.js, including while scrubbed - see #tunerGraphTimeAxis there). */
    .tuner-graph-scale { display: flex; justify-content: space-between; font-size: var(--font-xs); color: var(--label-color); margin-top: var(--space-2); }

    /* #tunerSettingsModal - every Tuner setting, in two labelled sections (Notation/Display). */
    .tuner-settings-section-label { font-size: var(--font-sm); font-weight: var(--font-weight-bold); letter-spacing: 0.03em; text-transform: uppercase; color: var(--label-color); margin: var(--space-6) 0 var(--space-3) 0; }
    .tuner-settings-section-label:first-of-type { margin-top: 0; }
    .tuner-settings-group { margin-bottom: var(--space-5); }
    .tuner-settings-group-label { font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--text-color); margin-bottom: var(--space-0-5); }
    .tuner-settings-group-sublabel { font-size: var(--font-xs); color: var(--label-color); margin-bottom: var(--space-2); }
    /* Same "opens a picker" role as .metroBlk-ctrl-value-btn, but a full-width row instead of a square tile - Tuning needs its value + a label on one
       line, not a tile grid, since it opens its own popup (a slider) rather than picking inline like
       the tile groups above it. */
    .tuner-settings-row-btn { position: relative; width: 100%; display: flex; align-items: center; justify-content: space-between; height: 52px; padding: 0 var(--space-4); margin-bottom: var(--space-5); border: 2px solid var(--control-border); border-radius: var(--radius-md); background: var(--input-bg); color: var(--text-color); font-size: var(--font-base); font-weight: var(--font-weight-bold); cursor: pointer; box-sizing: border-box; }
    .tuner-settings-row-btn span:last-child { font-weight: var(--font-weight-normal); color: var(--label-color); }

    /* #tunerTranspositionOptions - no caption under each tile (just the note letter), so the shared
       .flow-picker-tile's own min-height/reserved caption row (.flow-picker-tile-label, normally
       min-height:1em even when empty) would just be dead space here - shorter tiles, label hidden
       entirely so the letter centers in the remaining height instead of sitting above an empty gap. */
    #tunerTranspositionOptions .flow-picker-tile { min-height: 48px; padding: var(--space-2); }
    #tunerTranspositionOptions .flow-picker-tile-label { display: none; }

    /* Breathing room before Reset, matching the gap every other group in this popup gets. */
    #tunerA4ResetBtn { margin-top: var(--space-5); }

    /* #tunerSettingsModal's 3 toggle rows - same look as the equivalent rows on the main Settings
       screen (inline-styled there; a real class here since there are 3 of them in the same popup). */
    /* ML-286: Settings' switch rows and My account's read-only rows (were inline styles). Both are display
       rows - only the switch inside a setting row is tappable - so the plain surface, never --input-bg. */
    .setting-row, .info-row { display: flex; justify-content: space-between; align-items: center; background: var(--container-bg); border: 1px solid var(--input-border); border-radius: var(--radius-md); }
    .setting-row { margin-bottom: var(--space-5); font-size: var(--font-md); padding: var(--space-4); }
    .setting-row-grouped { margin-bottom: var(--space-3); }
    .info-row { margin-bottom: var(--space-3); padding: var(--space-3) var(--space-4); }
    .info-row-end { margin-bottom: var(--space-5); }
    /* ML-379: an editable info row (My details' Name, Display name, Avatar) is one button - the tappable
       surface like any tappable row. The pencil is only a sign it can be changed, so it wears the quiet grey
       circle of the list-row ⋮ (.list-item-menu-btn), not .btn-icon-edit's bold brown ring */
    button.info-row-edit { width: 100%; box-sizing: border-box; font: inherit; color: var(--text-color); text-align: left; background: var(--input-bg); border-color: var(--control-border); cursor: pointer; }
    .info-row-edit-icon { width: 40px; height: 40px; box-sizing: border-box; border: 1px solid var(--control-border); border-radius: var(--radius-circle); background: var(--input-bg); color: var(--label-color); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
    /* ML-282: Settings is a list of groups (.settings-link, a tappable list row with the neutral
       stripe), each opening its own screen (.settings-subview - starts .hidden-group like every other
       screen, and switchView shows it). .setting-field-pair: two fields side by side (practice year
       start day + month). */
    .history-item.settings-link { gap: var(--space-3); min-height: var(--touch-target); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-3); }
    .settings-link-icon { font-size: var(--icon-md); width: var(--icon-md); height: var(--icon-md); color: var(--label-color); flex: 0 0 auto; display: flex; align-items: center; justify-content: center; }
    .settings-link-icon .tool-icon-svg { width: var(--icon-md); height: var(--icon-md); }
    .settings-link-text { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
    .settings-link-title { font-weight: var(--font-weight-semibold); }
    .settings-link-sub { font-size: var(--font-sm); color: var(--label-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .settings-link-chevron { color: var(--label-color); flex: 0 0 auto; }
    .setting-field-pair { display: flex; gap: var(--space-3); margin-bottom: var(--space-5); }
    .tuner-display-toggle-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-5); font-size: var(--font-md); padding: var(--space-4); background: var(--container-bg); border-radius: var(--radius-md); border: 1px solid var(--input-border); }
    .tuner-display-toggle-row:last-child { margin-bottom: 0; }

    /* Bipolar (+50c to -50c, 0 in the middle) - same 1cent=1% mapping .tuner-bar-needle's own left:
       ${50+cents}% already uses, just read top-to-bottom instead of left-to-right. Every bar is a
       flex item "slot" (even width, full height) with its own absolutely-positioned coloured segment
       inside - keeps the flex row's own width distribution completely separate from each bar's own
       vertical top/height, which a single flex-item-is-also-the-bar element can't do at once. */
    .tuner-pitch-graph { position: relative; height: 90px; flex: 1; min-width: 0; }
    .tuner-pitch-graph-zone { position: absolute; left: 0; right: 0; top: 35%; height: 30%; background: var(--tuner-in-tune-zone-soft); }
    /* Dashed top/bottom edges - .at-top / .at-bottom place each one (shared with
       .tuner-dynamics-gridline, which also has .at-middle). Exactly 3 lines on this strip total, these 2 plus .tuner-pitch-graph-zero's own
       solid one at the centre - not a 4th dashed line doubling up at 0 alongside the solid one. */
    .tuner-pitch-graph-gridline { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--input-border); }
    :is(.tuner-pitch-graph-gridline, .tuner-dynamics-gridline).at-top { top: 0; }
    :is(.tuner-pitch-graph-gridline, .tuner-dynamics-gridline).at-bottom { bottom: 0; }
    :is(.tuner-pitch-graph-gridline, .tuner-dynamics-gridline).at-middle { bottom: 50%; }
    .tuner-pitch-graph-zero { position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: var(--input-border); }
    .tuner-pitch-graph-bars { position: absolute; inset: 0; display: flex; align-items: stretch; gap: var(--space-0-5); }
    .tuner-history-bar-slot { position: relative; flex: 1; min-width: 3px; }
    .tuner-history-bar { position: absolute; left: 0; right: 0; top: var(--bar-top); height: var(--bar-h); border-radius: var(--radius-2xs); background: var(--warning-color); } /* --bar-top/--bar-h set per sample by renderTunerHistoryGraphs */
    .tuner-history-bar.in-tune { background: var(--tuner-in-tune-green); }
    /* A "no signal" gap in the timeline (see pushTunerHistorySample) - a faint dot at the zero line
       rather than nothing at all, so a silent beat still reads as "a moment passed" in the timeline. */
    .tuner-history-bar-gap { position: absolute; left: 50%; top: 50%; width: 3px; height: 3px; border-radius: var(--radius-circle); background: var(--input-border); transform: translate(-50%, -50%); }
    /* Above the bar by default (top:-20px) - .below (assigned by renderTunerHistoryGraphs, app.js,
       when consecutive note-attack labels are close enough together to collide) flips to sit below
       the bar instead (bottom:-20px), alternating through a run of close attacks rather than letting
       them all stack on top of each other above. Position is relative to .tuner-history-bar itself
       (the label's own parent), not the strip - so "below" always means below THIS bar's own box,
       whatever cents value put that box at. */
    .tuner-history-bar-label { position: absolute; top: -20px; left: 50%; transform: translateX(-50%); font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--text-color); white-space: nowrap; background: var(--container-bg); border: 1px solid var(--input-border); padding: var(--space-0-5) var(--space-1); border-radius: var(--radius-sm); }
    .tuner-history-bar-label.below { top: auto; bottom: -20px; }

    /* Unipolar (0 dBFS at top, -60 at bottom) - baseline sits at the bottom, so plain flex items with
       their own height:% and align-items:flex-end are enough here, no slot wrapper needed (unlike the
       bipolar pitch graph above, nothing here ever needs to grow both up AND down from a mid-line). */
    .tuner-dynamics-graph { position: relative; height: 70px; flex: 1; min-width: 0; }
    /* Dashed halfway up and right at the top - no y-axis labels on this strip at all (loudness here is
       only ever relative, not calibrated to real pp/mf/ff dynamic markings, so labeling it that way
       would be misleading) - these are just plain visual reference ticks. Exactly 3 lines on this strip
       total, these 2 plus .tuner-dynamics-graph-zero's own solid baseline. */
    .tuner-dynamics-gridline { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--input-border); }
    /* Solid baseline the bars grow up from - same treatment as .tuner-pitch-graph-zero's centre line,
       just solid (not dashed, unlike the pp/ff gridlines above) since this is the strip's own true
       zero, not just a reference tick. */
    .tuner-dynamics-graph-zero { position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--input-border); }
    .tuner-dynamics-bars { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: var(--space-0-5); }
    .tuner-dynamics-bar { flex: 1; min-width: 3px; height: var(--bar-h, 0%); background: var(--primary-action); border-radius: var(--radius-2xs) var(--radius-2xs) 0 0; }

    /* padding (not modal-content's own width) is what guarantees the gap either side of the card -
       a fixed design-system space rather than a percentage, so it stays a real, constant gap on a
       narrow phone instead of shrinking proportionally with the viewport (previously 90%/95% width,
       which read as nearly edge-to-edge on a typical phone screen). box-sizing so the padding doesn't
       itself widen the fixed inset:0 box past the viewport. */
    /* Hidden by default; .show opens it (showModal/hideModal, app.js - ML-288). */
    .modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: var(--overlay-bg); z-index: var(--z-modal); justify-content: center; align-items: center; padding: 0 var(--space-5); box-sizing: border-box; }
    .modal.show { display: flex; }
    .modal-content { background: var(--container-bg); width: 100%; max-width: 400px; padding: var(--space-6); border-radius: var(--radius-xl); box-shadow: var(--shadow-xl); max-height: 90vh; overflow-y: auto; overscroll-behavior: contain; }
    /* A short explanatory line straight under a pop-up's title - pulled up into the title's bottom margin. */
    .modal-content .modal-intro { margin: calc(-1 * var(--space-2)) 0 var(--space-4) 0; font-size: var(--font-sm); }
    .modal-content h2 { margin-top: 0; margin-bottom: var(--space-5); font-size: var(--font-lg); font-weight: var(--font-weight-semibold); }


    /* white-space: pre-line so a message with an embedded \n (e.g. several validation problems
       reported together - ML-97 follow-up) actually breaks onto its own line instead of running
       into the next sentence with no separator; no effect on the plain single-line messages every
       other toast already uses. */
    /* Hidden by default; .show (showSuccessToast/showWarningToast/showInfoToast/showUndoToast add it,
       closeToast removes it) reveals one. bottom is the toast's one resting position, not an
       animated-in state. */
    .toast { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); color: var(--text-on-inverse); padding: var(--space-3) var(--space-5); border-radius: var(--radius-sm); display: none; align-items: center; gap: var(--space-4); z-index: var(--z-toast); font-size: var(--font-base); white-space: pre-line; overflow: hidden; }
    /* ML-449: on a screen with a bar pinned to the bottom, a toast sits above the bar, not on its buttons */
    body:has(#pieceOutlineView:not(.hidden-group)) .toast, body:has(#flowDetailsHubView:not(.hidden-group)) .toast { bottom: var(--toast-above-bottom-bar); }
    .toast.show { display: flex; }
    .toast.success { background-color: var(--surface-inverse); }
    .toast.warning { background-color: var(--warning-color); color: var(--text-on-warning); font-weight: var(--font-weight-bold); }
    .toast.info { background-color: var(--info-color); color: var(--text-on-accent); font-weight: var(--font-weight-bold); box-shadow: var(--shadow-info); }
    .toast.undo { background-color: var(--surface-inverse); }
    /* min 44px (--touch-target): Undo at 32px was easy to miss on a phone (ML-359 follow-up) */
    .toast button { background: var(--button-overlay); border: none; color: inherit; border-radius: var(--radius-xs); cursor: pointer; font-weight: var(--font-weight-bold); padding: var(--space-2) var(--space-3); min-height: var(--touch-target); min-width: var(--touch-target); }
    /* Visual auto-dismiss countdown (startToastCountdownBar) - a thin bar along the toast's own
       bottom edge that shrinks from full width to nothing over the toast's configured lifetime
       (TOAST_DURATIONS_MS), rather than a ticking number: the shrink itself reads as "time left"
       without needing to be read, and works identically for every toast type since it's just an
       inline width transition driven by one shared helper. */
    .toast-countdown-bar { position: absolute; left: 0; bottom: 0; height: 3px; width: 100%; background: currentColor; opacity: 0.35; }
    /* Counting down: shrinks to nothing over --countdown-ms (startToastCountdownBar/resumeToast). While
       the toast is hovered or focused it's frozen where it got to (--frozen-w, pauseToast). */
    .toast-countdown-bar.is-running { width: 0%; transition: width var(--countdown-ms) linear; }
    .toast-countdown-bar.is-paused { width: var(--frozen-w); transition: none; }

    /* ML-75: small popup anchored right next to whatever bar/heatmap square was just clicked, instead
       of a toast at the bottom of the screen - position:fixed and placed via getBoundingClientRect()
       (see showAnchoredPopup) so it's immune to whichever scrollable container the anchor happens to
       sit inside. */
    .anchored-popup { position: fixed; z-index: var(--z-toast); background: var(--container-bg); color: var(--text-color); border: 1px solid var(--input-border); padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); box-shadow: var(--shadow-xl); font-size: var(--font-sm); font-weight: var(--font-weight-bold); white-space: nowrap; pointer-events: none; }

    /* Splash / login screen */
    /* overflow-y + safe centring (ML-355): with the email login under Google's button the splash can
       be taller than a phone screen - it scrolls, and its top never slides out of reach. */
    .splash-screen { display: flex; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: var(--splash-bg); align-items: safe center; justify-content: center; z-index: var(--z-splash); padding: var(--space-5); box-sizing: border-box; overflow-y: auto; }
    .splash-content { text-align: center; max-width: 480px; width: 100%; }
    .splash-image { width: 100%; max-width: 420px; border-radius: var(--radius-md); box-shadow: var(--shadow-2xl); margin-bottom: var(--space-7); }
    .splash-title { color: var(--splash-title-color); margin: 0 0 var(--space-2) 0; font-size: var(--font-2xl); letter-spacing: 1px; }
    .splash-subtitle { color: var(--splash-subtitle-color); font-size: var(--font-md); margin: 0 0 var(--space-6) 0; }
    .splash-login-btn { display: none; width: 100%; padding: var(--space-4) var(--space-5); font-size: var(--font-md); font-weight: var(--font-weight-bold); cursor: pointer; background: linear-gradient(135deg, var(--primary-action), var(--primary-action-gradient-end)); color: var(--primary-action-text); border: none; border-radius: var(--radius-sm); }
    .splash-login-btn.show { display: inline-block; }
    /* ML-430: the Google sign-in button is Google's own picture (public/icons/google-sign-in-light.svg) - its colours,
       logo and wording are fixed by their branding guidelines, so it is scaled as a whole and never restyled. */
    .splash-google-btn { display: none; padding: 0; border: 0; background: none; border-radius: var(--radius-xs); cursor: pointer; }
    .splash-google-btn.show { display: inline-block; }
    .splash-google-btn img { display: block; height: calc(var(--touch-target) * 1.25); width: auto; }
    .splash-google-btn:disabled { opacity: var(--opacity-muted); cursor: default; } /* revealed by displayLoginScreen once the silent sign-in has failed */
    .splash-login-btn:disabled { opacity: var(--opacity-muted); cursor: default; }
    /* ML-355: the email + password forms on the (always dark) splash - labels and messages in the
       splash's own fixed colours, the fields and gold button as everywhere else. */
    .splash-or { margin: var(--space-6) 0 var(--space-2) 0; }
    .splash-form { display: flex; flex-direction: column; gap: var(--space-2); text-align: left; }
    .splash-form label { color: var(--splash-subtitle-color); font-weight: var(--font-weight-bold); margin-top: var(--space-2); }
    .splash-form .splash-login-btn { margin-top: var(--space-3); }
    .splash-hint { color: var(--splash-subtitle-color); font-size: var(--font-sm); margin: 0; }
    .splash-message { color: var(--splash-title-color); font-weight: var(--font-weight-bold); margin: var(--space-2) 0 0 0; }
    /* ML-355: a password box with an eye button at its right end to show / hide what's typed - see
       specs/components/password-field.md. The field keeps room on the right so text never runs under it. */
    .password-field { position: relative; }
    .password-field input { padding-right: calc(var(--touch-target) + var(--space-2)); }
    .password-toggle { position: absolute; top: 0; right: 0; bottom: 0; width: var(--touch-target); min-height: var(--touch-target); display: flex; align-items: center; justify-content: center; padding: 0; background: none; border: none; border-radius: var(--radius-lg); color: var(--label-color); cursor: pointer; }
    /* ML-355 batch 2: two-step sign-in - the setup key and the recovery codes, shown on a light card
       (the input look) in both the login splash and the app, in a monospace font so each character is
       unmistakable. See specs/components/two-step.md. */
    .two-step-key, .recovery-codes { background: var(--input-bg); color: var(--text-color); border: 1px solid var(--control-border); border-radius: var(--radius-lg); font-family: var(--font-mono); font-size: var(--font-md); margin: var(--space-2) 0; }
    .two-step-key { padding: var(--space-3) var(--space-4); overflow-wrap: anywhere; text-align: center; } /* wraps between the groups of 4, not inside one */
    .recovery-codes { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-2) var(--space-4); padding: var(--space-3) var(--space-4) var(--space-3) var(--space-7); }
    .two-step-message { color: var(--danger-text); font-weight: var(--font-weight-bold); margin: var(--space-2) 0; }
    .splash-link-btn { display: inline-flex; align-items: center; align-self: center; min-height: var(--touch-target); padding: var(--space-2) var(--space-3); background: none; border: none; color: var(--splash-title-color); font: inherit; font-weight: var(--font-weight-bold); text-decoration: underline; cursor: pointer; }

    /* Slider - shared design-system component (see style guide). A draggable track + thumb + fill,
       with an optional min/max scale caption underneath. Used for any single numeric value dragged
       along a line: the metronome's target-speed and volume controls both use it as-is. */
    .slider-wrap { margin-bottom: var(--space-5); }
    .slider-wrap.no-margin { margin-bottom: 0; }
    /* Bottom margin widened from 6px to 18px - the thumb (26px + 2*3px border, centred on the
       track) extends ~12px below the track's own bottom edge, which was overlapping the scale
       numbers directly beneath it at the old 6px gap. */
    .slider-track { position: relative; height: 8px; border-radius: var(--radius-xs); background: var(--input-border); margin: var(--space-5) var(--space-2) var(--space-4) var(--space-2); cursor: pointer; touch-action: none; }
    /* ML-288: --pct (set by each slider's own update function) is the value as a percentage - the
       fill's width and the thumb's position. */
    .slider-fill { position: absolute; left: 0; top: 0; bottom: 0; width: var(--pct); border-radius: var(--radius-xs); background: var(--primary-action); pointer-events: none; }
    /* Visual size stays 26px (a 48px dot here would look oversized against an 8px track), but the
       ::before extends the actual clickable/draggable area to the 48x48 touch-target minimum -
       a click anywhere in that invisible area still targets .slider-thumb itself (pseudo-elements
       aren't separately addressable), so the existing pointerdown handler needs no changes. */
    .slider-thumb { position: absolute; top: 50%; left: var(--pct); width: 26px; height: 26px; border-radius: var(--radius-circle); background: var(--primary-action); border: 3px solid var(--primary-action-strong); box-shadow: var(--shadow-md); transform: translate(-50%, -50%); cursor: grab; touch-action: none; }
    .slider-thumb::before { content: ''; position: absolute; top: 50%; left: 50%; width: 48px; height: 48px; transform: translate(-50%, -50%); }
    .slider-thumb:active { cursor: grabbing; }
    .slider-scale { display: flex; justify-content: space-between; font-size: var(--font-sm); color: var(--label-color); padding: 0 var(--space-2); }
    /* Design system: a slider's numeric readout is also a tap target - clicking/tapping it swaps
       in this input so a value can be typed directly instead of only dragging or stepping (see
       makeSliderReadoutEditable in app.js). Sized to inherit whatever the readout it's replacing
       looked like, not a fixed style, since readouts appear at several different sizes. */
    .slider-readout-input { width: 4.5em; text-align: center; font: inherit; font-weight: inherit; color: inherit; padding: var(--space-0-5) var(--space-1); border: 2px solid var(--primary-action-strong); border-radius: var(--radius-sm); background: var(--input-bg); box-sizing: border-box; }

    /* Metronome */
    /* A bit more breathing room below every section title than the site-wide default, specifically
       within this view - scoped here rather than widening .section-title itself everywhere else. */
    #metronomeView .section-title { margin-bottom: var(--space-4); }
    /* Same idea for Quick Play's "Bars" title specifically - the site-wide default (5px text-to-
       divider, 8px divider-to-content) read as too tight right above the first Bar card. */
    #quickPlayView .section-title { padding-bottom: var(--space-2); margin-bottom: var(--space-4); }
    .metro-display { padding: var(--space-4) 0 var(--space-2) 0; }
    /* Viewport clips to the visible width; content is either 100% (fits normally) or a fixed pixel
       width wider than the viewport (too many beats to fit) - see metroTierGeometry in app.js. When
       wider, JS drives a translateX on the content to keep the current beat in view as it plays. */
    /* Top padding keeps the accent dot's border, which is taller than its row, from being clipped by
       this element's own overflow:hidden. */
    .metro-display-viewport { width: 100%; overflow: hidden; padding-top: var(--space-2); }
    /* Needs its own transform transition so metroScrollFollow's JS-set transitionDuration actually
       animates the scroll - without a transition-property declared here, setting transitionDuration
       alone does nothing and the scroll teleports instantly to the next beat's centred position
       instead of gliding smoothly along with the beat. */
    /* ML-288: --content-w (metroApplyDisplayWidth - wider than the viewport when the dots don't fit),
       --scroll-x / --scroll-dur (metroScrollFollow - the follow-the-beat pan) are set from JS. */
    .metro-display-content { width: var(--content-w, 100%); transform: translateX(var(--scroll-x)); transition: transform var(--scroll-dur, 0s) linear; }

    .metro-tier { position: relative; height: 24px; margin-bottom: var(--space-1); }

    .metro-dot { position: absolute; top: 50%; left: var(--dot-x); /* --dot-x: its slot along the row (buildMetroDotRow) */ border-radius: var(--radius-circle); transform: translate(-50%, -50%); transition: background var(--duration-instant), border-color var(--duration-instant), opacity var(--duration-instant), transform var(--duration-instant); }

    /* Every base click (beat or subdivision) lives in one row now (ML-66): a note-boundary click gets
       the larger "note" dot below, everything in between gets the smaller "sub" dot. Only the very
       first note of the bar is visually accented - every other dot, note or sub, looks (and, per
       ML-62, sounds) the same, so there's nothing left to tell them apart.
       ML-111: every dot is fully opaque at every state now - a resting dot at opacity<1 let the grey
       connecting track line (drawn behind the row, connectMetroBlkDotsWithTrack) show through it,
       which read as the line running straight through the circles instead of being hidden behind them.
       Resting vs active is conveyed by fill/border colour alone, not transparency. */
    .metro-dot-note { width: 15px; height: 15px; background: var(--input-bg); border: 2px solid var(--primary-action-strong); }
    /* Same size as every other note dot now - filled solid gold instead of a bigger circle, so it
       doesn't need extra layout room to avoid overlapping its neighbours (feedback: the larger first
       circle only needs to be distinguished, not literally bigger). */
    .metro-dot-note.accent { background: var(--primary-action); }
    .metro-dot-note.lit { background: var(--selection-color); border-color: var(--selection-text); transform: translate(-50%, -50%) scale(1.25); }
    .metro-dot-note.accent.lit { background: var(--danger-color); border-color: var(--danger-text); transform: translate(-50%, -50%) scale(1.15); }

    /* ML-111: same gold as the main beat dots now, not a separate hue - was --selection-color (a blue/
       grey distinct from the beat colours), which read as an unrelated colour rather than "this is the
       same metronome, just a finer subdivision". */
    .metro-dot-sub { width: 10px; height: 10px; background: var(--primary-action); border: 1px solid var(--primary-action-strong); }
    /* Resting sub dots are already solid gold, so a lit state that only bumped scale (as before) barely
       read as a flash on a 10px circle - give it its own colour change, same pattern as
       .metro-dot-note.lit, so the sub-beat flash is actually visible. */
    .metro-dot-sub.lit { background: var(--danger-color); border-color: var(--danger-text); transform: translate(-50%, -50%) scale(1.35); }

    /* ML-130: a fermata's held beat - persistent (not the transient 120ms .lit flash, which keeps
       firing independently underneath this via flashTierDot) for the whole hold, gold-filled with a
       pulsing glow so it reads unmistakably as "time is suspended here", plus a live countdown
       numeral (metro-dot-count, injected/removed by updateFermataDotState) of beats left in the hold.
       Wins over .lit's own colour so the transient flash never fights the sustained gold underneath it. */
    @keyframes metro-fermata-glow {
        0%, 100% { box-shadow: var(--shadow-glow-rest); }
        50% { box-shadow: var(--shadow-glow); }
    }
    .metro-dot.fermata-holding,
    .metro-dot.fermata-holding.lit {
        background: var(--primary-action);
        border-color: var(--primary-action-strong);
        animation: metro-fermata-glow var(--duration-pulse) ease-in-out infinite;
    }
    .metro-dot-count { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: var(--font-2xs); font-weight: var(--font-weight-bold); color: var(--container-bg); pointer-events: none; }
    /* A fermata's held beat once the hold itself has finished - static gold (no glow, no countdown),
       kept for the rest of the bar (cleared by renderFermataMarkers at the next bar boundary) instead
       of snapping straight back to a plain dot the instant the hold ends, so there's still a visible
       record of "a fermata happened here" while that bar keeps playing. */
    .metro-dot.fermata-done, .metro-dot.fermata-done.lit { background: var(--primary-action); border-color: var(--primary-action-strong); }
    /* The small fermata glyph shown above a beat's dot for as long as that beat's own bar is playing
       (renderFermataMarkers) - a sibling of the dots in the same scrolling row, so it pans with them
       for free rather than needing its own scroll-follow logic. */
    .metro-fermata-marker { position: absolute; top: -16px; left: var(--dot-x); /* copied from the dot it sits over */ transform: translateX(-50%); color: var(--primary-action-strong); pointer-events: none; }
    /* Scaled down from flowSignIconSvg's default 32px picker-tile size - proportionate to the 15px
       beat dot it sits above, not the much bigger context it was originally sized for. */
    .metro-fermata-marker .flow-sign-svg { font-size: var(--font-md); }
    /* Extra headroom so the marker's -16px offset (above) doesn't get clipped by the viewport's own
       overflow:hidden (needed for horizontal scrolling) - toggled per-row by renderFermataMarkers only
       while that row actually has a marker to show, not permanently on every metronome-family row. */
    .metro-display-viewport.metro-has-fermata-marker { padding-top: var(--space-6); }

    .metro-bpm-step { width: 48px; height: 48px; flex-shrink: 0; border-radius: var(--radius-circle); border: 2px solid var(--control-border); background: var(--input-bg); color: var(--text-color); font-size: var(--font-lg); font-weight: var(--font-weight-bold); cursor: pointer; user-select: none; touch-action: manipulation; }
    .metro-bpm-step:active { border-color: var(--primary-action-strong); background: var(--primary-action-tint); color: var(--primary-action-strong); } /* press feedback in the selected-gold language (ML-218) */

    .metro-beats-2col { display: flex; gap: var(--space-2); margin-bottom: var(--space-3); }
    .metro-beats-col { flex: 1; text-align: center; }
    .metro-col-label { font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--label-color); margin-bottom: var(--space-2); }
    /* .metro-col-btn (the "opens a popup" trigger style it used to name) is gone - its last live use,
       the Blocks segment-edit modal's time-signature button, moved to .metroBlk-ctrl-value-btn as
       part of the ML-103 qp-style card (see index.html #metroSegStandardBox). */

    .metro-beats-options { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2); }
    .metro-beats-option { padding: var(--space-3) 0; border: 2px solid var(--control-border); border-radius: var(--radius-md); background: var(--input-bg); color: var(--text-color); font-weight: var(--font-weight-bold); cursor: pointer; }
    .metro-beats-option.selected { border-color: var(--primary-action-strong); background: var(--primary-action-tint); color: var(--primary-action-strong); } /* ML-218: gold selected pattern, not blue */
    .metro-beats-option.custom-option { grid-column: 1 / -1; display: flex; align-items: center; justify-content: center; gap: var(--space-1); }
    .metro-option-icon { font-size: var(--font-md); }

    .metro-picker-custom { display: flex; align-items: center; justify-content: center; gap: var(--space-5); margin: var(--space-4) 0 0 0; }
    .metro-picker-custom-value { font-size: var(--font-2xl); font-weight: var(--font-weight-bold); min-width: 50px; text-align: center; font-variant-numeric: tabular-nums; }

    .metro-advanced-readout { background: var(--input-bg); border: 1px solid var(--input-border); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-3); font-size: var(--font-base); color: var(--text-color); }

    .metro-speed-row { display: flex; align-items: center; justify-content: center; gap: var(--space-5); margin: var(--space-3) 0 var(--space-2) 0; }
    .metro-speed-btn { width: 48px; height: 48px; border-radius: var(--radius-circle); border: 2px solid var(--control-border); background: var(--input-bg); color: var(--text-color); font-size: var(--font-lg); font-weight: var(--font-weight-bold); cursor: pointer; }
    .metro-speed-btn:disabled { opacity: 0.4; cursor: not-allowed; }
    .metro-speed-readout { text-align: center; min-width: 90px; }
    .metro-speed-readout > div:first-child { font-size: var(--font-lg); font-weight: var(--font-weight-bold); }
    .metro-speed-sub { font-size: var(--font-sm); color: var(--label-color); }
    /* ML-297: the Italian speed name under the bpm (Moderato), kept to one line in a narrow box. */
    .metro-speed-name { font-size: var(--font-xs); font-weight: var(--font-weight-semibold); color: var(--label-color); white-space: nowrap; }
    .metro-speed-reset { display: block; margin: 0 auto var(--space-4) auto; background: none; border: none; color: var(--info-text); font-weight: var(--font-weight-bold); font-size: var(--font-sm); cursor: pointer; padding: var(--space-1) var(--space-2); }
    .metro-help-text { font-size: var(--font-sm); color: var(--label-color); margin: 0 0 var(--space-3) 0; line-height: var(--line-height-base); }

    .metro-volume-row { display: flex; align-items: center; gap: var(--space-4); margin: var(--space-5) 0; }
    .metro-mute-btn { width: 40px; height: 40px; flex-shrink: 0; border-radius: var(--radius-circle); border: 2px solid var(--control-border); background: var(--input-bg); color: var(--text-color); cursor: pointer; display: flex; align-items: center; justify-content: center; }
    .metro-volume-row .slider-wrap { flex: 1; }
    /* The default slider-track margin leaves room for a min/max scale caption below it (as on the
       target-speed slider) - the volume row has no such caption, so its track needs even top/bottom
       margins instead, or it renders visibly lower than the mute button beside it. */
    .metro-volume-row .slider-track { margin: 0 var(--space-2); }

    /* ML-293: Timer screen countdown ring - see specs/components/timer-ring.md. --ring-left / --lap-left
       (0-1, the time left on the main ring and the outer hour ring) are set by renderTimerRing. */
    .timer-ring { position: relative; width: min(100%, 260px); aspect-ratio: 1; margin: var(--space-2) auto var(--space-4) auto; }
    .timer-ring-svg { width: 100%; height: 100%; display: block; }
    .timer-ring-track, .timer-ring-arc, .timer-ring-lap-track, .timer-ring-lap { fill: none; }
    .timer-ring-track { stroke: var(--input-border); stroke-width: 8; }
    .timer-ring-arc, .timer-ring-lap { transform: rotate(-90deg); transform-origin: 60px 60px; stroke-dasharray: 100 100; transition: stroke-dashoffset var(--duration-pulse) linear; }
    .timer-ring-arc { stroke: var(--primary-action); stroke-width: 8; stroke-linecap: round; stroke-dashoffset: calc(100 - var(--ring-left, 1) * 100); }
    .timer-ring-lap-track, .timer-ring-lap { stroke-width: 3; opacity: 0; transition: opacity var(--duration-base) ease, stroke-dashoffset var(--duration-pulse) linear; }
    .timer-ring-lap-track { stroke: var(--input-border); }
    .timer-ring-lap { stroke: var(--primary-action-strong); stroke-linecap: round; stroke-dashoffset: calc(100 - var(--lap-left, 0) * 100); }
    .timer-ring.has-lap .timer-ring-lap-track, .timer-ring.has-lap .timer-ring-lap { opacity: 1; }
    /* The dot rides the arc's end: rotated round the centre by the fraction left. */
    .timer-ring-tip { transform: rotate(calc(var(--ring-left, 1) * 1turn)); transform-origin: 60px 60px; transition: transform var(--duration-pulse) linear; fill: var(--primary-action-strong); }
    .timer-ring-tip circle { transform-box: fill-box; transform-origin: center; }
    .timer-ring.is-running .timer-ring-tip circle { animation: timer-ring-pulse var(--duration-pulse) ease-in-out infinite; }
    @keyframes timer-ring-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.4); } }
    .timer-ring-readout { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-1); }
    .timer-ring-time { font-size: var(--font-3xl); font-weight: var(--font-weight-bold); font-variant-numeric: tabular-nums; color: var(--text-color); }
    .timer-ring.is-long .timer-ring-time { font-size: var(--font-2xl); } /* with hours in it (1:29:57) - keeps it inside the ring */
    .timer-ring-label { font-size: var(--font-sm); color: var(--label-color); }
    .timer-ring-hour { font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--primary-action-strong); background: var(--primary-action-tint); border-radius: var(--radius-pill); padding: var(--space-0-5) var(--space-2); }
    .metro-transport-row { display: flex; align-items: center; justify-content: center; gap: var(--space-4); margin: var(--space-2) 0 var(--space-4) 0; }
    .metro-transport-btn { padding: 0; width: 60px; height: 60px; border: none; border-radius: var(--radius-md); cursor: pointer; display: flex; align-items: center; justify-content: center; }
    .metro-transport-btn .material-symbols-outlined { font-size: var(--icon-lg); }
    /* Only the Blocks transport row ever actually sets `disabled` (Edit Mode, ML-97 follow-up) - the
       single-bar tool's own Play/Reset never do, so this never affects it. */
    .metro-transport-btn:disabled, .metroBlk-ctrl-value-btn:disabled, .metro-play-grid-btn:disabled { opacity: 0.4; cursor: not-allowed; }
    .metro-play-btn { background: var(--primary-action); color: var(--primary-action-text); }
    .metro-stop-btn { background: var(--nav-action); color: var(--nav-action-text); }

    /* Shared metronome-family transport row (Quick Play and Blocks/Flow both use this - one shared
       class rather than one per tool, so the layout only needs updating in one place): an equal-
       width, full-bleed grid instead of metro-transport-row's centered flex row - 4 columns (Play,
       Reset, sub beats, play speed; ML-146 - Volume moved out into the "now playing" box's own header,
       see .metroBlk-row-volume-btn below, freeing this row back down to 4 evenly-spread cells after
       ML-139 briefly grew it to 5), but the point of using grid rather than that fixed-column-count
       flex row is other rows can later drop in at 1, 2 or 4 cells wide and still line up with this one
       and with the front page's own tool-icon-row, which uses the same equal-width-cells idea. */
    .metro-transport-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2); margin: var(--space-2) 0 var(--space-4) 0; }
    .metro-transport-grid .metroBlk-ctrl-value-btn { width: 100%; min-width: 0; }
    /* ML-302: Rehearse's 5 controls - Play in the middle, a little wider, with where-you-are controls
       (reset, repeat) to its left and how-it-sounds ones (sub beats, speed) to its right. */
    .metro-transport-grid-5 { grid-template-columns: 1fr 1fr 1.4fr 1fr 1fr; gap: var(--space-1); }
    /* Narrower cells: less side padding, and a value like "12–16" stays on one line. */
    .metro-transport-grid-5 .metroBlk-ctrl-value-btn { padding: 0 var(--space-1); }
    .metro-transport-grid-5 .metroBlk-ctrl-value-btn strong { white-space: nowrap; }
    /* Two to a row - a Theory quiz's options (Content / Length), where a value can be words ("2 ledger lines"). */
    .metro-transport-grid-2 { grid-template-columns: repeat(2, 1fr); }
    /* A sign drawn in a value or a choice next to words (Theory's ♯ ♭): centred on the line, not sat on it. */
    .metroBlk-ctrl-value-btn svg, .flow-choice-option svg { vertical-align: middle; }
    /* ML-445: a beat note drawn in a value box or a table cell (Quick entry) - the pop-up's picture, at the height of the words it replaces */
    .metroBlk-ctrl-value-btn .metroBlk-note-svg, .outline-cell-btn .metroBlk-note-svg, .outline-cell-fixed .metroBlk-note-svg { height: 1.6em; width: auto; }
    /* ML-451 (owner rule, the whole app): the words under a value are in the same place in every value box, whatever
       the value is. A picture taller than a line of text keeps the value line one line high - it is centred on the
       line and spills a little above and below - so its label lines up with the boxes beside it. */
    .metroBlk-ctrl-value-btn strong:has(> .metroBlk-note-svg:only-child) { display: flex; align-items: center; justify-content: center; height: calc(var(--font-md) * var(--line-height-snug)); }
    /* ML-348: a date value box (a practice list's target date, "30 May 26"). Its cell is a fixed
       quarter of the grid and never grows with the date, so the date steps its size down to stay on one
       line in a narrow cell: --font-sm, then --font-base and --font-md as the cell gets wider. */
    .metroBlk-ctrl-value-btn.is-date { padding: 0 var(--space-1); container: value-date / inline-size; }
    .metroBlk-ctrl-value-btn.is-date strong { white-space: nowrap; font-size: var(--font-sm); }
    @container value-date (min-width: 72px) { .metroBlk-ctrl-value-btn.is-date strong { font-size: var(--font-base); } }
    @container value-date (min-width: 84px) { .metroBlk-ctrl-value-btn.is-date strong { font-size: var(--font-md); } }
    /* A value control that's switched on (Rehearse's repeat while it's repeating) - the same gold
       selected look as .flow-multiselect-num.selected. */
    .metroBlk-ctrl-value-btn.metroBlk-ctrl-value-btn-on { border-color: var(--primary-action-strong); background: var(--primary-action-tint); color: var(--primary-action-strong); }
    .metroBlk-ctrl-value-btn.metroBlk-ctrl-value-btn-on .metroBlk-ctrl-value-label { color: var(--primary-action-strong); }
    /* Play/pause reshaped to match the other cells (icon + small label) instead of the plain round
       .metro-play-btn. Press-and-hold still resets too (the shared setupPlayButtonHoldReset), but
       ML-139 restored a plain Reset button alongside it (.metro-play-grid-btn-reset below) since the
       hold gesture alone wasn't discoverable/reliable enough on its own. user-select:none and
       touch-action:manipulation stop a held press from turning into a text-selection instead of
       triggering the hold (ML-139 - without these the browser's own press-and-hold-to-select-text
       behaviour was winning the race against the JS timer on this button's label). Icon deliberately
       has no font-size override here - it inherits the same global 20px .material-symbols-outlined
       default the other cells use, so they all stay the same size and vertically aligned by
       construction rather than magic numbers kept in sync by hand. */
    .metro-play-grid-btn { width: 100%; height: 60px; border: none; border-radius: var(--radius-md); cursor: pointer; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-0-5); background: var(--primary-action); color: var(--primary-action-text); user-select: none; -webkit-user-select: none; touch-action: manipulation; }
    /* Grey/neutral (same --nav-action language as the mini bar's own Reset - .metro-stop-btn) so it
       reads as the secondary action next to gold Play. */
    .metro-play-grid-btn-reset { background: var(--nav-action); color: var(--nav-action-text); }
    /* ML-146: a plain icon, deliberately with no circle/box behind it ("more in the background" per
       the ticket) - margin-left:auto pushes it to the far right of .metroBlk-row-head's flex row
       while the mark/label stay left-aligned together, so it reads as sitting in the box's own corner
       rather than as a peer of the mark/label. Shares the same 20px .material-symbols-outlined default
       as the transport row's own icons rather than a size override, for the same reason. min-width/
       height (ML-148) give it a 44x44 tap target without enlarging the icon itself - the visible glyph
       stays small/"background", the invisible hit area around it doesn't. */
    /* ML-164: now a filled 40x40 circle in the same clickable-area colour as every other control
       (was a bare icon with no circle at all - see the superseded ML-146 rationale above). */
    .metroBlk-row-volume-btn { margin-left: auto; align-self: center; width: 40px; height: 40px; min-width: 40px; min-height: 40px; flex-shrink: 0; border-radius: var(--radius-circle); background: var(--input-bg); border: none; padding: 0; cursor: pointer; color: var(--label-color); display: flex; align-items: center; justify-content: center; }
    /* ML-148: mute is just volume 0 now (see setMetroBlkVolume/setQpVolume) - this fades the gold
       fill/thumb to grey so 0% via the mute button visually reads the same as 0% via dragging, rather
       than the mute button's own icon-swap being the only cue. A solid muted color rather than opacity
       - opacity let the track's own grey line bleed through the middle of the (semi-transparent)
       thumb, which read as a visible seam rather than a uniformly faded circle. */
    .metro-volume-row.is-muted .slider-fill, .metro-volume-row.is-muted .slider-thumb { background: var(--label-color); }

    /* ML-129: the timer's minimised state lives in the top bar itself, replacing the old full-width
       bar that used to sit under .top-bar - superseded, not left dormant here, since nothing in the
       HTML references .timer-mini-* any more.
       ML-129 follow-up: sized to its own content now (not flex-grown/centred - that's .top-bar-title's
       job again), right-aligned next to the tuner toggle with the same bordered-square-with-a-corner-
       arrow look as .metroBlk-ctrl-value-btn (e.g. the sub-beats button) rather than a filled pill. No dropdown
       arrow (ML-205): like the metronome value boxes, it opens the app's own popup, not a native dropdown. */
    .top-bar-timer-pill { position: relative; display: flex; align-items: center; gap: var(--space-1); margin-left: var(--space-2); background: transparent; border: 2px solid var(--control-border); border-radius: var(--radius-pill); padding: var(--space-2) var(--space-3); font-variant-numeric: tabular-nums; color: var(--text-color); cursor: pointer; }
    .top-bar-timer-pill-icon { font-size: var(--font-base); }
    .top-bar-timer-pill-time { font-size: var(--font-sm); font-weight: var(--font-weight-bold); }
    .top-bar-timer-pill-left { font-size: var(--font-sm); }
    /* Actively running (not paused) - a soft amber glow instead of the neutral border/background, so
       time visibly accumulating is obvious at a glance with no extra banner. Border left alone
       (still var(--input-border)) - only the fill and text call out "running". */
    .top-bar-timer-pill.top-bar-timer-pill-running { background: var(--primary-action-tint); color: var(--primary-action-strong); }

    /* ML-129 follow-up: opened by tapping either the running pill or the idle icon next to the tuner
       toggle - a true modal now (grey backdrop, same .modal/.modal-content everything else in the app
       uses) rather than an in-flow box, so it can't clash with whatever primary button (e.g. the
       metronome's own Play) happens to sit on the screen behind it. Tapping the backdrop closes it,
       same as the X - either way this never stops the timer, it just hides the popup (see
       closeTimerInlineBox); it keeps counting down regardless of which screen is on top or whether
       this is open. Same 4-equal-column shape as the metronome transport row (.metro-transport-grid). */
    /* ML-481: the pop-up's time is the Timer screen's ring, a size smaller, above the controls. The controls
       share the row equally however many are showing (the length to pick is there only when idle). */
    .timer-ring.timer-ring-sm { width: min(100%, 200px); margin-top: var(--space-5); }
    .timer-ring-sm .timer-ring-time { font-size: var(--font-2xl); }
    .timer-ring-sm.is-long .timer-ring-time { font-size: var(--font-xl); }
    .timer-ring.is-open-ended :is(.timer-ring-arc, .timer-ring-tip, .timer-ring-lap, .timer-ring-lap-track) { display: none; } /* nothing to count down to */
    .timer-inline-controls { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); margin: var(--space-2) var(--space-10) 0 0; }
    .timer-ring:not(.hidden-group) + .timer-inline-controls { margin-right: 0; } /* idle, the row keeps clear of the X; with the ring above it, it is under the X */
    /* .metroBlk- classes: started life as the Metronome Blocks multi-bar tool (ML-35, removed
       2026-09-27 - superseded by pieces in My music and Rehearse). What's left here is shared:
       the now-playing row, value boxes, tile strip, pickers and the mini tuner, used by Quick Play,
       Rehearse and the Flow editor. */

    /* Tuner access from this screen (previously a fully separate, self-contained tool/view) - toggled
       on/off from the top bar (ML-91, topTunerToggleBtn) rather than a close button of its own, and
       sits first in the view rather than between the transport row and the setup header, so it's the
       first thing on screen when it's on. Shares the same tunerEngine singleton as the full Tuner view
       (see METRONOME BLOCKS MINI TUNER in app.js), just with its own compact renderer.
       Collapsed by default (max-height/opacity/padding/border all animate together) rather than
       display:none, specifically so opening/closing it is a visible slide, not a hard cut - toggled
       via .metroBlk-mini-tuner-open, not hidden-group. */
    /* ML-164: standard page background when shown, not the clickable input-bg - the tuner is a
       display/readout, not a control, so it shouldn't read as tappable the way the buttons around it do.
       ML-164 follow-up: pulled up with a negative top margin so the gap to the top bar above it is a
       plain 4px rather than .container's full var(--space-5) top padding (used for every view, not
       just this one, so it can't just be shrunk generally) - only matters while open, but set
       unconditionally since it's harmless while collapsed too. */
    .metroBlk-mini-tuner { box-sizing: border-box; margin-top: calc(-1 * var(--space-3)); background: var(--container-bg); border: 2px solid var(--input-border); border-width: 0; border-radius: var(--radius-md); padding: 0 var(--space-4); margin-bottom: 0; max-height: 0; opacity: 0; overflow: hidden; pointer-events: none; transition: max-height var(--duration-base) ease, opacity var(--duration-base) ease, padding var(--duration-base) ease, margin-bottom var(--duration-base) ease, border-width var(--duration-base) ease, background var(--duration-fast) linear, border-color var(--duration-fast) linear; }
    .metroBlk-mini-tuner.metroBlk-mini-tuner-open { border-width: 2px; padding: var(--space-3) var(--space-4); margin-bottom: var(--space-3); max-height: 220px; opacity: 1; pointer-events: auto; }
    /* ML-385: while open it stays pinned under the top bar (--header-h, measured by app.js) as the page
       scrolls, like the Display preview - so the note is always in view while you play. Opaque, so nothing
       shows through: the in-tune tint (translucent) is laid over the plain surface. */
    .metroBlk-mini-tuner.metroBlk-mini-tuner-open { position: sticky; top: var(--header-h, var(--space-0)); z-index: var(--z-sticky); box-shadow: var(--shadow-md); }
    .metroBlk-mini-tuner.in-tune { border-color: var(--tuner-in-tune-text); background: linear-gradient(var(--tuner-in-tune-tint), var(--tuner-in-tune-tint)), var(--container-bg); }
    /* One big note on the left (not concert-and-instrument side by side any more - ML-84: showing both
       at once meant nothing to read the note against without already knowing which was which) at a
       fixed width, so the box no longer changes size as different note names come and go; the bar
       track fills the middle (flex:1 - narrows on its own as the instrument/close buttons beside it
       take more room, no separate width rule needed) and the instrument button + close icon sit on the
       right (ML-164 follow-up - replaces the old 3-dot Settings/Close menu; the instrument label row
       that used to sit underneath is gone too, for a shorter box overall). */
    .metroBlk-mini-tuner-body { display: flex; align-items: center; gap: var(--space-3); }
    .metroBlk-mini-tuner-note-big { min-width: 2.4ch; flex-shrink: 0; font-size: var(--font-2xl); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); text-align: center; color: var(--primary-action-strong); font-variant-numeric: tabular-nums; }
    .metroBlk-mini-tuner-bar-wrap { flex: 1; min-width: 0; }
    /* Opens the instrument picker directly - short code (C/B♭/E♭/F) rather than the old full-name row.
       Same tile look as .metroBlk-ctrl-value-btn, and no dropdown caret (ML-205). */
    .metroBlk-mini-tuner-instrument-btn { position: relative; flex-shrink: 0; height: 40px; padding: 0 var(--space-3); border: 1px solid var(--control-border); border-radius: var(--radius-lg); background: var(--input-bg); color: var(--text-color); font-size: var(--font-md); font-weight: var(--font-weight-bold); cursor: pointer; display: flex; align-items: center; justify-content: center; }
    /* Same 40x40 filled clickable-colour circle language as the row's own volume/menu buttons
       elsewhere on this page (ML-164) rather than a bare transparent icon. */
    .metroBlk-mini-tuner-close-btn { flex-shrink: 0; width: 40px; height: 40px; min-width: 40px; border-radius: var(--radius-circle); background: var(--input-bg); color: var(--label-color); border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; }
    .metroBlk-mini-tuner-close-btn .material-symbols-outlined { font-size: var(--icon-md); }
    /* The instrument-picker popup's buttons - one per instrument, applies immediately on click (ML-84
       follow-up: replaces the native <select>, which wasn't the "pop up with a set of buttons" asked
       for). Reuses .metroBlk-timesig-opt's look (same "pick one of a small fixed set" pattern already
       used for time signatures) rather than inventing a new button style. */
    /* ML-225: 3 across now it holds all 12 transpositions (4 rows), not just Concert/B♭/E♭/F. */
    .metroBlk-instrument-options { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-2); }
    /* Same height as the cog icon next to it (36px) rather than a thin strip - the two read as one
       row of matched controls instead of a slim bar dwarfed by a much taller button beside it. */
    .metroBlk-mini-tuner-bar-track { position: relative; height: 36px; background: var(--input-bg); border: 1px solid var(--input-border); border-radius: var(--radius-md); overflow: hidden; }
    .metroBlk-mini-tuner-bar-zone { position: absolute; top: 0; bottom: 0; left: 35%; right: 35%; background: var(--tuner-in-tune-zone); }
    .metroBlk-mini-tuner-bar-center { position: absolute; top: 0; bottom: 0; left: 50%; width: 2px; background: var(--input-border); transform: translateX(-50%); }
    .metroBlk-mini-tuner-needle { position: absolute; top: 1px; bottom: 1px; left: var(--needle-pos, 50%); width: 4px; border-radius: var(--radius-2xs); background: var(--danger-color); transform: translateX(-50%); transition: left var(--duration-instant) linear, background var(--duration-instant) linear; }
    .metroBlk-mini-tuner-needle.in-tune { background: var(--tuner-in-tune-green); }

    /* Sub beats / play speed controls (ML-91 follow-up): collapsed buttons showing a big current
       value and a small label underneath - same "square, not round" language as Play/Reset
       (.metro-transport-btn) and the same row height, so the whole transport row reads as one
       consistent set rather than a mix of shapes. Sub beats opens a popup built from the same
       stepper+slider box as the segment editor's own BPM/bar-count controls (.metroBlk-bpm-box);
       play speed opens a plain button grid instead (.metroBlk-speed-options) - see the JS/HTML
       comments on each for why they ended up different shapes. Background matches the block tiles
       below (.metroBlk-tile's --input-bg/--input-border) rather than the silver of Reset - these two
       are secondary adjustments, not transport controls, and shouldn't visually compete with Play. */
    /* Add a piece's "how do you want to make it" tiles (ML-400) - the square, icon-topped tiles My music's
       start screen had until ML-329, brought back: two to a row, so they read as ways to go on rather than
       as more options to pick. grid-auto-rows keeps every row the same height (ML-223), so a tile whose
       help text wraps further doesn't grow taller than the others. See specs/components/add-piece.md. */
    .metroBlk-entry-choices { display: grid; grid-template-columns: repeat(2, 1fr); grid-auto-rows: 1fr; gap: var(--space-3); }
    .metroBlk-entry-choice { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); min-height: 140px; padding: var(--space-4); border: 2px solid var(--control-border); border-radius: var(--radius-md); background: var(--input-bg); color: var(--text-color); font: inherit; cursor: pointer; text-align: center; box-sizing: border-box; }
    .metroBlk-entry-choice .material-symbols-outlined { font-size: var(--icon-xl); color: var(--primary-action-strong); }
    .metroBlk-entry-choice strong { font-size: var(--font-md); }
    .metroBlk-ctrl-value-btn { position: relative; height: 60px; min-width: 74px; padding: 0 var(--space-3); border: 1px solid var(--control-border); border-radius: var(--radius-lg); background: var(--input-bg); color: var(--text-color); cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; line-height: var(--line-height-snug); box-sizing: border-box; }
    .metroBlk-ctrl-value-btn strong { font-size: var(--font-md); font-weight: var(--font-weight-bold); font-variant-numeric: tabular-nums; }
    /* Caption under a control's icon/value. Inherits the button's text colour so it always matches the
       icon above it (Play/Reset/Start/Stop on their filled fills) - only a neutral value box mutes it. */
    .metroBlk-ctrl-value-label { font-size: var(--font-xs); color: inherit; white-space: nowrap; }
    .metroBlk-ctrl-value-btn .metroBlk-ctrl-value-label { color: var(--label-color); }
    /* ML-205: no dropdown caret - these open the app's own tile pickers, not a native dropdown, so they
       use the same tile look as the Flow block tiles (.flow-tile: 1px --input-border, --radius-lg,
       --font-md value over a --label-color caption) - see specs/components/selectable-tile.md. */

    /* Play speed's popup (ML-91 follow-up: the slider lost the old +/- steppers' quick, repeatable
       jumps, and a typed exact value doesn't really matter when only round 10%-step numbers are ever
       useful) - 13 presets from 30% to 150%, auto-filling as many columns as fit rather than a fixed
       count so the last row doesn't end up with one lonely button, applying immediately on tap like
       the time-signature/instrument pickers rather than needing a separate Save. */
    .metroBlk-speed-options { display: grid; grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); gap: var(--space-2); }

    /* A 4-across grid rather than a scrolling row - a setup can hold plenty of blocks, and a single
       scrolling row would hide most of them off-screen. Grid (not flex-wrap) so the columns stay
       even regardless of how many tiles end up in the last row. */
    .metroBlk-tile-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-3); padding: var(--space-2) 0 var(--space-4) 0; }
    /* ML-367: touch-action: manipulation, not the old Metronome Blocks editor's none (ML-81, for its
       reorder drag - that editor is gone). none stopped a swipe that started on a tile from scrolling
       the page, so Rehearse's 4-column strip - nearly all tiles - was hard to scroll. The tile is only
       tapped now; no text selection or long-press callout, so a press and hold then swipe just scrolls. */
    .metroBlk-tile { position: relative; min-height: 100px; min-width: 0; background: var(--input-bg); border: 2px solid var(--control-border); border-radius: var(--radius-md); padding: var(--space-2); cursor: pointer; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: var(--space-1); text-align: center; touch-action: manipulation; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
    .metroBlk-tile.dragging { opacity: 0.5; box-shadow: var(--shadow-lg); }
    /* ML-302: Rehearse's 4-column tiles line up row by row - heading, bpm, bars - whatever the heading
       is (a boxed rehearsal mark, a 2-line long mark, a bar number, "Lead-in"). Each tile is a subgrid
       over three shared rows, so every line sits at the same height as its neighbours'. Every tile in
       this strip has exactly those three children. */
    .metroBlk-tile-strip.flow-play-tile-strip { grid-auto-rows: auto; }
    .flow-play-tile-strip .metroBlk-tile { grid-row: span 3; display: grid; grid-template-rows: subgrid; row-gap: var(--space-1); align-content: center; justify-items: center; align-items: center; }
    .metroBlk-tile.lead-in { border-color: var(--selection-text); background: var(--selection-tint); }
    .metroBlk-tile-sig { font-size: var(--font-lg); font-weight: var(--font-weight-bold); }
    .metroBlk-tile-bpm { font-size: var(--font-sm); color: var(--label-color); }
    .metroBlk-tile-bars { font-size: var(--font-xs); color: var(--label-color); }
    /* Play Flow's own tile headline (ML-179 follow-up) - a rehearsal mark, boxed so it reads as a
       named section rather than a plain number; .metroBlk-tile-sig itself (same size/weight, no box)
       is reused for the plain starting-bar-number case. */
    .metroBlk-tile-mark-box { font-size: var(--font-lg); font-weight: var(--font-weight-bold); border: 2px solid var(--text-color); border-radius: var(--radius-sm); padding: 0 var(--space-2); line-height: var(--line-height-base); max-width: 100%; box-sizing: border-box; overflow-wrap: anywhere; }
    /* A mark longer than 3 characters: smaller, centred, at most 2 lines then "…" - never wider than
       the tile, even in the 4-column grid. The full name is in the tile's title/aria-label and leads
       the now-playing line. */
    .metroBlk-tile-mark-box-long { font-size: var(--font-sm); line-height: var(--line-height-snug); padding: var(--space-0-5) var(--space-1); align-self: stretch; overflow-wrap: normal; word-break: break-word; text-align: center; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
    /* Play Mode's own visual (ML-97/98) - replaces the old "coming next" preview row: whichever
       block is actually sounding right now gets a bold gold border/glow, both on a regular tile and
       the lead-in's own row (shares this class - see renderMetroBlkActiveTileHighlight). */
    .metroBlk-tile-active { border-color: var(--primary-action-strong); box-shadow: var(--focus-ring); }
    /* One shared menu (not one per tile) repositioned against whichever 3-dot button was tapped -
       position: fixed (not .dropdown-menu's own absolute) since it isn't nested inside the tile it's
       acting on, just placed near it via JS-computed top/left, clamped to the viewport on both axes
       (openMetroBlkTileMenu). */
    .metroBlk-tile-menu { position: fixed; top: auto; right: auto; left: auto; min-width: 160px; }
    /* Set apart from Copy to end/here (already separated from each other by .dropdown-item's own
       border-bottom) with extra space plus its own line above, so the destructive option isn't
       sitting flush against the harmless ones and easy to tap by mistake (ML-100). */
    .metroBlk-tile-menu-delete { color: var(--danger-text); margin-top: var(--space-2); border-top: 1px solid var(--input-border); }

    /* Play panel: current block + up to 2 faded upcoming blocks, each boxed as its own card so a
       run of circles reads as one connected unit rather than loose dots floating on the page. The
       top box scrolls within itself exactly like the single-bar tool's .metro-display-viewport/
       -content (reused as-is via the same JS helpers, just against these rows' own element ids). */
    /* ML-164: same background as the page itself, not the "clickable" input-bg - this row is a plain
       display surface (mark/label/dots), not a control, so it shouldn't visually compete with the
       actual buttons (volume, transport row) that ARE clickable. */
    .metroBlk-row { background: var(--container-bg); border: 1px solid var(--input-border); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-3); }
    /* ML-146 added the volume button as a third child of this row (align-self:center there, see
       .metroBlk-row-volume-btn) - align-items:center here (not the old baseline) keeps the mark/label
       text vertically centered against it too, rather than baseline-aligning the text while the
       button centers independently, which read as the text sitting near the top of the row once the
       button's own 44px tap target (ML-148) made the row noticeably taller than the text alone. */
    .metroBlk-row-head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-1); font-size: var(--font-sm); color: var(--label-color); font-weight: var(--font-weight-bold); }
    /* Reserved for a future rehearsal-mark readout (ML-35) - empty for now, but the layout leaves it
       room so that later addition doesn't reflow this row. */
    .metroBlk-row-mark { min-width: 28px; }
    /* Connects the first dot to the last - inserted as the first child of the dots row so later
       dots (added after it in DOM order) paint on top without needing an explicit z-index. Its
       left/right are set in JS from the dots' own already-computed positions (see
       connectMetroBlkDotsWithTrack), not recalculated here, so it can never drift out of step with
       where the dots actually are. */
    .metroBlk-row-track { position: absolute; top: 50%; left: var(--track-start); right: calc(100% - (var(--track-end))); /* first dot to the bar's end (connectMetroBlkDotsWithTrack) */ height: 2px; background: var(--input-border); transform: translateY(-50%); }
    /* Marks where the line's extra one-slot extension (past the last dot) ends - the loop-back point
       time still has to pass through before returning to beat 1, not a hard stop. */
    .metroBlk-row-track-end { position: absolute; top: 50%; left: var(--track-end); width: 2px; height: 14px; background: var(--input-border); transform: translate(-50%, -50%); }

    /* Note-value picker ("Set by note value" in the block editor) - real vector glyphs (drawn in JS,
       see metroNoteIconSvg) rather than relying on unicode musical symbols, which render as missing-
       glyph boxes on plenty of systems. `currentColor` on every shape means the button's own `color`
       (themed already, light/dark alike) is what colours the glyph - no separate light/dark case
       needed here. */
    /* Icon-only now (no text label) - the picker modal has room to make the glyphs themselves
       bigger instead, which reads faster than a row of small icon+text pairs. Fixed 4-column grid
       (same "don't stretch buttons to fill the row" rule as every other picker in this app - see
       .flow-tile-grid/metroSeg-timesig-family-grid) rather than flex:1, so a picker with fewer than
       4 options (Start of bar's 2) or more doesn't change how wide each button is - Beat note's 8
       fills exactly two rows, 2 leaves the outer columns empty (individual pickers re-center
       themselves into the middle two columns where that's wanted - see #flowBarStartPicker below). */
    .metroBlk-note-picker { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2); margin-bottom: var(--space-2); }
    .metroBlk-note-btn { background: var(--input-bg); border: 2px solid var(--control-border); border-radius: var(--radius-md); padding: var(--space-3) var(--space-1); cursor: pointer; color: var(--text-color); display: flex; align-items: center; justify-content: center; }
    .metroBlk-note-btn.selected { border-color: var(--primary-action-strong); color: var(--primary-action-strong); background: var(--primary-action-tint); }
    .metroBlk-note-btn .metroBlk-note-svg { width: 26px; height: 44px; }
    /* Start of bar has just 2 options - centered into the grid's middle two columns (2 and 3)
       rather than left-packed into 1 and 2. */
    #flowBarStartPicker .metroBlk-note-btn:nth-child(1) { grid-column: 2; }
    #flowBarStartPicker .metroBlk-note-btn:nth-child(2) { grid-column: 3; }

    /* The Blocks segment-edit modal's own trigger for .metroBlk-note-picker above used to be a
       separate compact "note = bpm" row (.metroBlk-note-select-btn/-eq/-bpm-row); ML-103 replaced it
       with the qp-timesig-cell/qp-notelen-cell/qp-bpm-cell card layout below instead (still opens
       this same picker), so those three rules were removed as dead weight. */
    .metroBlk-bpm-box { flex: 1; min-width: 0; background: var(--container-bg); border: 1px solid var(--input-border); border-radius: var(--radius-md); padding: var(--space-3); }
    /* .slider-thumb's border defaults to var(--container-bg) so it reads as a gap against the
       page background it normally sits on - inside this box the surrounding surface is
       var(--input-bg) instead (a shade lighter), so the ring needs to match that here specifically
       rather than leaving a visible mismatched edge. */
    .metroBlk-bpm-box .slider-thumb { border-color: var(--primary-action-strong); }

    /* Close [x] in a modal's top-right corner - the standard way to dismiss a pure picker modal
       (no separate Cancel/Save pair makes sense for one, since picking an option already applies
       it immediately and closes) without making any change. Most modals in this app still use a
       Cancel/Save row instead; this is deliberately only for that picker shape. Applies its
       relative positioning automatically to any modal-content that contains one, rather than
       needing a new scoped rule added per modal. */
    .modal-close-x { padding: 0; position: absolute; top: var(--space-3); right: var(--space-3); width: 48px; height: 48px; border-radius: var(--radius-circle); border: none; background: var(--input-bg); color: var(--text-color); font-size: var(--font-lg); line-height: var(--line-height-tight); cursor: pointer; display: flex; align-items: center; justify-content: center; }
    .modal-content:has(.modal-close-x) { position: relative; }

    /* Opt-in modifier (ML-103, the block editor modal only - not applied to .modal-content
       generally, which every other modal in the app still uses as a single scrolling box) - splits
       the box into a scrolling field area plus a Cancel/Save bar that stays pinned to the bottom of
       the popup itself, not just the end of a long scroll. */
    .modal-content-sticky-footer { display: flex; flex-direction: column; max-height: 90vh; padding: 0; overflow: visible; }
    .modal-content-sticky-footer .metroSeg-scroll-area { overflow-y: auto; padding: var(--space-6); flex: 1 1 auto; min-height: 0; }
    .metroSeg-sticky-footer { flex-shrink: 0; display: flex; gap: var(--space-2); padding: var(--space-4) var(--space-6); border-top: 1px solid var(--input-border); background: var(--container-bg); border-radius: 0 0 var(--radius-xl) var(--radius-xl); }

    /* Time signature picker (ML-153): the underlying "grey tap-target, gold when selected" button
       look is still .metroBlk-timesig-opt (shared with the tuner instrument picker - Concert/Bb/Eb/F -
       so its own rule stays put), just laid out as three fixed 4-column family grids
       (renderMetroSegTimeSigPicker groups the catalog by time_signature_options.family) instead of
       one column per denominator. Replaces a native <select> so the trigger can be the same grey
       "opens a popup" button already used elsewhere (.metroBlk-ctrl-value-btn/.qp-timesig-cell). */
    .metroBlk-timesig-opt { width: 100%; min-height: 48px; background: var(--input-bg); border: 2px solid var(--control-border); border-radius: var(--radius-sm); padding: var(--space-3) var(--space-1); text-align: center; font-weight: var(--font-weight-bold); font-size: var(--font-md); cursor: pointer; color: var(--text-color); box-sizing: border-box; display: flex; align-items: center; justify-content: center; }
    .metroBlk-timesig-opt.selected { border-color: var(--primary-action-strong); background: var(--primary-action-tint); color: var(--primary-action-strong); box-shadow: var(--shadow-glow-soft); }
    .metroSeg-timesig-family { margin-bottom: var(--space-4); }
    .metroSeg-timesig-family-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2); }
    .metroSeg-timesig-divider { border-top: 1px solid var(--input-border); margin: var(--space-2) 0 var(--space-4) 0; }
    .metroSeg-timesig-custom-row { display: flex; align-items: flex-end; gap: var(--space-2); }
    .metroSeg-timesig-custom-field { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }
    .metroSeg-timesig-custom-field label { font-size: var(--font-sm); color: var(--label-color); }
    .metroSeg-timesig-custom-input { width: 100%; height: 44px; box-sizing: border-box; text-align: center; font-weight: var(--font-weight-bold); font-size: var(--font-md); background: var(--input-bg); border: 1px solid var(--control-border); border-radius: var(--radius-sm); color: var(--text-color); }
    .metroSeg-timesig-custom-divider { padding-bottom: var(--space-3); font-size: var(--font-lg); font-weight: var(--font-weight-bold); color: var(--label-color); }
    .metroSeg-timesig-set-btn { height: 44px; padding: 0 var(--space-4); border: none; border-radius: var(--radius-sm); background: var(--primary-action); color: var(--primary-action-text); font-weight: var(--font-weight-bold); cursor: pointer; flex-shrink: 0; }
    .metroSeg-timesig-chips-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
    .metroSeg-timesig-chips-label { font-size: var(--font-sm); color: var(--label-color); }
    .metroSeg-timesig-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
    /* Wrapper around a select button (tap to apply) and a small trailing × (archives the signature
       instead of selecting it - see renderMetroSegTimeSigChips in app.js, ML-153). Same standard-box
       look as the preset tiles above it (.metroBlk-timesig-opt) - rounded corners, not a pill. */
    .metroSeg-timesig-chip { display: flex; align-items: stretch; background: var(--input-bg); border: 2px solid var(--control-border); border-radius: var(--radius-sm); overflow: hidden; }
    .metroSeg-timesig-chip-select { background: none; border: none; padding: var(--space-2) var(--space-1) var(--space-2) var(--space-4); font-weight: var(--font-weight-bold); font-size: var(--font-sm); color: var(--text-color); cursor: pointer; }
    .metroSeg-timesig-chip-select:hover { background: var(--input-border); }
    .metroSeg-timesig-chip-remove { background: none; border: none; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-2); font-size: var(--font-md); line-height: var(--line-height-tight); color: var(--label-color); cursor: pointer; }
    .metroSeg-timesig-chip-remove:hover { color: var(--danger-text); }
    /* Overrides the base .metroSeg-sticky-footer's plain gap-of-buttons layout (metroSegmentModal's
       Cancel/Save) to right-align the lone Done button instead. */
    .metroSeg-timesig-footer { justify-content: flex-end; align-items: center; }

    #metroBlkEntryLibrary { margin-top: var(--space-4); }

    .metroBlk-mini-label-readout { font-size: var(--font-sm); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

    /* Quick Play (front page, replaces the old single-bar Metronome): every bar's full detail entry
       is always visible and stacked, rather than a tile you tap to open a modal - so each one gets
       its own bordered card ("Bar N") to read as a distinct bar. Always exactly 1 bar/no repeat per
       block (that's a Flow-only concept) - just time signature, note length and target BPM. */
    /* ML-145: the box is now just a clipping wrapper (overflow:hidden) around two layers - the red
       delete underlay (.qp-block-delete-underlay, revealed by swiping) sits behind, and the visible
       card itself (.qp-block-surface) sits in front and slides. touch-action: pan-y (not none) lets
       native vertical page scroll work when a touch starts on the card body - only a deliberate
       horizontal swipe (locked in by wireQpBarSwipe, >30px horizontal/<15px vertical before it takes
       over) intercepts the gesture at all, which is the whole point of this ticket (the old
       touch-action:none on the full box, grabbing horizontal AND vertical movement anywhere on it,
       is what was fighting page scroll). Vertical reordering moved entirely to the dedicated grab
       handle below, which keeps its own touch-action:none since IT is meant to capture vertical drag. */
    .qp-block-box { position: relative; border-radius: var(--radius-md); margin-bottom: var(--space-3); overflow: hidden; touch-action: pan-y; }
    /* ML-164: page background, not the clickable input-bg - the card itself isn't a control, only the
       time-signature/beat-unit/BPM-stepper buttons inside it (which keep their own explicit input-bg
       background regardless of what this parent sets) are. */
    .qp-block-surface { position: relative; background: var(--container-bg); border: 1px solid var(--input-border); border-radius: var(--radius-md); padding: var(--space-4); transition: border-color var(--duration-fast) ease, border-width var(--duration-fast) ease; }
    /* Whichever bar is actually sounding right now, while playing only (renderQpActiveBoxHighlight) -
       same bold-gold-border language as Blocks' own .metroBlk-tile-active. Targets the surface (the
       visible card), not the outer clipping box, which has no border/background of its own any more. */
    .qp-block-box.qp-block-playing .qp-block-surface { border-color: var(--primary-action-strong); box-shadow: var(--focus-ring); }
    /* Revealed behind .qp-block-surface as it slides left - width matches QP_SWIPE_OPEN_PX in app.js,
       the two must stay in sync. */
    .qp-block-delete-underlay { position: absolute; top: 0; right: 0; bottom: 0; width: 96px; background: var(--danger-color); color: var(--text-on-accent); border: none; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-0-5); cursor: pointer; font-weight: var(--font-weight-bold); font-size: var(--font-sm); }
    .qp-block-delete-underlay .material-symbols-outlined { font-size: var(--icon-lg); }
    /* min-height matches .qp-bar-grab-handle's own 44px (not just its icon's natural line-height) so
       a 1-bar card (no handle - see qpBlockBoxHtml) reserves exactly the same header height a
       multi-bar card's handle would occupy, rather than shrinking to fit the title text alone - without
       this the card's overall height visibly differed depending on whether the handle was present. */
    .qp-block-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-3); min-height: 44px; }
    .qp-block-header-left { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
    .qp-block-title { font-weight: var(--font-weight-bold); font-size: var(--font-md); }
    /* 44x44 touch target (ML-145) with a small, subtle glyph inside it - grey by default, gold on
       hover/active to match the drag affordance, same primary-action token the rest of the app uses
       rather than a one-off colour. touch-action:none isolates vertical drag-and-drop strictly to this
       handle (wireQpBarGrabHandle) - the card body around it deliberately does NOT set this, see
       .qp-block-box above. Only rendered at all when there's more than one bar to reorder (qpBlockBoxHtml). */
    /* justify-content:flex-start (not center) so the glyph itself sits flush with the card's left
       edge, lining up with .qp-bar-fields-grid below it - the 44px width is purely an invisible tap
       target beyond that, not something the icon should visually center within. */
    .qp-bar-grab-handle { width: 44px; height: 44px; flex-shrink: 0; border: none; border-radius: var(--radius-sm); background: transparent; color: var(--label-color); display: flex; align-items: center; justify-content: flex-start; cursor: grab; touch-action: none; }
    .qp-bar-grab-handle:hover, .qp-bar-grab-handle:active { color: var(--primary-action-strong); }
    .qp-bar-grab-handle:active { cursor: grabbing; }
    /* Same corner-menu language as the Blocks tile's own per-tile menu button (.metroBlk-tile-menu-btn)
       - always shown, even with only one bar (opening it then only offers Duplicate, see
       openQpBarMenu), rather than the old dustbin icon that hid entirely on the one bar that couldn't
       be deleted. */
    /* ML-164: 40x40 filled clickable-colour circle, same treatment as the row's own volume button,
       instead of a bare transparent icon. */
    .qp-bar-menu-btn { width: 40px; height: 40px; border: none; border-radius: var(--radius-circle); background: var(--input-bg); color: var(--label-color); display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; }
    .qp-bar-menu-btn .material-symbols-outlined { font-size: var(--icon-md); }
    /* Time signature stacked above note length in column 1 (each takes half its combined height,
       via the 2-row grid + stretch below) - target BPM fills the remaining 3 columns at that same
       combined height, rather than a plain quarter/quarter/half row of equal-height cells. */
    .qp-bar-fields-grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, 1fr); gap: var(--space-2); }
    /* .metroBlk-ctrl-value-btn's own fixed height (60px, right for the single-row transport buttons
       above) would stop these two stretching to fill half the taller combined row height - auto lets
       the grid's default stretch take over instead. */
    .qp-timesig-cell, .qp-notelen-cell { height: auto; }
    .qp-timesig-cell { grid-column: 1; grid-row: 1; }
    .qp-notelen-cell { grid-column: 1; grid-row: 2; }
    /* ML-164: page background here too (not .metroBlk-bpm-box's own default input-bg, still used as-is
       by the segment editor modal) - only the +/-1 steppers inside (.metro-bpm-step) are clickable and
       keep their own explicit input-bg regardless of what this parent sets. Border stays the normal
       light var(--input-border) (not hidden against the background) so this box still reads as one
       grouped unit, same as the time/beat-unit boxes beside it. */
    .qp-bpm-cell { grid-column: 2 / span 3; grid-row: 1 / span 2; background: var(--container-bg); border-color: var(--input-border); }
    .qp-note-btn-icon { display: flex; align-items: center; justify-content: center; }
    .qp-note-btn-icon svg { width: 14px; height: 24px; }
    /* Pulled in tight around the number rather than metro-speed-row's normal wide gap (meant for a
       standalone, narrower bpm box) - this one now spans 3 of 4 columns, wide enough that the default
       gap would push -/+ out past where the slider below actually starts/ends. */
    .qp-bpm-speed-row { gap: var(--space-2); }
    /* Add/move/duplicate/delete feedback (qpAnimateBlocksChange/qpAnimateDeleteThenRerender) - applied
       just for the moment of the animation itself (added/removed by JS once it finishes) rather than
       permanently, so a bar's own layout-affecting style changes (e.g. resizing while editing a field)
       never unintentionally animate through this. */
    .qp-block-animating { transition: transform var(--duration-base) ease, opacity var(--duration-base) ease; }
    /* Same move-feedback treatment, Flow's own block cards (flowAnimateBlocksChange/animateFlipReorder) -
       applied just for the moment of the animation itself, same reasoning as .qp-block-animating above. */
    .flow-block-animating { transition: transform var(--duration-base) ease, opacity var(--duration-base) ease; }
    /* Gold, thicker border on whichever card is actually being reordered - applied the moment the
       grab handle is picked up (wireVerticalDragHandle) and kept through the settle animation
       (animateFlipReorder re-applies it to the freshly re-rendered element, matched by raiseUid),
       reverting the instant it lands. .qp-block-surface/.flow-block-card's own border-color/width
       transition (above) is what makes both the appearance and the reversion animate smoothly rather
       than snapping. One shared class/rule for both surfaces, not a copy per screen. */
    .qp-block-surface.drag-reorder-highlight, .flow-block-card.drag-reorder-highlight { border-color: var(--primary-action-strong); border-width: 3px; }
    /* "Bars" section-title's own right-side slot - low-emphasis text links, not buttons, so they read
       as secondary next to the bold title beside them; a small gap keeps the two from reading as one
       run-on phrase. */
    .qp-bars-title-links { display: flex; gap: var(--space-4); }
    .qp-bars-title-link { font-size: var(--font-sm); font-weight: var(--font-weight-normal); color: var(--label-color); text-decoration: underline; cursor: pointer; }

    /* ML-163: "Merge bars?" - see the modal's own HTML comment for the shape. Reuses the sticky-footer
       modal skeleton (.metroSeg-sticky-footer et al) but stacks a toggle row above the button row
       instead of that base class's plain single row of buttons. */
    .qp-merge-sticky-footer { flex-direction: column; align-items: stretch; gap: var(--space-3); }
    .qp-merge-toggle-row { display: flex; align-items: center; justify-content: space-between; font-size: var(--font-base); }
    .qp-merge-buttons-row { display: flex; gap: var(--space-2); }
    .qp-merge-buttons-row .btn-cancel, .qp-merge-buttons-row .btn-submit { flex: 1; margin-bottom: 0; }

    .qp-merge-group { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); }
    /* The border-left is the "bracket" - every bar feeding the same block visually converges on one
       line before the arrow/connector column, no per-bar line-drawing needed. */
    .qp-merge-bars { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); border-left: 2px solid var(--input-border); padding-left: var(--space-3); }
    .qp-merge-bar-row, .qp-merge-flat-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-1); font-size: var(--font-xs); background: var(--input-bg); border: 1px solid var(--input-border); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-2); }
    .qp-merge-flat-row { margin-bottom: var(--space-1); font-size: var(--font-sm); }
    .qp-merge-bar-num { color: var(--label-color); flex-shrink: 0; }
    .qp-merge-bar-fields { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; column-gap: var(--space-2); row-gap: var(--space-0-5); font-weight: var(--font-weight-bold); justify-content: flex-end; text-align: right; }
    /* Sits on the bar row itself, right where its own line "leaves the bar" for the shared merge
       point - tapping it peels just that bar out into a new block (qpMergeSplitBar), not the whole
       group. Only rendered on a bar that's actually part of a multi-bar block. */
    .qp-merge-split-btn { flex-shrink: 0; width: 26px; height: 26px; border-radius: var(--radius-circle); border: none; background: transparent; color: var(--label-color); display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
    .qp-merge-split-btn .material-symbols-outlined { font-size: var(--icon-sm); }
    .qp-merge-connector { width: 40px; flex-shrink: 0; display: flex; flex-direction: column; align-items: center; gap: var(--space-1); }
    .qp-merge-arrow { color: var(--label-color); font-size: var(--font-lg); }
    /* A freshly split-out bar's connector - replaces the plain arrow once it's its own block awaiting
       a merge decision (qpMergeToggleMenu opens the Merge above/below choice below it). */
    .qp-merge-branch-btn { width: 30px; height: 30px; border-radius: var(--radius-circle); border: 2px solid var(--primary-action-strong); background: var(--secondary-color); color: var(--primary-action-strong); display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
    .qp-merge-branch-btn .material-symbols-outlined { font-size: var(--icon-sm); }
    .qp-merge-menu { display: flex; flex-direction: column; gap: var(--space-1); align-items: stretch; }
    .qp-merge-menu-btn { font-size: var(--font-xs); padding: var(--space-1) var(--space-2); border-radius: var(--radius-sm); border: 1px solid var(--input-border); background: var(--container-bg); color: var(--text-color); cursor: pointer; white-space: nowrap; }
    .qp-merge-menu-btn:disabled { color: var(--label-color); opacity: 0.5; cursor: not-allowed; }
    .qp-merge-block { flex: 1; min-width: 0; border: 2px solid var(--primary-action-strong); border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); background: var(--secondary-color); }
    .qp-merge-block-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); font-size: var(--font-sm); font-weight: var(--font-weight-bold); }
    .qp-merge-block-badge { flex-shrink: 0; font-size: var(--font-xs); font-weight: var(--font-weight-bold); background: var(--primary-action); color: var(--primary-action-text); padding: var(--space-0-5) var(--space-2); border-radius: var(--radius-pill); }
    .qp-merge-block-fields { display: flex; flex-wrap: wrap; column-gap: var(--space-3); row-gap: var(--space-0-5); font-size: var(--font-sm); margin-top: var(--space-1); }

    /* ML-34 follow-up: the currently-loaded history entry's name, under the "Bars" title. No negative
       margin here any more - that pulled this row up into .section-title's own border-bottom line
       above it, overlapping the two. */
    .qp-loaded-history-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin: var(--space-2) 0 var(--space-3) 0; font-size: var(--font-sm); color: var(--label-color); }
    .qp-loaded-history-info { display: flex; align-items: center; gap: var(--space-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    /* Decorative only (aria-hidden, no click handler) - just marks this line as "loaded from history"
       at a glance, same muted colour as the rest of the row. */
    .qp-loaded-history-icon { font-size: var(--font-base); flex-shrink: 0; }
    .qp-loaded-history-edited-tag { flex-shrink: 0; }
    /* 32px (up from an earlier 22px pass) - still compact for a lightweight info row, but a
       meaningfully bigger tap target than before. */
    .qp-loaded-history-clear-btn { width: 32px; height: 32px; flex-shrink: 0; border-radius: var(--radius-circle); border: 1px solid var(--control-border); background: var(--input-bg); color: var(--label-color); font-size: var(--font-base); line-height: var(--line-height-tight); display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
    .qp-loaded-history-undo-btn { flex-shrink: 0; width: 28px; height: 28px; border-radius: var(--radius-circle); border: none; background: transparent; color: var(--label-color); display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
    .qp-loaded-history-undo-btn .material-symbols-outlined { font-size: var(--icon-md); }

    /* ML-34: "Show history" list rows - .history-item's own flex row plus a clickable body (selects
       the row) and a trailing 3-dot menu button. */
    .qp-history-item-body { flex-grow: 1; cursor: pointer; display: flex; align-items: center; gap: var(--space-3); }
    /* ML-288: a row's left edge in its session category's colour (.category-* sets --category-accent). */
    .history-item.category-edge { border-left-color: var(--category-accent); }
    /* Challenge editor: an item's left edge shows its status - open (gold), complete, closed. */
    .history-item.challenge-item-edge { border-left: 4px solid var(--primary-action); }
    .history-item.challenge-item-edge.is-complete { border-left-color: var(--success-color); }
    .history-item.challenge-item-edge.is-closed { border-left-color: var(--control-off-bg); }
    .history-item.is-muted { opacity: var(--opacity-muted); } /* an archived teacher/organisation */
    .history-item.qp-history-item-selected { border-left-color: var(--primary-action); background: var(--primary-action-tint); }
    .qp-history-star { color: var(--primary-action-strong); font-variation-settings: 'FILL' 1; font-size: var(--font-lg); }
    /* ML-175: generic 3-dot menu trigger for any list row (first used by Quick Play history, now
       also the session history list) - same grey-outline-plus-lighter-fill language as the main
       Quick Play page's own icon buttons (.qp-bar-menu-btn/.metroBlk-row-volume-btn) rather than
       .btn-icon-edit's bold, coloured-border look, which read as too heavy/large for a menu trigger
       sitting inside a compact list row. Pair with .dropdown-menu.metroBlk-tile-menu (+ .dropdown-item/
       .metroBlk-tile-menu-delete) for the menu itself - see openQpHistoryItemMenu/openSessionHistoryItemMenu
       for the shared positioning pattern. */
    /* .flow-bar-popup-icon-btn (ML-206): the same circle for the bar popup's ‹ › ✕, which navigate/close
       rather than open a menu - so they don't carry the menu-button class the a11y audit keys on. */
    .list-item-menu-btn, .flow-bar-popup-icon-btn { padding: 0; width: 40px; height: 40px; flex-shrink: 0; border-radius: var(--radius-circle); border: 1px solid var(--control-border); background: var(--input-bg); color: var(--label-color); display: flex; align-items: center; justify-content: center; cursor: pointer; }
    /* No :disabled styling existed on .btn-submit anywhere else in the app (nothing else disables
       one) - without this the Load button looked fully enabled (solid gold, pointer cursor) even
       while genuinely disabled, same dimmed-0.4 language every other disableable button already uses. */
    #qpHistoryLoadBtn:disabled { opacity: 0.4; cursor: not-allowed; }
    /* ML-164: same clickable colour as every other control on this page instead of .btn-cancel's
       hollow gold-outline treatment, which read as part of the plain background rather than as a
       tappable button. */
    /* scroll-margin-bottom (not a JS scroll offset) - scrollIntoView({block:'end'}) then naturally
       leaves this much breathing room below the button instead of pinning it flush to the viewport's
       bottom edge, with no extra timing/animation juggling needed around the smooth scroll itself. */
    #qpAddBlockBtn { background: var(--input-bg); border: 2px solid var(--control-border); color: var(--text-color); scroll-margin-bottom: 8px; }

    /* ML-164: the sub-beats Off/Auto/Fixed choice and the play-speed presets both use gold (matching
       the beat-unit picker's .metroBlk-note-btn.selected) for the selected option rather than the
       generic .radio-group's blue - scoped to just these two modals since every OTHER radio-group in
       the app still uses that shared blue treatment deliberately. Unselected reads at normal weight so
       only the actual selection stands out as bold. */
    #metroBlkSubdivideModal .radio-group.compact label,
    #qpSubdivideModal .radio-group.compact label { font-weight: var(--font-weight-normal); }
    #metroBlkSubdivideModal .radio-group.compact input[type="radio"]:checked + label,
    #qpSubdivideModal .radio-group.compact input[type="radio"]:checked + label {
        background: var(--primary-action-tint); color: var(--primary-action-strong); border-color: var(--primary-action-strong); font-weight: var(--font-weight-bold);
    }
    /* Same "only the selection is bold" principle for the play-speed preset grid - .metroBlk-timesig-opt
       is bold unconditionally everywhere else it's used (time signature, instrument picker), so this is
       scoped to just the speed-options grid rather than changing that shared base rule. */
    .metroBlk-speed-options .metroBlk-timesig-opt { font-weight: var(--font-weight-normal); }
    .metroBlk-speed-options .metroBlk-timesig-opt.selected { font-weight: var(--font-weight-bold); }

    /* The unused leading beats of a partial lead-in's bar (see greyOutSkippedDots) - a plain grey
       outline, not just a dimmed version of the normal dot. Full opacity and a background matching
       the row card (not transparent) so the connecting track line never shows through it. */
    /* .metro-dot-note prefixed for specificity - beat 1 of the bar is both "accent" (.metro-dot-note.accent)
       and, whenever the pickup is shorter than the bar, one of the skipped leading beats, and needs to
       render as skipped, not accented. */
    .metro-dot-note.metroBlk-dot-skipped, .metro-dot-sub.metroBlk-dot-skipped { opacity: 1; background: var(--input-bg); border-color: var(--input-border); }

    /* Layout utilities for the small set of repeated static inline-style patterns */
    .flex-row { display: flex; }
    .flex-col { display: flex; flex-direction: column; }
    .flex-1 { flex: 1; }
    .gap-sm { gap: var(--space-2); }
    .gap-md { gap: var(--space-3); }
    .text-center { text-align: center; }
    .no-margin { margin: 0; }

    /* ML-179 Phase 1: Flow Details Hub. One bordered/rounded "card" shape per
       section (details/recordings/documents/blocks), same design-system tokens
       as everywhere else in the app rather than a one-off palette (see the
       naming/theme note in that ticket's plan) - --container-bg/--input-border/
       --radius-xl instead of hardcoding either the mockup's light Tailwind
       colours or the ticket text's separate dark hex spec. */
    /* Edit Flow's 3-way Details/Media/Blocks tab bar - a segmented control, not the .flow-pill
       badge language, so its own radius scale (not --radius-pill, reserved for badges/chips). No
       more 3-dot menu sharing this row (see index.html) - it had nothing left in it once Visibility/
       Delete moved out of the popup, so the row is tabs-only again. */
    .flow-edit-tabs { display: flex; gap: var(--space-0-5); background: var(--input-bg); border: 1px solid var(--input-border); border-radius: var(--radius-lg); padding: var(--space-1); margin-bottom: var(--space-4); }
    .flow-edit-tab { flex: 1; background: transparent; border: none; color: var(--label-color); font-size: var(--font-sm); font-weight: var(--font-weight-bold); padding: var(--space-2) var(--space-1); border-radius: var(--radius-md); cursor: pointer; }
    .flow-edit-tab.active { background: var(--container-bg); color: var(--text-color); box-shadow: var(--shadow-sm); }
    .flow-edit-tab-count { color: inherit; opacity: 0.7; font-weight: var(--font-weight-normal); }
    /* Edit Flow's own content sits under a pinned sticky bottom bar (.flow-edit-sticky-bar below) -
       extra bottom padding on the view itself (not a scrolling element of its own - body is what
       scrolls, same as everywhere else in the app) keeps the bar from permanently covering the last
       bit of real content once scrolled all the way down. Sized for Edit mode's taller 3-row bar
       (Cancel/Save + Delete link, on top of Create mode's own single button) plus headroom for a
       safe-area inset. */
    #flowDetailsHubView { padding-bottom: var(--bottom-bar-clearance-lg); }
    /* Mirrors .top-bar-sticky-group/.top-bar's own "full-width flex-centered wrap + max-width inner
       bar" pattern, just pinned to the bottom instead of the top. The wrap spans the full viewport
       so it can center the bar under body's own centered column on desktop; pointer-events:none on
       it (restored to auto on the bar) stops its transparent side margins from eating clicks on
       whatever's behind them. */
    .flow-edit-sticky-bar-wrap { width: 100%; display: flex; justify-content: center; position: fixed; left: 0; bottom: 0; z-index: var(--z-header); pointer-events: none; }
    /* Visual boundary (border + upward elevation shadow, mirroring --shadow-md's values with the
       offset flipped to point up instead of down) separating the bar from scrolled content behind
       it, plus safe-area-aware bottom padding for the iOS home indicator (env() resolves to 0 - a
       harmless no-op - on any device/browser without one). */
    .flow-edit-sticky-bar { pointer-events: auto; width: 100%; max-width: var(--app-max-width); box-sizing: border-box; background: var(--container-bg); border-top: 1px solid var(--input-border); box-shadow: var(--shadow-top); padding: var(--space-3) var(--space-5) calc(var(--space-3) + env(safe-area-inset-bottom, 0px)) var(--space-5); }
    .flow-edit-sticky-bar .btn-submit { margin-bottom: 0; }
    /* Cancel + Save side by side, same shape as .metroBlk-edit-bar-actions. */
    .flow-edit-sticky-bar-actions { display: flex; gap: var(--space-3); }
    .flow-edit-sticky-bar-actions .btn-cancel, .flow-edit-sticky-bar-actions .btn-submit { flex: 1; margin-bottom: 0; }
    .flow-card { background: var(--container-bg); border: 1px solid var(--input-border); border-radius: var(--radius-xl); padding: var(--space-5); margin-bottom: var(--space-4); }
    .flow-card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-3); margin-bottom: var(--space-4); }
    /* Sentence case, same weight/size/color as a block card's own title (.flow-block-name) rather
       than the small uppercase label style used elsewhere - these are card titles, not field labels. */
    .flow-card-label { display: block; font-size: var(--font-md); font-weight: var(--font-weight-bold); color: var(--text-color); }
    .flow-card-sublabel { margin: var(--space-0-5) 0 0; font-size: var(--font-sm); color: var(--label-color); }
    .flow-visibility-value { margin: 0; font-size: var(--font-base); color: var(--text-color); }
    .flow-pill { flex-shrink: 0; font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--label-color); background: var(--input-bg); border: 1px solid var(--input-border); padding: var(--space-1) var(--space-3); border-radius: var(--radius-pill); white-space: nowrap; }
    .flow-pill-accent { color: var(--primary-action-strong); border-color: var(--primary-action-strong); background: var(--primary-action-tint); }
    .flow-required { color: var(--danger-text); }
    .flow-help-text { display: block; font-size: var(--font-xs); color: var(--label-color); margin-top: var(--space-1); }

    .flow-action-row { display: flex; gap: var(--space-3); margin-top: var(--space-3); }
    .flow-action-row .flow-action-btn { flex: 1; }
    /* Same neutral/hollow language as Quick Play's own #qpAddBlockBtn rather
       than a filled primary button - these are secondary "add another item"
       actions, not the one main action on the card. */
    .flow-action-btn { display: flex; align-items: center; justify-content: center; gap: var(--space-2); background: var(--input-bg); border: 2px solid var(--control-border); color: var(--text-color); font-size: var(--font-sm); font-weight: var(--font-weight-bold); padding: var(--space-3); border-radius: var(--radius-lg); cursor: pointer; }
    .flow-action-btn .material-symbols-outlined { font-size: var(--icon-md); }
    .flow-action-btn-wide { width: 100%; margin-top: var(--space-3); box-sizing: border-box; }

    /* ML-79 Phase 1: "Create from file" (flowFromFileView) - the tip banner ahead of the dropzone,
       same "informational, not a warning" tone as elsewhere, so it reads as a plain lightbulb tip
       (--secondary-color's pale wash) rather than borrowing --warning-color/--info-color, which
       already carry a specific meaning (toasts) elsewhere in this app. */
    .flow-from-file-banner { display: flex; gap: var(--space-3); align-items: flex-start; background: var(--secondary-color); border-radius: var(--radius-md); padding: var(--space-4); margin-bottom: var(--space-5); }
    .flow-from-file-banner .material-symbols-outlined { color: var(--primary-action-strong); font-size: var(--icon-lg); flex-shrink: 0; }
    .flow-from-file-banner p { margin: 0; font-size: var(--font-sm); line-height: var(--line-height-base); color: var(--text-color); }
    /* Dashed border marks this as a drop target even though drag-and-drop itself isn't wired up
       (tap-to-browse only) - same visual convention as everywhere else that pattern's used. */
    .flow-from-file-dropzone { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); width: 100%; box-sizing: border-box; padding: var(--space-6) var(--space-4); border: 2px dashed var(--primary-action-strong); border-radius: var(--radius-lg); background: var(--input-bg); color: var(--text-color); cursor: pointer; text-align: center; margin-bottom: var(--space-5); }
    .flow-from-file-dropzone .material-symbols-outlined { font-size: var(--icon-xl); color: var(--primary-action-strong); }
    .flow-from-file-dropzone strong { font-size: var(--font-md); }
    .flow-from-file-chip-row { display: flex; gap: var(--space-2); margin-top: var(--space-2); }
    .flow-from-file-result { background: var(--secondary-color); border-radius: var(--radius-md); padding: var(--space-5); margin-top: var(--space-4); }
    .flow-from-file-result-head { display: flex; align-items: center; gap: var(--space-2); font-weight: var(--font-weight-bold); margin-bottom: var(--space-4); }
    .flow-from-file-result-head .material-symbols-outlined { color: var(--success-text); }
    .flow-from-file-result-row { display: flex; justify-content: space-between; align-items: center; font-size: var(--font-sm); padding: var(--space-2) 0; border-bottom: 1px solid var(--input-border); }
    .flow-from-file-result-row:last-of-type { border-bottom: none; }
    .flow-from-file-result-ok { color: var(--success-text); font-weight: var(--font-weight-bold); }
    .flow-from-file-result .btn-submit { margin-top: var(--space-4); }
    /* ML-204: import warnings on the result card */
    .flow-from-file-warnings { margin-top: var(--space-4); padding: var(--space-3) var(--space-4); border-left: 3px solid var(--warning-text); background: var(--input-bg); border-radius: var(--radius-md); font-size: var(--font-sm); }
    .flow-from-file-warnings ul { margin: var(--space-2) 0 0; padding-left: var(--space-5); }
    .flow-from-file-warnings li { margin-bottom: var(--space-1); line-height: var(--line-height-base); }

    .flow-upload-progress { margin-top: var(--space-3); padding: var(--space-3); background: var(--input-bg); border: 1px solid var(--input-border); border-radius: var(--radius-md); }
    .flow-upload-progress-text { display: flex; justify-content: space-between; font-size: var(--font-sm); font-weight: var(--font-weight-bold); margin-bottom: var(--space-2); }
    .flow-upload-progress-text span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 70%; }
    .flow-upload-progress-track { background: var(--input-border); height: 8px; border-radius: var(--radius-pill); overflow: hidden; }
    .flow-upload-progress-fill { background: var(--primary-action); height: 100%; width: var(--progress, 0%); border-radius: var(--radius-pill); transition: width var(--duration-fast); }

    /* Recording/document rows reuse .history-item's own row shape (icon + text +
       action), just with a type glyph instead of a category colour. Recording rows
       are stacked (top line + inline player below) rather than .history-item's usual
       single row, so the player has full row width to sit in - see .flow-media-item
       below and renderFlowRecordingsList's own comment on why it's an inline player
       now, not an "open in a new tab" button. */
    .flow-media-item { flex-direction: column; align-items: stretch; gap: var(--space-3); margin-bottom: var(--space-3); }
    .flow-doc-item { margin-bottom: var(--space-2); }
    .flow-media-item-top { display: flex; justify-content: space-between; align-items: center; width: 100%; }
    .flow-media-icon, .flow-doc-icon { width: 32px; height: 32px; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-right: var(--space-3); font-size: var(--font-md); }
    .flow-media-icon.type-audio, .flow-media-icon.type-metronome { background: var(--primary-action-tint); color: var(--primary-action-strong); }
    .flow-media-icon.type-youtube { background: var(--danger-tint); color: var(--danger-text); }
    .flow-media-icon.type-metronome svg { width: 18px; height: 18px; }
    .flow-doc-icon { background: var(--info-tint); color: var(--info-text); font-size: var(--font-xs); font-weight: var(--font-weight-bold); }
    .flow-media-item .history-details, .flow-doc-item .history-details { display: flex; align-items: center; }
    /* Scoped to the text div's own children, not a bare "span" - that used to also match
       .flow-media-icon/.flow-doc-icon (also a span, also a direct child of .history-details),
       forcing it to display:block and killing its own centering (the icon glyph collapsed to the
       top-left of its box instead of sitting centered). */
    .flow-media-item .history-details > div strong, .flow-doc-item .history-details > div strong { display: block; font-size: var(--font-sm); }
    .flow-media-item .history-details > div span, .flow-doc-item .history-details > div span { display: block; font-size: var(--font-xs); color: var(--label-color); }
    .flow-doc-item-actions { display: flex; gap: var(--space-2); flex-shrink: 0; }
    /* Same grey-outline "clickable circle" shape as .list-item-menu-btn, tinted red for delete
       rather than .btn-icon-delete's solid red fill - Recordings/Documents rows delete far more
       casually than a whole flow (deleted from My music's list), so it shouldn't read as loudly. */
    .flow-delete-btn { width: 40px; height: 40px; flex-shrink: 0; border-radius: var(--radius-circle); border: 1px solid var(--control-border); background: var(--input-bg); color: var(--danger-text); display: flex; align-items: center; justify-content: center; cursor: pointer; }
    /* Native <audio> controls, full row width - no extra chrome needed for a compact,
       "just play it" control. */
    .flow-media-player-audio { width: 100%; height: 40px; }
    /* YouTube iframe / <video> both need a real box to sit in, unlike <audio> - fixed
       16:9 aspect ratio so the row's height is predictable regardless of source. */
    .flow-media-player-video { position: relative; width: 100%; aspect-ratio: 16 / 9; border-radius: var(--radius-md); overflow: hidden; background: var(--media-bg); }
    .flow-media-player-video iframe, .flow-media-player-video video { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

    /* ML-166: Play Flow's swipeable media carousel (Metronome + one slide per attached audio
       track/video link) - a self-contained set of classes rather than reusing .history-item/
       .flow-media-item, which are shaped for Edit Flow's list-of-rows context, not a single
       full-width carousel slide. Mirrors .flow-edit-tab's active-state language for the dots. */
    .flow-media-dots { display: flex; justify-content: center; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); }
    .flow-media-dot { width: 36px; height: 36px; flex-shrink: 0; border-radius: var(--radius-circle); border: 2px solid transparent; background: var(--input-bg); display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--label-color); padding: 0; }
    .flow-media-dot .material-symbols-outlined { font-size: var(--icon-md); }
    .flow-media-dot svg { width: 18px; height: 18px; }
    .flow-media-dot.active { border-color: var(--primary-action-strong); color: var(--primary-action-strong); background: var(--primary-action-tint); }
    /* Height is set inline by JS (renderFlowMediaCarousel/goToFlowMediaSlide) to the active
       slide's own measured scrollHeight - the transition here is what makes that change smooth
       rather than an instant jump, same "measure, don't hardcode per type" reasoning as the plan. */
    .flow-media-carousel-viewport { overflow: hidden; height: var(--viewport-h); transition: height var(--duration-base) ease; } /* --viewport-h: the showing slide's height (updateFlowMediaViewportHeight) */
    /* touch-action:pan-y (not none) - vertical page scroll still works natively; only horizontal
       drag is captured by JS (wireFlowMediaSwipe), same split Quick Play's own swipe-to-delete
       relies on for its own gesture. */
    /* align-items:flex-start (not the flex default of stretch) - otherwise every slide is
       stretched to match the tallest sibling's height, which is exactly what the JS-measured
       per-slide scrollHeight above is trying to avoid. */
    .flow-media-carousel-track { display: flex; align-items: flex-start; transition: transform var(--duration-base) ease; touch-action: pan-y; }
    .flow-media-slide { flex: 0 0 100%; width: 100%; box-sizing: border-box; }
    .flow-media-slide-header { display: flex; align-items: center; margin-bottom: var(--space-3); }
    .flow-media-slide-header > div { min-width: 0; }
    .flow-media-slide-header > div strong { display: block; font-size: var(--font-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .flow-media-slide-header > div span { display: block; font-size: var(--font-xs); color: var(--label-color); }

    .flow-blocks-card { border: 2px solid var(--primary-action-border-soft); }
    /* .no-margin on this button (avoids unwanted space below it, inside the card, since it's the
       last element) strips its bottom margin but leaves no gap above it either - add that back
       without reintroducing the bottom one. */
    .flow-blocks-card .btn-submit { margin-top: var(--space-4); }

    /* ML-179 Phase 2: Blocks Studio (Full density view). */
    .flow-studio-summary { text-align: center; font-weight: var(--font-weight-bold); color: var(--label-color); margin-bottom: var(--space-4); }

    /* ML-206: Bars tab toolbar - summary on the left, 1-column / 4-column layout switch on the right.
       The switch is the same inset segmented look as .flow-edit-tabs, icons only. */
    .flow-bars-toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-bottom: var(--space-4); }
    .flow-bars-toolbar .flow-studio-summary { text-align: left; margin-bottom: 0; }
    .flow-layout-toggle { display: flex; flex-shrink: 0; gap: var(--space-0-5); background: var(--input-bg); border: 1px solid var(--input-border); border-radius: var(--radius-lg); padding: var(--space-0-5); }
    .flow-layout-btn { width: var(--touch-target); height: var(--touch-target); padding: 0; display: flex; align-items: center; justify-content: center; background: transparent; border: none; border-radius: var(--radius-md); color: var(--label-color); cursor: pointer; }
    .flow-layout-btn[aria-pressed="true"] { background: var(--container-bg); color: var(--text-color); box-shadow: var(--shadow-sm); }
    .flow-layout-icon { width: var(--icon-md); height: var(--icon-md); }

    /* ML-206: 4-column layout - #flowBlocksList takes .metroBlk-tile-strip (Play Flow's grid) and each
       bar is a Play Flow .metroBlk-tile, as a real <button>. touch-action: pan-y (not the tile's
       usual manipulation) so only a vertical swipe scrolls the page - a drag only starts after a press and hold,
       at which point flowBarGridBlockScroll stops the scroll instead. */
    .flow-bar-grid-tile { width: 100%; font: inherit; color: var(--text-color); touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
    .flow-bar-grid-tile-held { position: relative; z-index: var(--z-raised); pointer-events: none; border-color: var(--primary-action-strong); box-shadow: var(--shadow-lg); }
    .flow-bar-grid-tile-shuffling { transition: transform var(--duration-base) ease; }

    /* ML-208: 2-column layout - #flowBlocksList.flow-bar-grid-2, each bar a detail tile that reads like
       the bar itself (flowBarDetailTileHtml). Three bands: a fixed-height header, a middle that never
       stretches, and a strip that is always present and takes any spare height in the row - so across
       a row the headers, the time signatures and the dividing lines all line up whatever each tile holds. */
    .flow-bar-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); padding: var(--space-2) 0 var(--space-4) 0; }
    .flow-bar-detail-tile { display: flex; flex-direction: column; width: 100%; min-width: 0; padding: 0; font: inherit; color: var(--text-color); text-align: left; background: var(--input-bg); border: 2px solid var(--control-border); border-radius: var(--radius-md); cursor: pointer; overflow: hidden; }
    .flow-bar-detail-head { display: flex; align-items: center; gap: var(--space-1); height: calc(var(--icon-lg) + 2 * var(--space-2)); box-sizing: border-box; padding: 0 var(--space-2); border-bottom: 1px solid var(--input-border); min-width: 0; flex-shrink: 0; }
    .flow-bar-detail-head > * { height: var(--icon-lg); display: inline-flex; align-items: center; box-sizing: border-box; }
    .flow-bar-detail-sign { font-size: var(--font-md); line-height: var(--line-height-tight); flex-shrink: 0; }
    /* Same boxed-mark language as Play Flow's .metroBlk-tile-mark-box, sized to the header line; a long
       mark truncates rather than pushing the bar range out. */
    .flow-bar-detail-mark { font-size: var(--font-sm); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); border: 2px solid var(--text-color); border-radius: var(--radius-sm); padding: 0 var(--space-1); flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .flow-bar-detail-name { font-size: var(--font-sm); font-weight: var(--font-weight-bold); white-space: nowrap; flex-shrink: 0; }
    .flow-bar-detail-mid { flex: 0 0 auto; display: flex; align-items: center; gap: var(--space-1); padding: var(--space-3) var(--space-1); }
    .flow-bar-detail-edge { display: flex; align-items: center; flex: 0 0 auto; }
    .flow-bar-detail-barline { height: 2.6em; width: auto; }
    .flow-bar-detail-count { font-size: var(--font-xs); font-weight: var(--font-weight-bold); }
    .flow-bar-detail-meter { flex: 1; display: flex; flex-direction: column; align-items: center; min-width: 0; }
    .flow-bar-detail-time { font-size: var(--font-lg); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); font-variant-numeric: tabular-nums; }
    .flow-bar-detail-bpm { height: var(--icon-md); display: inline-flex; align-items: center; font-size: var(--font-sm); color: var(--label-color); white-space: nowrap; }
    .flow-bar-detail-strip { flex: 1 0 auto; min-height: calc(var(--icon-lg) + 2 * var(--space-2)); box-sizing: border-box; display: flex; flex-wrap: wrap; align-content: flex-start; align-items: center; gap: var(--space-1) var(--space-3); padding: var(--space-2); border-top: 1px solid var(--input-border); font-size: var(--font-sm); font-weight: var(--font-weight-bold); }
    .flow-bar-detail-chip { display: inline-flex; align-items: center; gap: var(--space-0-5); white-space: nowrap; }
    .flow-bar-detail-chip > .flow-sign-svg-inline:not(.flow-sign-svg-coda) { font-size: 1.5em; vertical-align: 0; }
    .flow-bar-detail-times { font-size: var(--font-xs); }
    .flow-bar-detail-ramp { height: 1em; width: auto; }
    /* Needs fixing: same red tile as .flow-tile-warning; the middle keeps a normal middle's height
       (barline + padding) so the row still lines up. */
    .flow-bar-detail-tile-warning { border-color: var(--danger-text); background: var(--danger-tint); }
    .flow-bar-detail-mid-warning { justify-content: center; gap: var(--space-2); color: var(--danger-text); min-height: calc(2.6em + 2 * var(--space-3)); box-sizing: border-box; }
    .flow-bar-detail-warning-icon { width: var(--icon-xl); height: var(--icon-xl); }
    .flow-bar-detail-warning-text { font-size: var(--font-md); font-weight: var(--font-weight-bold); }
    /* ML-209: the same tile on Play Flow - the bar that's sounding gets the gold edge every Play tile
       uses (.metroBlk-tile-active); restated here because the tile's own border rule comes later. */
    .flow-bar-detail-tile.metroBlk-tile-active { border-color: var(--primary-action-strong); box-shadow: var(--focus-ring); }
    /* ML-207: Play Flow's 1-column bar (flowBarFullTileHtml) - read-only, all 12 settings, each roughly
       where it sits on a score: header, "top of the bar" row, the bar itself, "under the bar" row. Both
       outer rows are always there and hold one --icon-lg line, so every bar is the same height and the
       time signatures fall in the same place down the list. Reuses the 2-column tile's mark, name,
       barline, chip and count pieces (flowBarReadingParts). */
    .flow-bar-list-1 { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-2) 0 var(--space-4) 0; }
    .flow-bar-full-tile { display: flex; flex-direction: column; width: 100%; padding: 0; font: inherit; color: var(--text-color); text-align: left; background: var(--input-bg); border: 2px solid var(--control-border); border-radius: var(--radius-md); cursor: pointer; overflow: hidden; }
    .flow-bar-full-tile.metroBlk-tile-active { border-color: var(--primary-action-strong); box-shadow: var(--focus-ring); }
    /* ML-302: the bars Rehearse is repeating - a dashed gold outline, so they read as a range; the
       bar playing now keeps the solid active outline on top. */
    .metroBlk-tile.flow-tile-in-repeat, .flow-bar-detail-tile.flow-tile-in-repeat, .flow-bar-full-tile.flow-tile-in-repeat { border-style: dashed; border-color: var(--primary-action-strong); }
    .flow-tile-in-repeat.metroBlk-tile-active { border-style: solid; }
    .flow-bar-full-head { display: flex; align-items: center; gap: var(--space-1); height: calc(var(--icon-lg) + 2 * var(--space-2)); box-sizing: border-box; padding: 0 var(--space-3); border-bottom: 1px solid var(--input-border); }
    .flow-bar-full-head > * { height: var(--icon-lg); display: inline-flex; align-items: center; box-sizing: border-box; }
    .flow-bar-full-bars { margin-left: auto; font-size: var(--font-sm); color: var(--label-color); }
    .flow-bar-full-row { display: flex; align-items: center; gap: var(--space-4); box-sizing: border-box; min-height: calc(var(--icon-lg) + var(--space-2) + var(--space-1)); padding: var(--space-1) var(--space-3); font-size: var(--font-sm); font-weight: var(--font-weight-bold); }
    .flow-bar-full-row > *, .flow-bar-full-zone > * { height: var(--icon-lg); display: inline-flex; align-items: center; box-sizing: border-box; white-space: nowrap; }
    /* Top of the bar: three zones - sign at the start, pauses over the notes, ending/jump at the end. */
    .flow-bar-full-top { display: grid; grid-template-columns: 1fr auto 1fr; padding-top: var(--space-2); }
    .flow-bar-full-zone { display: flex; align-items: center; gap: var(--space-3); }
    .flow-bar-full-zone-mid { justify-content: center; }
    .flow-bar-full-zone-end { justify-content: flex-end; }
    .flow-bar-full-bottom { padding-bottom: var(--space-2); }
    .flow-bar-full-sign { font-size: var(--font-md); line-height: var(--line-height-tight); }
    .flow-bar-full-mid { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-3); }
    .flow-bar-full-mid .flow-bar-detail-barline { height: 3.2em; }
    .flow-bar-full-edge-end { margin-left: auto; }
    .flow-bar-full-count { font-size: var(--font-sm); font-weight: var(--font-weight-bold); }
    .flow-bar-full-meter { flex: 1; display: flex; align-items: center; justify-content: center; gap: var(--space-5); }
    .flow-bar-full-time { font-size: var(--font-xl); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); font-variant-numeric: tabular-nums; }
    .flow-bar-full-tempo { display: inline-flex; align-items: center; gap: var(--space-1); white-space: nowrap; }
    .flow-bar-full-note { display: inline-flex; }
    .flow-bar-full-note svg { height: var(--icon-lg); width: auto; }
    .flow-bar-full-eq { color: var(--label-color); }
    .flow-bar-full-bpm { font-size: var(--font-lg); font-weight: var(--font-weight-bold); font-variant-numeric: tabular-nums; }
    .flow-bar-full-unit { font-size: var(--font-sm); color: var(--label-color); }

    /* Play Flow's 1 / 2 / 4-column switch: its own row under the title, on the right like the Bars tab's. */
    .flow-play-layout-bar { justify-content: flex-end; margin-bottom: var(--space-2); }

    /* ML-248: bars the save-time review found a problem with - outlined (an outline, not a border, so
       nothing shifts) in whichever Bars layout is showing, until the problem is fixed. */
    .flow-bar-grid-tile.flow-block-has-issue { outline: 3px solid var(--danger-text); outline-offset: 2px; }
    /* The 1-column card sits in a clipping box (swipe-to-delete), so its ring is drawn inside it. */
    .flow-block-box.flow-block-has-issue .flow-block-card { border-color: var(--danger-text); outline: 3px solid var(--danger-text); outline-offset: -3px; }
    /* The review list (#flowCheckModal): one tappable row per problem, grouped "Won't play as written"
       then "Worth checking". Tapping a row goes to that bar. */
    .flow-check-group-title { font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--label-color); margin: var(--space-4) 0 var(--space-2) 0; }
    .flow-check-list { list-style: none; margin: 0 0 var(--space-4) 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
    .flow-check-item { width: 100%; min-height: var(--touch-target); padding: var(--space-3); text-align: left; font: inherit; font-size: var(--font-sm); line-height: var(--line-height-snug); color: var(--text-color); background: var(--input-bg); border: 1px solid var(--control-border); border-left: 4px solid var(--danger-text); border-radius: var(--radius-md); cursor: pointer; }

    /* ML-206: bar popup (#flowBarPopupModal). One header row: ‹, rehearsal mark, name with "n of N"
       under it, ›, then ✕. The title takes the spare width so › stays put whatever the name's length,
       and the ✕ gets extra space so it isn't hit instead of ›. */
    .flow-bar-popup { padding: var(--space-4); }
    .flow-bar-popup-header { display: flex; align-items: center; gap: var(--space-2); padding-bottom: var(--space-3); margin-bottom: var(--space-3); border-bottom: 1px solid var(--input-border); }
    .flow-bar-popup-title { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; }
    .flow-bar-popup-pos { font-size: var(--font-xs); color: var(--label-color); }
    .flow-bar-popup-close { margin-left: var(--space-4); }
    /* Move earlier / Duplicate / Move later: one 4-column tile wide each, centred (.flow-tile-grid-3-centered).
       Deliberately shorter than the setting tiles, text only and on the smaller button radius, so they
       read as actions rather than three more settings. */
    .flow-bar-popup-actions { margin-top: var(--space-5); padding-top: var(--space-4); border-top: 1px solid var(--input-border); }
    .flow-bar-popup-action { min-height: var(--touch-target); padding: var(--space-1) var(--space-2); background: var(--input-bg); border: 1px solid var(--control-border); border-radius: var(--radius-md); color: var(--text-color); font: inherit; font-size: var(--font-sm); font-weight: var(--font-weight-bold); line-height: var(--line-height-snug); cursor: pointer; }
    .flow-bar-popup-delete { margin-top: var(--space-2); }
    /* Disabled (first/last bar): drop the "tappable" cues - no fill, the display-card border, grey
       text - rather than fading with --opacity-disabled, which took the text to 2:1 in light mode.
       Text stays at 4.5:1+ in both themes. */
    .flow-bar-popup-icon-btn:disabled, .flow-bar-popup-action:disabled { background: transparent; border-color: var(--input-border); color: var(--label-color); cursor: not-allowed; }

    /* Reorderable block cards - same rounded-card language as .flow-card, plus a drag handle and
       rehearsal-mark badge in the header. Gesture split now genuinely mirrors Quick Play's own
       qp-block-box/qp-block-surface/qp-bar-grab-handle (wireQpBarGrabHandle/wireQpBarSwipe): a plain
       clipping wrapper (.flow-block-box - delete underlay behind, the card itself in front and
       sliding) with touch-action:pan-y, so native vertical page scroll works from anywhere on a bar
       card - only the grab handle (touch-action:none, isolated to itself) owns vertical drag, and a
       swipe only ever intercepts once it's confirmed horizontal (wireFlowBlockSwipe). */
    .flow-block-box { position: relative; border-radius: var(--radius-xl); margin-bottom: var(--space-3); overflow: hidden; touch-action: pan-y; }
    .flow-block-card { background: var(--container-bg); border: 1px solid var(--input-border); border-radius: var(--radius-xl); padding: var(--space-4); position: relative; transition: border-color var(--duration-fast) ease, border-width var(--duration-fast) ease; }
    .flow-block-card-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-bottom: var(--space-3); }
    .flow-block-card-title { display: flex; align-items: center; gap: var(--space-2); flex: 1; min-width: 0; } /* takes the header's free width so the rehearsal-mark box can grow into it (ML-219) */
    .flow-block-grab-handle { width: 32px; height: 32px; flex-shrink: 0; border: none; background: transparent; padding: 0; display: flex; align-items: center; justify-content: center; color: var(--label-color); cursor: grab; touch-action: none; }
    .flow-block-grab-handle:active { cursor: grabbing; }
    /* Revealed behind .flow-block-card as it slides left - width matches FLOW_BLOCK_SWIPE_OPEN_PX
       in app.js, same shape as Quick Play's own .qp-block-delete-underlay. */
    .flow-block-delete-underlay { position: absolute; top: 0; right: 0; bottom: 0; width: 96px; background: var(--danger-color); color: var(--text-on-accent); border: none; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-0-5); cursor: pointer; font-weight: var(--font-weight-bold); font-size: var(--font-sm); }
    .flow-block-delete-underlay .material-symbols-outlined { font-size: var(--icon-lg); }
    .flow-block-name { font-weight: var(--font-weight-bold); font-size: var(--font-md); white-space: nowrap; flex-shrink: 0; } /* stays on one line beside a wide rehearsal mark (ML-219) */
    /* A <button> now (tappable to edit), not just a display badge - reset the native button
       chrome so it still reads as the same small pill it always was. */
    /* Mark box - same slot whether filled or empty (ML-179 follow-up) so the layout never shifts
       between the two states: a plain "+" in the same neutral-tile language as .flow-tile when
       there's no mark yet, gold border/text (matching every other "this has content" tile below)
       once one's set. Both share a fixed square footprint. */
    /* ML-219: at least 40px square, but grows with its text (a mark can be a few words) - the header's
       "Bars X" title moves across to make room. It is the part of the header that gives way: the "Bars X" name never
       shrinks, so a very long mark truncates with "…" (full text in its accessible name and tooltip). */
    .flow-block-mark-box, .flow-block-mark-empty { font: inherit; font-size: var(--font-md); font-weight: var(--font-weight-bold); border-radius: var(--radius-sm); cursor: pointer; min-width: 40px; height: 40px; flex: 0 1 auto; display: block; padding: 0 var(--space-2); box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; line-height: 36px; /* token-audit-ignore: centres one line in the fixed 40px box */ }
    .flow-block-mark-empty { color: var(--label-color); background: var(--input-bg); border: 1px solid var(--control-border); font-size: var(--font-sm); line-height: 38px; /* token-audit-ignore: centres "+ RM" in the 40px box */ }
    .flow-block-mark-box { color: var(--primary-action-strong); background: transparent; border: 2px solid var(--primary-action-strong); }

    /* Generic tile grid - the Block Inspector's own 4x3 shape, and the lead-in's 3-tile row,
       share this same primitive rather than each having bespoke grid CSS. */
    .flow-tile-grid { display: grid; gap: var(--space-2); }
    .flow-tile-grid-3 { grid-template-columns: repeat(3, 1fr); }
    .flow-tile-grid-4 { grid-template-columns: repeat(4, 1fr); }
    /* Jump sign (#flowSignOptions, 3 tiles) and Score jump (#flowJumpOptions, 6 tiles - wraps to 2
       rows of 3 automatically) pickers - tiles at a 4-column grid's own per-tile width, centered as a
       block (75% = 3/4 columns) rather than stretched to fill the row - same technique as
       .flow-multiselect-grid-3. */
    .flow-tile-grid-3-centered { grid-template-columns: repeat(3, 1fr); width: 75%; margin-left: auto; margin-right: auto; }
    /* Same technique, 2 tiles instead of 3 (e.g. the Tuner's sharps/flats picker, #tunerAccidentalOptions)
       - 50% = 2/4 columns. */
    .flow-tile-grid-2-centered { grid-template-columns: repeat(2, 1fr); width: 50%; margin-left: auto; margin-right: auto; }
    /* "Repeats"/"Intro" and "Tempo"/"Jumps" (flowBlockCardHtml) - each a single 4-tile row shared by
       two section labels instead of two separate grids, so all 4 tiles keep the same standard width
       every other row uses. Mirrors the tile grid's own 4 columns; default split is 2/2 (first label
       over the first pair, second over the last pair) - "Tempo"/"Jumps" uses this as-is. "Repeats"/
       "Intro" needs 3/1 instead (alt ending groups with start/end under "Repeats", not with the bar
       range tile under "Intro") - .flow-tile-section-split-labels-3-1 overrides the span accordingly. */
    .flow-tile-section-split-labels { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2); margin-bottom: var(--space-2); }
    .flow-tile-section-split-labels .flow-tile-section-label { grid-column: span 2; margin-bottom: 0; }
    .flow-tile-section-split-labels-3-1 .flow-tile-section-label:first-child { grid-column: span 3; }
    .flow-tile-section-split-labels-3-1 .flow-tile-section-label:last-child { grid-column: span 1; }
    .flow-tile-section { margin-bottom: var(--space-4); }
    .flow-tile-section:last-of-type { margin-bottom: 0; }
    .flow-tile-section-label { display: block; font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--label-color); margin-bottom: var(--space-2); }
    /* Same "tap opens a picker" tile shape used both inside the Block Inspector Modal and inline
       for the lead-in's own 3 tiles - light hollow card, no heavy border, gold on select-state
       controls elsewhere already covers "this is active", so plain neutral here is enough for
       "tap to change". */
    /* Fixed min-height sized for the tallest tile content today (the 3-line alt-ending value: bracket
       + "from bar N" + caption) - every tile grows to at least that, so a row's short tiles ("None")
       and its tall ones share one height instead of each shrinking to its own content. The value
       area is flex:1 and centers its own content vertically within whatever space that leaves, so
       the caption underneath always sits at the same distance from the tile's bottom edge regardless
       of how tall the value above it is - not both centered as one block (main-axis centering would
       move a short tile's whole label+value group, and only it, toward the middle). */
    .flow-tile { background: var(--input-bg); border: 1px solid var(--control-border); border-radius: var(--radius-lg); padding: var(--space-3) var(--space-2); display: flex; flex-direction: column; align-items: center; gap: var(--space-0-5); cursor: pointer; min-height: 90px; }
    .flow-tile-value { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-0-5); font-size: var(--font-md); font-weight: var(--font-weight-bold); color: var(--text-color); line-height: var(--line-height-tight); }
    .flow-tile-label { flex-shrink: 0; font-size: var(--font-xs); color: var(--label-color); }
    /* metroNoteIconSvg's paths all use fill/stroke="currentColor" - sizing/coloring the icon is
       just sizing/coloring this wrapper. */
    .flow-tile-value svg { height: 1.6em; width: auto; color: inherit; }
    /* End tile's repeat count (flowEndLabel) - same smaller, icon-centered format as the End of bar
       picker's own .flow-picker-tile-count, not the tile-value's own big bold baseline-aligned text. */
    .flow-tile-value .flow-barline-value-row { display: inline-flex; align-items: center; gap: var(--space-1); }
    .flow-tile-value .flow-barline-value-count { font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: inherit; }
    /* Repeat bar tile's empty state (flowRepeatBarLabel) - a plain "+" in the same muted colour as
       the tile's own caption underneath it, not the bold value colour every other tile's "None"
       text uses. */
    .flow-tile-value .flow-tile-value-empty { color: var(--label-color); font-weight: var(--font-weight-normal); }
    .flow-tile-value .flow-warning-icon { width: 1.2em; height: 1.2em; vertical-align: -0.2em; }
    /* Repeat-bar/intro tile's "stale" state (flowRepeatBarInvalid/flowIntroInvalid) - a stored bar
       number left stranded outside the block after it was shortened. Replaces the tile's normal
       value entirely (no point showing a bracket/bar number that's no longer valid) with a big
       triangle + "Update", and reddens the tile itself so it reads as needing attention at a glance. */
    .flow-tile.flow-tile-warning { border-color: var(--danger-text); background: var(--danger-tint); }
    .flow-tile-value-warning { display: flex; flex-direction: column; align-items: center; gap: var(--space-0-5); }
    .flow-tile-value-warning .flow-tile-warning-icon { width: 26px; height: 26px; color: var(--danger-text); }
    .flow-tile-value-warning-text { font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--danger-text); }
    /* Volta/alternate-ending bracket (flowRepeatBarLabel) - real notation, not plain "1-3,5&7-9."
       text: a corner (::before) whose horizontal stroke sits above the figures and whose vertical
       tick runs down immediately to their left, overlapping alongside them rather than floating
       disconnected above - padding-top/left on the wrapper is exactly the tick's own reach, so the
       figures start right where the corner leaves off. "from bar N" sits below as its own smaller
       line, only shown once a start bar is set. */
    .flow-volta-bracket { position: relative; display: inline-block; padding-top: var(--space-0-5); padding-left: var(--space-2); }
    .flow-volta-bracket::before {
        content: '';
        position: absolute;
        top: 3px; left: 3px; right: -4px;
        height: 14px;
        border-top: 2px solid currentColor;
        border-left: 2px solid currentColor;
    }
    .flow-volta-bracket-numbers { display: inline-block; font-size: var(--font-sm); font-weight: var(--font-weight-bold); line-height: var(--line-height-snug); white-space: nowrap; }
    .flow-tile-value .flow-volta-from-bar { display: block; font-size: var(--font-xs); font-weight: var(--font-weight-normal); color: var(--label-color); margin-top: var(--space-0-5); }

    /* Intro tile's cue bracket (#flowIntroModal's main-card display) - a tall tick to the left of the
       "Bar N" text and a line underneath it, with a short answering tick at the line's right end -
       same real-notation language as .flow-volta-bracket, mirrored: the stroke sits below the text
       here instead of above it. */
    .flow-intro-bracket { position: relative; display: inline-block; padding: 0 var(--space-2) var(--space-0-5) var(--space-2); }
    .flow-intro-bracket::before {
        content: '';
        position: absolute;
        top: 3px; left: 3px; right: 3px; bottom: 0;
        border-left: 2px solid currentColor;
        border-bottom: 2px solid currentColor;
    }
    .flow-intro-bracket::after {
        content: '';
        position: absolute;
        top: 3px; right: 3px; bottom: 0;
        border-right: 2px solid currentColor;
    }
    /* No end bar set ("End intro early?" off) - the bottom line just runs off the right edge with no
       closing tick, reading as "continues to the end of the section" rather than a bounded range. */
    .flow-intro-bracket-open-end::after { display: none; }
    .flow-intro-bracket-text { display: inline-block; font-size: var(--font-sm); font-weight: var(--font-weight-bold); line-height: var(--line-height-snug); white-space: nowrap; }

    /* Start-of-bar/End-of-bar picker option cards (flowBarlinePickerTileHtml) - reuses
       .flow-tile-grid/-3/-4 for layout, its own tile class since these carry an icon+count row and
       a caption below, neither of which the card's own .flow-tile needs. Same "icon area is flex:1
       and centers its own content, caption stays pinned at a fixed distance from the bottom" split
       as .flow-tile/.flow-tile-value use (see that rule's own comment) - a fixed min-height plus
       flex:1 on the icon row, not both centered together as one block. */
    .flow-picker-tile { background: var(--input-bg); border: 2px solid var(--control-border); border-radius: var(--radius-lg); padding: var(--space-3) var(--space-2); display: flex; flex-direction: column; align-items: center; gap: var(--space-0-5); cursor: pointer; color: var(--text-color); min-height: 84px; }
    .flow-picker-tile.selected { border-color: var(--primary-action-strong); background: var(--primary-action-tint); color: var(--primary-action-strong); }
    /* Icon and its repeat count (when it has one) sit side by side, not stacked - the icon keeps
       its natural size either way, nothing reserves extra height for a count that isn't there.
       font-size/weight here (rather than on each glyph individually) covers the Score jump picker's
       plain-text icons too ("D.C.", "D.S.", "None") which have no glyph/svg of their own to size
       themselves - .flow-sign-svg-inline's coda glyph still scales off this same 1em, so text and
       glyph read at a matching size within one option ("D.C. al <coda>"). Kept modest (var(--font-md),
       not -lg) so these tiles don't run visibly bigger than the rest of the page. */
    .flow-picker-tile-icon-row { flex: 1; display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: var(--space-1); font-size: var(--font-md); font-weight: var(--font-weight-bold); }
    .flow-picker-tile-count { font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: inherit; }
    .flow-picker-tile .metroBlk-note-svg { width: 22px; height: 38px; margin: var(--space-0-5) 0; }
    /* ML-298 / ML-295 / ML-296: the drill tools - see specs/components/drills.md. The pad is the one big
       target a drill is played on: gold like Play, round, and it gives under the finger. */
    .drill-pad { display: flex; align-items: center; justify-content: center; width: calc(var(--touch-target) * 4); aspect-ratio: 1 / 1; margin: var(--space-4) auto; border: none; border-radius: var(--radius-circle); background: var(--primary-action); color: var(--primary-action-text); font: inherit; font-size: var(--font-lg); font-weight: var(--font-weight-bold); cursor: pointer; user-select: none; -webkit-user-select: none; touch-action: manipulation; transition: transform var(--duration-instant) var(--ease-standard); }
    .drill-pad.is-hit { transform: scale(0.94); }
    .drill-pad:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
    .drill-feedback { min-height: calc(var(--font-base) * 1.5); text-align: center; color: var(--text-color); font-weight: var(--font-weight-bold); margin: var(--space-2) 0; }
    /* Tap tempo's live meter: the needle sits at --pos (-1 much too slow ... +1 much too fast). */
    .drill-meter { margin: var(--space-3) auto 0; max-width: calc(var(--touch-target) * 7); }
    .drill-meter-track { position: relative; height: var(--space-2); border-radius: var(--radius-pill); background: var(--control-off-bg); }
    .drill-meter-mark { position: absolute; left: 50%; top: calc(var(--space-2) * -0.5); bottom: calc(var(--space-2) * -0.5); width: var(--space-0-5); background: var(--label-color); }
    .drill-meter-needle { position: absolute; top: calc(var(--space-3) * -0.5); left: calc(50% + var(--pos, 0) * 50%); width: var(--space-3); height: calc(var(--space-3) * 2); margin-left: calc(var(--space-3) * -0.5); border-radius: var(--radius-pill); background: var(--primary-action-strong); transition: left var(--duration-fast) var(--ease-standard); }
    .drill-meter-labels { display: flex; justify-content: space-between; margin-top: var(--space-2); font-size: var(--font-xs); color: var(--label-color); }
    /* Gap trainer: the 8 bars, 4 beats each; a silent beat is an empty ring, the beat now is gold. */
    .drill-beats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-2); margin: var(--space-3) 0; }
    .drill-bar { display: flex; justify-content: space-around; padding: var(--space-2) var(--space-1); border-radius: var(--radius-md); background: var(--container-bg); border: 1px solid var(--input-border); }
    .drill-beat { width: var(--space-3); height: var(--space-3); border-radius: var(--radius-circle); background: var(--label-color); border: 2px solid var(--label-color); box-sizing: border-box; }
    .drill-beat.is-silent { background: transparent; }
    .drill-beat.is-now { background: var(--primary-action-strong); border-color: var(--primary-action-strong); }
    .drill-ear-icon { font-size: calc(var(--icon-xl) * 2); color: var(--label-color); }
    .drill-listen { text-align: center; color: var(--label-color); font-size: var(--font-md); }
    .drill-result-list { list-style: none; margin: var(--space-4) 0; padding: 0; }
    .drill-result-list li { display: flex; justify-content: space-between; gap: var(--space-2); padding: var(--space-2) 0; border-bottom: 1px solid var(--input-border); font-size: var(--font-sm); }
    .drill-result-list li:last-child { border-bottom: none; }
    /* ML-9: Scales practice - see specs/components/scales.md. */
    /* Previous / Next / My ... : the same icon-over-label buttons as the transport row below
       (.metro-play-grid-btn, grey), so the two rows match: previous, next, shuffle, my ... */
    .scales-pick-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2); margin: var(--space-3) 0; }
    .scales-staff-card { padding: var(--space-3) var(--space-2); }
    /* ML-357: the setting row along the top (scales / clef / direction / detail) - the transport's value
       boxes with a one-line value, so "Treble" fits a quarter of a phone */
    .scales-setting-btn { padding: var(--space-2) var(--space-1); }
    /* ML-361: fewer than four (Warm-ups: warm-ups and clef) - each still a quarter wide, centred */
    .scales-setting-row.is-centred { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: calc((100% - 3 * var(--space-2)) / 4); justify-content: center; }
    .scales-setting-btn strong { font-size: var(--font-base); line-height: var(--line-height-snug); text-align: center; }
    /* The Scales and Warm-ups pickers (clef, direction, detail): the value with its caption right under it,
       centred up and down in the tile like every other picker tile (owner, 2026-09-30 - it used to sit at
       the top, leaving a gap below). Each tile is a subgrid over two shared rows (value, caption - as
       Rehearse's tiles), so the values still line up across a row when one caption wraps. A subgrid tile
       doesn't keep the picker tile's own min-height, so the two rows hold it instead: a 32px value row and
       a 24px caption row, + 12px padding and 2px border top and bottom = the same 84px as every picker tile. */
    .scales-choice-grid .flow-picker-tile { display: grid; grid-row: span 2; grid-template-rows: subgrid; row-gap: 0; align-content: center; justify-items: center; }
    .scales-choice-grid .flow-picker-tile-icon-row { align-self: end; align-items: flex-end; min-height: var(--space-7); }
    .scales-choice-grid .flow-picker-tile-label { align-self: start; min-height: var(--space-6); }
    /* Direction: "down & up" is the up & down arrows flipped, so the first arrow is the first way you go */
    .scales-dir-flip { transform: scaleX(-1); }
    /* Previous / Next / Shuffle / Select sit under the stave, inside the scale's box */
    .scales-staff-card .scales-pick-row { margin: var(--space-3) 0 0 0; }
    /* Use metronome: its own box, the title and the on/off switch on one line, the transport under it */
    .scales-metronome-card { margin-top: var(--space-3); }
    .scales-metronome-card .scales-staff-head { padding-bottom: 0; }
    .scales-staff-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding: 0 var(--space-2) var(--space-2); }
    .scales-staff-heading { display: flex; flex-direction: column; gap: var(--space-0-5); min-width: 0; }
    /* Play from memory (ML-9): the scale's name in place of the notes. */
    .scales-memory { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); padding: var(--space-7) var(--space-3); text-align: center; }
    .scales-memory-name { font-size: var(--font-2xl); font-weight: var(--font-weight-bold); color: var(--text-color); }
    .scales-memory-detail { font-size: var(--font-md); color: var(--label-color); }
    .scales-memory-progress { min-height: 1.5em; font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--primary-action-strong); }
    .scales-staff-title { margin: 0; font-size: var(--font-md); font-weight: var(--font-weight-bold); }
    .scales-staff-sub { font-size: var(--font-sm); color: var(--label-color); }
    .scales-staff svg { display: block; width: 100%; height: auto; }
    .scales-staff-row-short { width: calc(var(--row-frac, 1) * 100%); } /* the last, shorter row - same scale as the full rows */
    /* The note to play, while the metronome runs (Notation note cls). */
    .scales-staff .is-now { fill: var(--primary-action-strong); }
    /* ML-305 / ML-322: Range. The run to your comfortable note with the note to hold in gold (Notation
       note cls is-now, as Scales' playing note); the range picker's two edges, each a stave you tap near
       a note (a button - its height is the note's height on the stave) above a -/+ row. */
    .range-staff { margin: var(--space-3) 0; }
    .range-staff svg { display: block; width: 100%; height: auto; }
    .range-staff .is-now { fill: var(--primary-action-strong); }
    .range-edge { margin-bottom: var(--space-4); }
    .range-stave-btn { display: block; width: 100%; min-height: calc(var(--touch-target) * 3); margin: var(--space-1) 0 var(--space-2); padding: var(--space-2); box-sizing: border-box; background: var(--input-bg); border: 2px solid var(--control-border); border-radius: var(--radius-md); color: var(--text-color); cursor: pointer; }
    .range-stave-btn svg { display: block; margin: 0 auto; max-width: 100%; height: auto; pointer-events: none; }
    /* The key-note picker: the keyboard layout - 7 columns, sharps above, flats below (placement shared with .theory-answers-keyboard). */
    .note-keyboard { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--space-1); }
    .note-keyboard .flow-picker-tile { min-height: var(--touch-target); padding: var(--space-2) 0; }
    /* ML-294: Warm-ups shares the Scales layout classes; its own bit is the exercise's tip under the title. */
    .warmups-tip { margin: 0 var(--space-2) var(--space-2); font-size: var(--font-sm); color: var(--label-color); }
    /* ML-357 Choose a scale: your list row by row, each key a 44px+ button, five across */
    .scales-choose-group { margin-top: var(--space-4); }
    .scales-choose-label { margin: 0 0 var(--space-2) 0; font-size: var(--font-sm); font-weight: var(--font-weight-bold); }
    .scales-choose-label .scale-grid-sub { font-weight: var(--font-weight-normal); }
    .scales-choose-keys { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--space-2); }
    .scales-choose-keys .flow-picker-tile { min-height: var(--touch-target); padding: var(--space-2) 0; }
    /* ML-361 Choose a warm-up: a row per warm-up, the title in the body weight; a locked one muted, with a lock */
    .flow-choice-option.warmups-choose-row { font-weight: var(--font-weight-normal); }
    .flow-choice-option.warmups-choose-row.is-locked { opacity: var(--opacity-muted); cursor: default; }
    .flow-choice-option.warmups-choose-row.is-locked .material-symbols-outlined { color: var(--label-color); }
    /* The Scales pop-up: the count and See your range (a text link) on one line */
    .scales-pool-line { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
    .scales-pool-line .scales-pool-count { margin: 0; }
    /* ML-375: a link that leaves the app (a band's website) - the address in the link colour, underlined,
       with ↗ after it; opens in a new tab. Tall enough to tap (--touch-target) even inside a list row. */
    .external-link { display: inline-flex; align-items: center; gap: var(--space-1); min-height: var(--touch-target); color: var(--link-color); font-size: var(--font-sm); text-decoration: none; overflow-wrap: anywhere; }
    .external-link-text { text-decoration: underline; } /* the address only - not under the ↗ */
    .external-link-icon { font-size: var(--icon-sm); text-decoration: none; }
    .band-row-text { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; }
    .scales-range-link { flex: none; min-height: var(--touch-target); padding: var(--space-2) 0; background: none; border: none; color: var(--link-color); font: inherit; font-weight: var(--font-weight-bold); text-decoration: underline; cursor: pointer; }
    .scales-pool-count { margin: var(--space-2) 0 0 0; font-size: var(--font-sm); color: var(--label-color); }
    /* ML-357: the grade grid in My scales (specs/components/scales.md) - a label column, then the 15 keys
       in chromatic order. Every state has a shape or mark as well as a colour: filled (ready), "8"
       (another octave), a lock (outside your range), a cross (beyond the instrument), dashed (not needed). */
    .scale-grid { margin-top: var(--space-4); }
    .scale-grid-head, .scale-grid-row { display: grid; grid-template-columns: var(--scale-grid-label-width) repeat(15, minmax(0, 1fr)); gap: var(--space-0-5); align-items: center; }
    .scale-grid-head { font-size: var(--font-2xs); color: var(--label-color); text-align: center; margin-bottom: var(--space-1); }
    /* The key's sharp or flat sits under its letter - fifteen columns are too narrow for C♯ D♭ side by side */
    .scale-grid-acc { display: block; line-height: var(--line-height-tight); }
    .scale-grid-head .scale-grid-label { font-size: var(--font-sm); color: var(--text-color); font-weight: var(--font-weight-bold); text-align: left; }
    .scale-grid-row { margin-top: var(--space-0-5); }
    .scale-grid-label { font-size: var(--font-xs); line-height: var(--line-height-snug); }
    .scale-grid-sub { display: block; color: var(--label-color); }
    .scale-grid-cell { display: flex; align-items: center; justify-content: center; height: var(--space-4); border: 1px dashed var(--control-border); border-radius: var(--radius-xs); font-size: var(--font-2xs); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); box-sizing: border-box; }
    .scale-grid-cell .material-symbols-outlined { font-size: var(--font-2xs); }
    .scale-grid-cell.is-ready { background: var(--success-text); border: 1px solid var(--success-text); }
    /* ML-391: a scale in your list holding its Level (.lv-1 to .lv-5 give the colour; a learnt one holds a star) */
    .scale-grid-cell.is-level { border: 1px solid transparent; font-weight: var(--font-weight-bold); }
    .scale-grid-cell.is-other { background: var(--status-green-bg); border: 1px solid var(--status-green-fg); color: var(--status-green-fg); }
    .scale-grid-cell.is-locked { background: var(--status-amber-bg); border: 1px solid var(--status-amber-fg); color: var(--status-amber-fg); }
    .scale-grid-cell.is-beyond { background: var(--control-off-bg); border: 1px solid var(--control-off-bg); color: var(--container-bg); }
    .scale-grid-legend { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2) var(--space-3); margin-top: var(--space-4); font-size: var(--font-sm); color: var(--label-color); }
    .scale-grid-legend > span { display: flex; align-items: center; gap: var(--space-2); }
    .scale-grid-legend .scale-grid-cell { width: var(--space-4); flex: none; }
    /* My scales' option rows (clef, direction, grades): four to a row, however many there are - a compact
       radio group's labels are content-box, so four would wrap. A last one on its own (Everything else) fills its row. */
    .scales-grade-options label { flex: 1 1 calc(25% - var(--space-2)); min-width: 0; box-sizing: border-box; }
    .scales-range-staff { display: flex; justify-content: center; margin: var(--space-3) 0; }
    /* ML-370: See your range - one bar, the instrument's range (Potential range) with yours inside it, the
       key under it. Positions are run-time percentages of the bar (--rb-from / --rb-to / --rb-at). On brass
       and woodwind the part past the usual top (a 4th, where an experienced player goes) is open-ended:
       a dashed edge with no end cap, and the usual top is a tick. Colours are the selected tile's (gold
       fill, strong-gold edge) on a tappable surface's (input bg, control border) - both 3:1 edges. */
    .range-bar { position: relative; height: var(--space-5); margin: var(--space-4) 0 var(--space-1); }
    .range-bar-part { position: absolute; top: 0; bottom: 0; left: var(--rb-from); right: calc(100% - var(--rb-to)); box-sizing: border-box; }
    .range-bar-potential { background: var(--input-bg); border: 2px solid var(--control-border); border-radius: var(--radius-pill); }
    .range-bar-potential.is-open { border-right: none; border-radius: var(--radius-pill) 0 0 var(--radius-pill); }
    .range-bar-stretch { background: var(--input-bg); border: 2px dashed var(--control-border); border-left: none; border-right: none; }
    .range-bar-yours { background: var(--primary-action); border: 2px solid var(--primary-action-strong); border-radius: var(--radius-pill); }
    /* ML-384: the slim bar - on its own under a list row's text (My instruments), no names or key */
    .range-bar.is-slim { height: var(--space-2); margin: var(--space-2) 0 0; }
    .range-bar.is-slim .range-bar-potential, .range-bar.is-slim .range-bar-yours { border-width: 1px; }
    .range-bar.is-slim .range-bar-stretch { border-width: 1px; }
    .range-bar.is-slim .range-bar-tick { top: calc(-1 * var(--space-0-5)); bottom: calc(-1 * var(--space-0-5)); }
    .range-bar-tick { position: absolute; top: calc(-1 * var(--space-1)); bottom: calc(-1 * var(--space-1)); left: var(--rb-at); width: 2px; background: var(--text-color); }
    .range-bar-ends { position: relative; height: 1.5em; font-size: var(--font-xs); color: var(--label-color); }
    .range-bar-end { position: absolute; top: 0; white-space: nowrap; }
    .range-bar-end.is-start { left: 0; }
    /* The usual top is always near the right (the stretch is only a 4th), so its label ends at the tick */
    .range-bar-end.is-at { left: var(--rb-at); transform: translateX(-100%); }
    .range-bar-end.is-end { right: 0; }
    .range-bar-key { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4); list-style: none; padding: 0; margin: var(--space-2) 0 var(--space-3); font-size: var(--font-sm); }
    .range-bar-key li { display: flex; align-items: center; gap: var(--space-2); }
    .range-bar-swatch { width: var(--space-4); height: var(--space-3); box-sizing: border-box; border-radius: var(--radius-pill); }
    .range-bar-swatch.is-yours { background: var(--primary-action); border: 2px solid var(--primary-action-strong); }
    .range-bar-swatch.is-potential { background: var(--input-bg); border: 2px solid var(--control-border); }

    .flow-picker-tile-label { display: block; font-size: var(--font-xs); color: var(--label-color); min-height: 1em; }
    .flow-picker-tile.selected .flow-picker-tile-label { color: var(--primary-action-strong); }

    /* Reused inside #flowChoiceModal for every discrete-option tile. */
    .flow-choice-option { display: flex; align-items: center; justify-content: space-between; padding: var(--space-3) var(--space-4); border: 2px solid var(--control-border); border-radius: var(--radius-lg); margin-bottom: var(--space-2); cursor: pointer; font-weight: var(--font-weight-bold); }
    .flow-choice-option.selected { border-color: var(--primary-action-strong); background: var(--primary-action-tint); color: var(--primary-action-strong); }
    .flow-choice-option .material-symbols-outlined { color: var(--primary-action-strong); }
    /* ML-354: the generic choice pop-up's options are real <button>s (keyboard and screen reader), made to
       look exactly like the old <div>s. :where keeps this at zero specificity, so .level-answer and
       .selected still win wherever they're used. */
    :where(button.flow-choice-option) { width: 100%; box-sizing: border-box; background: none; color: inherit; font: inherit; font-weight: var(--font-weight-bold); text-align: left; }
    /* ML-351/353: pick list - a multi-select row with a tick box on the left (the whole row toggles
       it), and the Select all / Unselect all bar above the list. See specs/components/pick-list.md. */
    .pick-row { justify-content: flex-start; gap: var(--space-3); }
    .flow-choice-option .pick-row-check { flex-shrink: 0; color: var(--label-color); }
    .flow-choice-option.selected .pick-row-check { color: var(--primary-action-strong); font-variation-settings: 'FILL' 1; }
    .pick-bar { display: flex; justify-content: flex-end; gap: var(--space-4); margin-bottom: var(--space-2); }
    .pick-bar .btn-text { width: auto; padding: var(--space-3) var(--space-1); }
    /* Segno/coda glyphs (flowSignIconSvg) - the real Unicode Musical Symbols characters, set in Noto
       Music (loaded in index.html) rather than drawn by hand; plain text, so it inherits currentColor
       for free (no separate selected-state handling needed, unlike a stroked SVG). */
    .flow-sign-svg { font-family: var(--font-music); font-size: var(--font-2xl); line-height: var(--line-height-tight); display: inline-block; }
    /* Sized to sit inline with surrounding text (jump tile labels like "D.<segno>") - matches the
       text's own line-height rather than a fixed pixel size. */
    .flow-sign-svg-inline { font-family: var(--font-music); font-size: 1em; line-height: var(--line-height-tight); vertical-align: -0.15em; }
    /* Coda's own ink sits smaller within its character box than segno's, even at the same font-size -
       bumped up to 1.6x so the two read as the same visual size next to each other, wherever coda
       shows up (main tile, Jump sign picker, inline "al <coda>" labels). */
    .flow-sign-svg.flow-sign-svg-coda { font-size: var(--font-3xl); }
    /* vertical-align is relative to the ELEMENT's own font-size, so the base -0.15em (tuned for
       plain-size segno) pulls coda down 1.6x further once its font-size is bumped to 1.6em - net
       effect was coda sitting visibly low against the surrounding text's baseline. Own value here
       instead of inheriting the segno one, to land coda's own bottom edge level with the text. */
    .flow-sign-svg-inline.flow-sign-svg-coda { font-size: 1.6em; vertical-align: 0.1em; }

    /* Inline extra control shown below the choice list itself (ML-179 follow-up) - e.g. the
       repeat-count slider once "Repeat end" is picked - rather than stacking a second popup on
       top of the first. */
    .flow-choice-inline-extra { margin-top: var(--space-2); padding-top: var(--space-3); border-top: 1px solid var(--input-border); }

    /* Repeat-ending multi-select (1-10) - each number toggles independently, gold when selected.
       -3 is the Repeat bar/volta modal's own 3x3 layout (1-9) - 3 columns sized to match every
       other picker's standard 4-column width (75% of the row, so each column is still a quarter of
       the full row) rather than 3 columns stretched to fill it, centered since there's no sibling
       grid here to align widths against (unlike End-of-bar's Structure/Repeats pair). */
    .flow-multiselect-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--space-2); margin: var(--space-3) 0; }
    .flow-multiselect-grid-3 { grid-template-columns: repeat(3, 1fr); width: 75%; margin-left: auto; margin-right: auto; }
    .flow-multiselect-num { display: flex; align-items: center; justify-content: center; height: 44px; border-radius: var(--radius-md); border: 2px solid var(--input-border); font-weight: var(--font-weight-bold); cursor: pointer; background: var(--input-bg); color: var(--text-color); }
    .flow-multiselect-num.selected { border-color: var(--primary-action-strong); background: var(--primary-action-tint); color: var(--primary-action-strong); }

    /* Repeat bar / volta modal (#flowRepeatBarModal) - small pieces not shared with anything else:
       the "Bar N" context pill next to the title, and the toggle row's label+helptext-left,
       switch-right layout. */
    .flow-modal-bar-pill { display: inline-block; font-size: var(--font-sm); font-weight: var(--font-weight-normal); color: var(--label-color); background: transparent; border: 1px solid var(--input-border); border-radius: var(--radius-md); padding: var(--space-0-5) var(--space-2); vertical-align: middle; margin-left: var(--space-2); }
    .flow-volta-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); background: var(--container-bg); border: 1px solid var(--input-border); border-radius: var(--radius-lg); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4); }
    .flow-volta-toggle-text { display: flex; flex-direction: column; gap: var(--space-0-5); }
    .flow-volta-toggle-title { font-weight: var(--font-weight-bold); }
    .flow-volta-toggle-help { font-size: var(--font-xs); color: var(--label-color); }
    /* Intro modal's "End intro early?" row - grayed out and non-interactive until "Start intro?" is
       on, since an early end has no meaning without a start (#flowIntroModal). */
    .flow-volta-toggle-row.flow-toggle-row-disabled { opacity: 0.5; }
    .toggle-switch input:disabled + .toggle-slider { cursor: not-allowed; }
    /* Intro modal's own stepper sections sit tighter to their neighbours than .flow-tile-section's
       default margin-bottom allows for - a bit more room below the Start bar stepper (before the
       End intro toggle row) and below the End bar stepper/toggle row (before Save/Cancel). */
    #flowIntroStartFields .flow-tile-section, #flowIntroEndFields .flow-tile-section { margin-bottom: var(--space-6); }
    #flowIntroEndToggleRow { margin-bottom: var(--space-5); }
    /* Shared stepper-readout style for #flowRepeatBarStartValue/#flowIntroStartValue/
       #flowIntroEndValue - flags a value left stranded above the block's own bar count after the
       block was shortened (these bar offsets aren't DB-bounded against bar count, see
       db/migrations/037_volta_start_bar.sql), rather than silently clamping it. */
    .flow-bar-readout { display: inline-flex; align-items: center; gap: var(--space-1); }
    .flow-bar-readout.flow-bar-readout-invalid { color: var(--danger-text); }
    .flow-warning-icon { width: 18px; height: 18px; flex-shrink: 0; color: var(--danger-text); }

    /* Opened while #flowFermataModal is on screen (unlike every other .metroBlk-tile-menu, which
       only ever opens on a plain page, never over a modal) - same z-index (100) as .modal, and
       earlier in the DOM, meant it painted BEHIND the modal's own full-viewport backdrop, so a tap
       on Edit/Delete was actually landing on the modal instead. Needs to sit above it explicitly. */
    #flowFermataRowMenu { z-index: var(--z-modal-stacked); }
    /* "Current pauses" list header - only shown once there's at least one (renderFlowFermataList),
       with a small count badge next to the label rather than baking the number into the text itself. */
    .flow-pause-list-header { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
    .flow-pause-list-header .flow-tile-section-label { margin-bottom: 0; }
    /* --radius-pill (a fixed px value), not --radius-circle (50% - only correct for a square box,
       and this badge's width grows with the digit count) - a badge/chip per this file's own radius
       convention (see :root's own comment on --radius-pill/--radius-circle), same class the ramp
       count badge (#flowRampCountBadge) reuses too. */
    .flow-pause-count-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 var(--space-2); border-radius: var(--radius-pill); background: var(--input-bg); border: 1px solid var(--input-border); font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--label-color); }
    /* Breathing room before the "+ Add another pause" button/form below the list. */
    #flowFermataList { margin-bottom: var(--space-4); }
    /* Same clipping-wrapper-with-delete-underlay-behind shape as Flow's own block cards/Quick Play's
       bar boxes (wireFlowBlockSwipe/wireQpBarSwipe) - touch-action:pan-y so page scroll still works
       from anywhere on a pause row, only a confirmed horizontal swipe (wireFlowPauseSwipe) reveals
       the delete underlay behind it. */
    .flow-fermata-box { position: relative; border-radius: var(--radius-md); margin-bottom: var(--space-2); overflow: hidden; touch-action: pan-y; }
    .flow-fermata-delete-underlay { position: absolute; top: 0; right: 0; bottom: 0; width: 96px; background: var(--danger-color); color: var(--text-on-accent); border: none; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-0-5); cursor: pointer; font-weight: var(--font-weight-bold); font-size: var(--font-sm); }
    .flow-fermata-delete-underlay .material-symbols-outlined { font-size: var(--icon-lg); }
    /* Same 1px surround as every other "boxed" element in this app (.flow-block-card/.flow-tile) -
       a plain background fill on its own doesn't read as a bounded box against the modal's own
       (also var(--input-bg)-free) background. This is the sliding surface now, not the outer element -
       margin-bottom lives on .flow-fermata-box instead. */
    .flow-fermata-row { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); padding: var(--space-3); background: var(--input-bg); border: 1px solid var(--control-border); border-radius: var(--radius-md); position: relative; }
    .flow-fermata-row-text { display: flex; align-items: center; gap: var(--space-2); }
    .flow-fermata-row-tag { font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--label-color); }
    /* Stale entry - the bar it's on no longer exists after the block was shortened (same "flag,
       don't clamp/hide" treatment as .flow-bar-readout-invalid). */
    .flow-fermata-row-text-invalid, .flow-fermata-row-text-invalid .flow-fermata-row-tag { color: var(--danger-text); }
    #flowFermataAddBox .btn-submit, #flowFermataAddBox .btn-cancel { margin-top: var(--space-2); }
    /* Add/edit pause form - each field is its OWN bordered card (label left, control right), not one
       big box wrapping everything - matches the design's individually-boxed fields, each with a bold
       title + a smaller grey subtitle explaining what it's for. */
    .flow-pause-field-card { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); background: var(--container-bg); border: 1px solid var(--input-border); border-radius: var(--radius-lg); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-3); }
    .flow-pause-field-label { display: flex; flex-direction: column; gap: var(--space-0-5); }
    .flow-pause-field-title { font-weight: var(--font-weight-bold); font-size: var(--font-sm); }
    .flow-pause-field-subtitle { font-size: var(--font-xs); color: var(--label-color); }
    /* Bar in block's own warning icon slot (flowFermataBarWarning) - sits to the left of the stepper's
       own minus button, not inside the readout between minus/plus like every other stale-value
       warning in this app, so it reads as "this whole row needs fixing" rather than being buried
       between the two step buttons. Empty (no width) when not invalid. */
    .flow-pause-field-control { display: flex; align-items: center; gap: var(--space-2); }
    .flow-pause-field-warning-slot .flow-warning-icon { display: block; }
    /* Fixed left/centre/right columns (scaled down to fit alongside a label in the same card)
       instead of .metro-speed-row's default centered-as-a-group layout, whose minus/plus buttons
       would otherwise drift depending on the readout text's own width. A fixed overall width (not
       shrink-to-fit) with the centre column as 1fr, rather than auto, is what actually centres the
       readout between the two buttons - auto-sizing the centre column to its own content left the
       whole cluster hugging the right edge (where + anchors via the card's own space-between), so -
       sat close to the number instead of symmetric either side of it. */
    .flow-pause-field-card .metro-speed-row { display: grid; grid-template-columns: 32px 1fr 32px; align-items: center; gap: var(--space-2); margin: 0; width: 220px; }
    .flow-pause-field-card .metro-bpm-step { width: 32px; height: 32px; font-size: var(--font-md); }
    .flow-pause-field-card .metro-speed-readout { min-width: 0; }
    /* Pause type toggle - two cards (glyph + the plain-English effect, bold, + the musical term,
       smaller, underneath) rather than plain text alone - most people recognise the symbol/effect
       before the term. Fixed width so it sits compactly on the field card's right side rather than
       stretching to fill the remaining space. */
    .flow-pause-kind-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-2); width: 190px; flex-shrink: 0; }
    .flow-pause-kind-btn { display: flex; flex-direction: column; align-items: center; gap: var(--space-0-5); padding: var(--space-2) var(--space-1); border-radius: var(--radius-md); border: 2px solid var(--control-border); background: var(--input-bg); cursor: pointer; color: var(--text-color); }
    .flow-pause-kind-btn.selected { border-color: var(--primary-action-strong); background: var(--primary-action-tint); color: var(--primary-action-strong); }
    .flow-pause-kind-btn .flow-sign-svg { font-size: var(--font-lg); }
    .flow-pause-kind-title { font-weight: var(--font-weight-bold); font-size: var(--font-xs); }
    .flow-pause-kind-sub { font-size: var(--font-xs); color: var(--label-color); }
    .flow-pause-kind-btn.selected .flow-pause-kind-sub { color: var(--primary-action-strong); }
    /* A disabled target-speed card (Next block, once a ramp ends at a specific mid-block beat
       rather than the block boundary - see flowRampTargetGrid) - same muted treatment wherever else
       a kind-grid-style card needs to say "not selectable right now" rather than being hidden. */
    .flow-pause-kind-btn:disabled { opacity: 0.45; cursor: not-allowed; }

    /* Tempo ramps mini list (#flowRampModal) - same shape as the Pauses/Fermata modal directly
       above throughout this section; only the row text/swipe machinery gets its own copy of
       classes (own JS state too - flowOpenSwipeRampIndex etc.) rather than sharing .flow-fermata-*,
       same "own copy per feature" precedent as wireFlowPauseSwipe itself not sharing with
       wireFlowBlockSwipe/wireQpBarSwipe. */
    #flowRampRowMenu { z-index: var(--z-modal-stacked); }
    /* The Tempo (bpm) popup (#flowBpmModal) can now be opened FROM this modal too (the "Set custom
       tempo" row, flowRampCustomTempoBtn) - same "needs to paint above the modal already on screen"
       z-index bump as the row menus above, harmless when it's opened the normal way (directly over
       the page, nothing else below it to out-rank). */
    #flowBpmModal { z-index: var(--z-modal-stacked); }
    #flowRampList { margin-bottom: var(--space-4); }
    .flow-ramp-box { position: relative; border-radius: var(--radius-md); margin-bottom: var(--space-2); overflow: hidden; touch-action: pan-y; }
    .flow-ramp-delete-underlay { position: absolute; top: 0; right: 0; bottom: 0; width: 96px; background: var(--danger-color); color: var(--text-on-accent); border: none; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-0-5); cursor: pointer; font-weight: var(--font-weight-bold); font-size: var(--font-sm); }
    .flow-ramp-delete-underlay .material-symbols-outlined { font-size: var(--icon-lg); }
    /* All four swipe-to-delete underlays (Quick play bars, Flow blocks, fermatas, ramps) sit behind a
       card with the same rounded corners - at rest the red showed through the card's anti-aliased
       corner as a pink fringe. Hidden until the card is actually moved - the sliding card has .is-moved while swiped (swipeTo/
       setMove), and on close the hide waits out the 0.2s snap-back so the red doesn't vanish
       mid-slide. (It used to look for an inline translateX, which ML-288 removed, leaving the delete
       hidden behind every swiped card.) Only a swipe moves the card itself: a vertical drag or FLIP
       reorder moves the outer box, so this never shows the red during a reorder. */
    :is(.qp-block-delete-underlay, .flow-block-delete-underlay, .flow-fermata-delete-underlay, .flow-ramp-delete-underlay) { visibility: hidden; transition: visibility 0s linear var(--duration-base); }
    :has(> .is-moved) > :is(.qp-block-delete-underlay, .flow-block-delete-underlay, .flow-fermata-delete-underlay, .flow-ramp-delete-underlay) { visibility: visible; transition-delay: 0s; }
    .flow-ramp-row { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); padding: var(--space-3); background: var(--input-bg); border: 1px solid var(--control-border); border-radius: var(--radius-md); position: relative; }
    /* Two-line row text (bold "Bar X, beat Y -> ..." title + a lighter "Tempo change - To N bpm"
       subtitle underneath) - unlike the Pauses list's single-line row, since a ramp's start/end/
       target is more to read than one line comfortably holds. */
    .flow-ramp-row-text { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
    /* Same tinted rounded-square badge as the Media tab's own row icons (.flow-media-icon) - a bare
       <svg> has no intrinsic size without this, which is what made the plain arrow glyph render at
       its raw default size (see flowRampIconSvg/flowWarningIconSvg, both stroke="currentColor" so
       the badge's own color drives them). */
    .flow-ramp-row-icon { width: 32px; height: 32px; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: var(--primary-action-tint); color: var(--primary-action-strong); }
    .flow-ramp-row-icon svg { width: 18px; height: 18px; }
    .flow-ramp-row-icon.invalid { background: var(--danger-tint); color: var(--danger-text); }
    /* Overrides .flow-tile-value svg's default 1.6em (tuned for the barline icons' own tall/narrow
       viewBox) - this icon's square viewBox reads too bulky at that height next to the pause tile's
       own slighter font-glyph icon. */
    .flow-tile-value .flow-ramp-tile-icon { height: 0.85em; }
    /* Up to three icon+count groups (accel/rit/flat) can now share this tile, so they sit tighter
       than .flow-barline-value-row's own default 4px gap (tuned for a single icon+count pair) -
       needs to match that selector's own specificity (two classes) to actually win over it. */
    .flow-tile-value .flow-ramp-tile-row { gap: var(--space-0-5); }
    .flow-ramp-tile-row .flow-barline-value-count { margin-right: var(--space-0-5); }
    .flow-ramp-row-lines { display: flex; flex-direction: column; gap: var(--space-0-5); min-width: 0; }
    .flow-ramp-row-title { font-weight: var(--font-weight-bold); font-size: var(--font-sm); }
    .flow-ramp-row-tag { font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--label-color); }
    .flow-ramp-row-text-invalid .flow-ramp-row-title, .flow-ramp-row-text-invalid .flow-ramp-row-tag { color: var(--danger-text); }
    /* ML-286: the existing pauses and ramps are a list you open - so they're list rows, like the
       Flow library and session history: the thick left bar marks a row you can tap, where a plain
       bordered box read as just another button. Red when the row needs fixing. */
    .flow-fermata-row, .flow-ramp-row { border-left-width: 6px; }
    /* ML-286: the row's text is a button filling the row, so a tap anywhere but the ⋮ opens it to edit. */
    .flow-list-row-body { flex: 1 1 auto; min-height: var(--touch-target); padding: 0; background: transparent; border: none; color: inherit; font: inherit; text-align: left; }
    .flow-fermata-row:has(.flow-fermata-row-text-invalid), .flow-ramp-row:has(.flow-ramp-row-text-invalid) { border-left-color: var(--danger-text); }
    #flowRampAddBox .btn-submit, #flowRampAddBox .btn-cancel { margin-top: var(--space-2); }
    /* "Ramp ends at" toggle - a compact two-option segmented pill (End of block/Specific beat)
       rather than the Pause-type grid's larger glyph+title+subtitle cards, since there's no glyph
       to show here and the field card's own label already explains what it's for. */
    .flow-ramp-toggle-row { display: flex; border: 1px solid var(--input-border); border-radius: var(--radius-md); overflow: hidden; }
    .flow-ramp-toggle-btn { padding: var(--space-2) var(--space-3); border: none; background: var(--container-bg); color: var(--text-color); font-size: var(--font-xs); font-weight: var(--font-weight-bold); cursor: pointer; white-space: nowrap; }
    .flow-ramp-toggle-btn + .flow-ramp-toggle-btn { border-left: 1px solid var(--input-border); }
    .flow-ramp-toggle-btn.selected { background: var(--primary-action); color: var(--container-bg); }
    /* "Set custom tempo" row's own button - opens the same Tempo (bpm) popup as the block's own bpm
       tile (flowOpenBpmModal, generalised to take an onApply callback for this) rather than a new
       picker of its own. */
    .flow-ramp-tempo-btn { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); border: 1px solid var(--input-border); background: var(--container-bg); color: var(--text-color); font-weight: var(--font-weight-bold); font-size: var(--font-sm); cursor: pointer; }
    .flow-ramp-tempo-btn .material-symbols-outlined { font-size: var(--icon-sm); color: var(--label-color); }
    .flow-ramp-tempo-unit { font-weight: var(--font-weight-normal); font-size: var(--font-xs); color: var(--label-color); }

    /* Mobile responsive - remove max-width and adjust spacing */
    @media (max-width: 699px) { /* ML-239: was 768 - from 700 up it is the tablet layout (the end of this file) */
        .nav-menu-backdrop { background: var(--menu-backdrop-bg); }
        body { align-items: stretch; }
        .top-bar { max-width: 100%; padding: var(--space-2) var(--space-4); }
        .top-bar-title { font-size: var(--font-md); }
        .container { max-width: 100%; padding: var(--space-4); min-height: calc(100vh - 55px); }
        .flow-edit-sticky-bar { max-width: 100%; padding-left: var(--space-4); padding-right: var(--space-4); }
        .btn-large, .btn-submit, .btn-nav { padding: var(--space-4); font-size: var(--font-base); }
        .form-group { margin-bottom: var(--space-4); }
        label { margin-bottom: var(--space-1); font-size: var(--font-base); }
        input, select { padding: var(--space-3) var(--space-3); font-size: var(--font-base); }
        .dashboard-grid { margin-bottom: var(--space-3); }
        .stat-card { padding: var(--space-3); }
        .stat-card .value { font-size: var(--font-lg); }
        .section-title { margin: var(--space-5) 0 var(--space-2) 0; font-size: var(--font-base); }
        .heatmap-wrapper { padding-top: var(--space-5); padding-bottom: var(--space-1); }
        .heatmap-legend { margin-bottom: var(--space-4); }
        .chart-wrapper { height: 200px; margin-bottom: var(--space-2); }
        .chart-scroll-area { padding: var(--space-2) var(--space-1) var(--space-7) var(--space-1); }
        .chart-bar-container { min-width: 12px; margin-right: var(--space-0-5); }
        .tuner-graph-card { padding: var(--space-3); margin-bottom: var(--space-4); }
        .tuner-pitch-graph { height: 75px; }
        .tuner-dynamics-graph { height: 55px; }
        .history-item { padding: var(--space-3); margin-bottom: var(--space-2); }
        .play-card { padding: var(--space-5) var(--space-4); margin-bottom: var(--space-5); }
        .modal-content { padding: var(--space-5); }
        .scroll-btn { width: 28px; height: 28px; font-size: var(--font-base); }
        .scroll-btn.left { left: 40px; }
    }

    @media (max-width: 480px) {
        body { align-items: stretch; }
        .top-bar { padding: var(--space-1) var(--space-3); }
        .top-bar-title { font-size: var(--font-base); }
        .container { padding: var(--space-3); }
        .flow-edit-sticky-bar { padding-left: var(--space-3); padding-right: var(--space-3); }
        .btn-large, .btn-submit, .btn-nav { padding: var(--space-3); font-size: var(--font-base); margin-bottom: var(--space-3); }
        input, select { font-size: var(--font-base); padding: var(--space-3); }
        .stat-card { padding: var(--space-3); }
        .stat-card .value { font-size: var(--font-md); }
        .stat-card .label { font-size: var(--font-sm); }
        .history-item { padding: var(--space-2); margin-bottom: var(--space-1); font-size: var(--font-base); }
    }

    /* =========================================================================================
       ML-210 accessibility foundations (see specs/foundations/accessibility.md). Checked on every
       release by scripts/a11y-audit.mjs via the design gate.
       ========================================================================================= */

    /* Screen-reader-only text (live announcements, extra context) - present for assistive tech, invisible on screen. */
    .visually-hidden { position: absolute !important; width: 1px; height: 1px; margin: -1px; /* token-audit-ignore: standard screen-reader-only clip pattern */ padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

    /* ML-211: a row that holds a single toggle switch is tappable anywhere (public/a11y.js flips the switch). */
    :where(*:has(> .toggle-switch)):not(button) { cursor: pointer; }

    /* A5 - one visible keyboard focus style for everything interactive: a page-coloured gap plus a
       strong-gold ring (--focus-ring), so it shows on any surface including gold buttons. Only on
       :focus-visible, so a mouse/touch tap never leaves a ring behind. */
    :where(button, a[href], input, select, textarea, summary, [tabindex]:not([tabindex="-1"]), [role="button"], [role="slider"], [role="switch"], [role="menuitem"]):focus-visible { outline: none; box-shadow: var(--focus-ring); }
    /* Controls whose real input is visually hidden show the ring on the part you can see. */
    .toggle-switch input:focus-visible + .toggle-slider, .radio-group input[type="radio"]:focus-visible + label, .radio-group input[type="checkbox"]:focus-visible + label { box-shadow: var(--focus-ring); }

    /* A6 - 44x44 minimum hit area (house rule; WCAG 2.5.8 minimum is 24). The visible control keeps
       its size; an invisible ::before centred on it catches taps out to var(--touch-target). The
       controls listed in the first rule aren't otherwise positioned, so they become the ::before's
       containing block; the absolutely-positioned ones (.scroll-btn, .tuner-card-settings-btn,
       .metroBlk-tile-menu-btn) already are. */
    :where(.top-tuner-toggle, .tuner-pause-btn, .metro-mute-btn, .metroBlk-row-volume-btn, .metroBlk-mini-tuner-close-btn, .qp-bar-menu-btn, .qp-merge-split-btn, .qp-merge-branch-btn, .qp-loaded-history-clear-btn, .qp-loaded-history-undo-btn, .list-item-menu-btn, .flow-bar-popup-icon-btn, .flow-delete-btn, .flow-block-grab-handle, .metro-bpm-step, .steps-progress-back) { position: relative; }
    :is(.top-tuner-toggle, .scroll-btn, .tuner-card-settings-btn, .tuner-pause-btn, .metro-mute-btn, .metroBlk-row-volume-btn, .metroBlk-mini-tuner-instrument-btn, .metroBlk-mini-tuner-close-btn, .qp-bar-menu-btn, .qp-merge-split-btn, .qp-merge-branch-btn, .qp-loaded-history-clear-btn, .qp-loaded-history-undo-btn, .list-item-menu-btn, .flow-bar-popup-icon-btn, .flow-delete-btn, .flow-block-grab-handle, .metro-bpm-step, .steps-progress-back)::before { content: ''; position: absolute; top: 50%; left: 50%; width: max(100%, var(--touch-target)); height: max(100%, var(--touch-target)); transform: translate(-50%, -50%); }

    /* A7 - respect the OS "reduce motion" setting: no slides, fades, pulses or scale bounces. A near-zero
       duration (not 0) so any code waiting on transitionend/animationend still gets its event. Beat dots
       keep their colour change but lose the scale pulse. */
    @media (prefers-reduced-motion: reduce) {
        *, *::before, *::after { animation-duration: 0.01ms !important; /* token-audit-ignore: reduced-motion override */ animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; /* token-audit-ignore: reduced-motion override */ scroll-behavior: auto !important; }
        .metro-dot-note.lit, .metro-dot-note.accent.lit, .metro-dot-sub.lit { transform: translate(-50%, -50%); }
    }

    /* =========================================================================================
       ML-288: layout and type utilities - the replacement for inline style="". One class, one job,
       one token, so a tablet/landscape/desktop layout or a new colour scheme can reach every style
       from the stylesheet. Kept last in the file so a utility beats the single-class component rule
       it adjusts (the job the inline style used to do). Reach for a component class first; a utility
       is for a one-off nudge. The older .flex-row/.gap-sm/.no-margin set stays where it is (further
       up) - existing component rules are written to override it. See
       specs/components/utilities-and-states.md.
       ========================================================================================= */
    .flex-wrap { flex-wrap: wrap; }
    .items-center { align-items: center; }
    .justify-between { justify-content: space-between; }
    .justify-center { justify-content: center; }
    .justify-end { justify-content: flex-end; }
    .gap-xs { gap: var(--space-1); }
    .pos-relative { position: relative; }
    .text-left { text-align: left; }
    .text-sm { font-size: var(--font-sm); }
    .text-base { font-size: var(--font-base); }
    .text-md { font-size: var(--font-md); }
    .text-2xl { font-size: var(--font-2xl); }
    .fw-normal { font-weight: var(--font-weight-normal); }
    .fw-bold { font-weight: var(--font-weight-bold); }
    .leading-base { line-height: var(--line-height-base); }
    .pre-line { white-space: pre-line; } /* a message with line breaks in it */
    .p-2 { padding: var(--space-2); }
    .p-4 { padding: var(--space-4); }
    .mt-0 { margin-top: 0; }
    .mt-1 { margin-top: var(--space-1); }
    .mt-2 { margin-top: var(--space-2); }
    .mt-3 { margin-top: var(--space-3); }
    .mt-4 { margin-top: var(--space-4); }
    .mt-5 { margin-top: var(--space-5); }
    .mt-6 { margin-top: var(--space-6); }
    .mt-7 { margin-top: var(--space-7); }
    .mb-1 { margin-bottom: var(--space-1); }
    .mb-2 { margin-bottom: var(--space-2); }
    .mb-3 { margin-bottom: var(--space-3); }
    .mb-4 { margin-bottom: var(--space-4); }
    .mb-5 { margin-bottom: var(--space-5); }
    .mb-6 { margin-bottom: var(--space-6); }
    /* An inline (content-width) button, e.g. Save/Join beside an input. -sm is the tighter row of
       challenge actions, which sits in a flex row so never takes the stacked-button margin. */
    .btn-inline { width: auto; padding: var(--space-3) var(--space-5); }
    .btn-inline-sm { width: auto; padding: var(--space-2) var(--space-4); margin: 0; }
    /* A small native checkbox inside a filter/label row. */
    .check-sm { width: var(--icon-sm); height: var(--icon-sm); }
    .pointer-none { pointer-events: none; }
    .btn-inline-xs { width: auto; padding: var(--space-2) var(--space-3); }
    .grow { flex-grow: 1; }
    .items-start { align-items: flex-start; }
    .ml-1 { margin-left: var(--space-1); }
    .pl-1 { padding-left: var(--space-1); }
    .p-3 { padding: var(--space-3); }
    .p-5 { padding: var(--space-5); }
    .text-2xs { font-size: var(--font-2xs); }
    .icon-md { font-size: var(--icon-md); } /* a Material Symbols glyph at the default icon size */
    .text-danger { color: var(--danger-text); }
    .text-success { color: var(--success-text); }
    .text-selection { color: var(--selection-text); }
    .w-full { width: 100%; }
    .text-accent-strong { color: var(--primary-action-strong); }

    /* ML-288: session categories. One class per category sets its two colours; components read them. */
    .category-practise { --category-accent: var(--cat-practise); --category-accent-text: var(--cat-practise-text); }
    .category-rehearsal { --category-accent: var(--cat-rehearsal); --category-accent-text: var(--cat-rehearsal-text); }
    .category-lesson { --category-accent: var(--cat-lesson); --category-accent-text: var(--cat-lesson-text); }
    .category-performance { --category-accent: var(--cat-performance); --category-accent-text: var(--cat-performance-text); }
    .category-text { color: var(--category-accent-text); }
    /* Stats filter pills: an active category pill takes that category's colour (see .filter-pill.active). */
    .filter-pill[data-filter-cat] { --filter-pill-accent: var(--category-accent); --filter-pill-accent-text: var(--category-accent-text); }

    /* ML-288: small form/section patterns that used to be inline styles. */
    /* A tinted block that groups related fields inside a form (challenge task options). -row lays out
       a label and its control either side of one line. */
    .inset-panel { background: var(--secondary-color); padding: var(--space-4); border-radius: var(--radius-md); }
    .inset-panel.inset-panel-row { display: flex; justify-content: space-between; align-items: center; padding: var(--space-3) var(--space-4); }
    /* Manage challenges: the filter checkboxes in a bordered box. */
    .challenge-filter-box { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-4); background: var(--input-bg); padding: var(--space-3); border-radius: var(--radius-md); border: 1px solid var(--input-border); }
    /* Edit challenge: the progress summary line under the name. */
    /* Challenge session summary: one row per task worked on - name and ref left, status right. */
    .summary-item-row { padding: var(--space-3); border-bottom: 1px solid var(--input-border); display: flex; justify-content: space-between; }
    /* Challenge session summary title - gold, or green once every challenge is complete. */
    .summary-title { color: var(--primary-action-strong); }
    .summary-title.is-complete { color: var(--success-text); }
    /* About: the bulleted change list under each release. */
    .release-change-list { margin: var(--space-1) 0 0 0; padding-left: var(--space-5); }
    .challenge-edit-stats { font-weight: var(--font-weight-bold); margin-top: var(--space-2); margin-bottom: var(--space-4); font-size: var(--font-base); color: var(--primary-action-strong); }
    /* A sub-heading inside a form, after a .divider-dashed (the challenge editor's Tasks). */
    .form-subheading { margin-top: 0; margin-bottom: var(--space-4); font-size: var(--font-md); color: var(--text-color); }
    .divider-dashed { border: 0; border-top: 1px dashed var(--input-border); margin: var(--space-6) 0 var(--space-4) 0; }

    /* =========================================================================================
       ML-288: run-time placement and motion. Where something is, or how far it's been dragged, is
       only known while the app runs, so JS sets it as a custom property and one of these classes
       reads it (app.js: placeAt, setMove/clearMove, swipeTo). Last in the file on purpose: while it
       applies, it has to win over the component's own resting position, the way an inline style did.
       ========================================================================================= */
    /* A fixed menu or popup put next to whatever opened it. */
    .is-placed { left: var(--place-x); top: var(--place-y); }
    /* Dragged, swiped, or part-way through a FLIP reorder. */
    .is-moved { transform: translate(var(--move-x), var(--move-y)); }
    .is-faded { opacity: 0; }
    .is-raised { z-index: var(--z-raised); }
    /* Transitions: off while a finger is moving something (or for a one-frame jump), a swipe settling
       open/shut, or a dragged box springing back. */
    .no-transition { transition: none; }
    .transition-swipe { transition: transform var(--duration-swipe) ease; }
    .transition-transform { transition: transform var(--duration-base) ease; }

    /* Practice Levels (ML-316, epic ML-314) - see specs/components/level-map.md. A piece's Level map
       (one square per bar, the Level number in it so it never relies on colour), the thin strip for
       list rows, the Level chip and the 1-5 Level picker. Colours: --level-1..5, silver to gold. */
    /* ML-336: 10 bars a row, the row's bar numbers in a left column ("1 - 10", or just "1" when narrow). */
    .level-map { display: grid; grid-template-columns: max-content repeat(10, minmax(0, 1fr)); gap: var(--space-0-5); margin-bottom: var(--space-3); container-type: inline-size; }
    .level-map-bars { grid-column: 1; align-self: center; padding-right: var(--space-1); font-size: var(--font-xs); color: var(--label-color); font-variant-numeric: tabular-nums; white-space: nowrap; }
    @container (max-width: 300px) { .level-map-bars-to { display: none; } }
    .level-cell { min-height: var(--level-cell-size); aspect-ratio: 1; border-radius: var(--radius-2xs); display: grid; place-items: center; font-size: var(--font-xs); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); box-sizing: border-box; }
    .level-strip { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: var(--space-0-5); height: var(--level-strip-height); }
    .level-strip .level-cell { min-height: 0; min-width: 0; aspect-ratio: auto; font-size: 0; }
    /* ML-334: a long piece's strip has a cell per bar - an unset bar's dashed border (2px a cell) pushed
       the strip off the page, so in a strip an unset bar is a plain fill instead. */
    .level-strip { overflow: hidden; }
    .level-strip .lv-0 { border: none; background: var(--level-unset-border); }
    /* ML-390: a piece's Levels as one bar chart on a list row (.level-bar) - how many of its bars are at each
       Level, 1 to 5 left to right, then the bars not painted (dashed). At most six parts however long the
       piece is. Each part's width is its share of the bars (--bars, set by sizeLevelBars), never less than
       a sliver; the Level number shows inside when the part is wide enough to hold it. */
    .level-bar { display: flex; gap: var(--space-0-5); height: var(--level-bar-height); }
    .level-bar-part { flex: var(--bars, 1) 1 0; min-width: var(--space-1); border-radius: var(--radius-2xs); display: grid; place-items: center; font-size: var(--font-xs); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); box-sizing: border-box; overflow: hidden; container-type: inline-size; }
    @container (max-width: 14px) { .level-bar-num { display: none; } }
    .level-legend:last-child { margin-bottom: 0; }
    .lv-0 { background: transparent; border: 1px dashed var(--level-unset-border); color: var(--label-color); }
    .lv-1 { background: var(--level-1); color: var(--level-1-text); }
    .lv-2 { background: var(--level-2); color: var(--level-2-text); }
    .lv-3 { background: var(--level-3); color: var(--level-3-text); }
    .lv-4 { background: var(--level-4); color: var(--level-4-text); }
    .lv-5 { background: var(--level-5); color: var(--level-5-text); }
    .level-cell.is-marked { box-shadow: var(--focus-ring); }
    .level-legend { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); font-size: var(--font-sm); color: var(--label-color); margin-bottom: var(--space-4); }
    .level-legend > span { display: inline-flex; align-items: center; gap: var(--space-1); }
    .level-legend .level-cell { width: var(--icon-md); min-height: var(--icon-md); }
    .level-chip { flex-shrink: 0; width: var(--icon-xl); height: var(--icon-xl); border-radius: var(--radius-xs); display: grid; place-items: center; font-size: var(--font-sm); font-weight: var(--font-weight-bold); box-sizing: border-box; }
    .level-picker { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--space-1); margin-bottom: var(--space-3); }
    .level-pick { min-height: var(--touch-target); border: 2px solid transparent; border-radius: var(--radius-md); display: grid; place-items: center; align-content: center; gap: var(--space-0-5); font: inherit; font-size: var(--font-md); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); cursor: pointer; padding: var(--space-1); }
    .level-pick small { font-size: var(--font-xs); font-weight: var(--font-weight-semibold); }
    .level-pick.selected { border-color: var(--primary-action-strong); box-shadow: var(--focus-ring); }
    .level-notice { background: var(--status-amber-bg); color: var(--status-amber-fg); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4); font-size: var(--font-sm); }
    .level-row-body { display: flex; align-items: center; gap: var(--space-3); min-width: 0; min-height: var(--touch-target); background: none; border: none; padding: 0; font: inherit; color: inherit; cursor: pointer; }
    .level-fit-warn { color: var(--status-amber-fg); font-weight: var(--font-weight-bold); }
    .level-answer { width: 100%; text-align: left; background: var(--input-bg); color: var(--text-color); font: inherit; font-weight: var(--font-weight-bold); line-height: var(--line-height-base); min-height: var(--touch-target); }
    .history-item.level-row-selected { border-color: var(--primary-action-strong); box-shadow: var(--focus-ring); }

    /* Practice session (ML-320, epic ML-314) - see specs/components/practice-session.md. The bar under the
       top bar while a session runs (in .top-bar-sticky-group, so it stays put on every screen) and the
       strip of the session's 5-minute blocks: done gold, the one now dark gold, the rest grey. */
    .session-bar { width: 100%; max-width: var(--app-max-width); box-sizing: border-box; display: grid; gap: var(--space-1); padding: var(--space-2) var(--space-5); min-height: var(--touch-target); background: var(--container-bg); color: var(--text-color); border: none; border-bottom: 2px solid var(--primary-action-strong); font: inherit; text-align: left; cursor: pointer; }
    .session-bar-row { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-3); font-size: var(--font-sm); font-weight: var(--font-weight-semibold); }
    .session-bar-row strong { font-variant-numeric: tabular-nums; }
    /* ML-220: the offline bar - under the top bar on every screen while there is no connection, or while
       things logged offline are still waiting to be sent. Amber, like other "take note" states; the icon
       and the words carry the meaning, not the colour. The list of what is waiting (.offline-item) is in
       its pop-up. specs/components/offline-status.md */
    .offline-bar { width: 100%; max-width: var(--app-max-width); box-sizing: border-box; display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-5); min-height: var(--touch-target); background: var(--status-amber-bg); color: var(--status-amber-fg); font-size: var(--font-sm); font-weight: var(--font-weight-semibold); }
    .offline-bar-text { flex: 1; min-width: 0; }
    .offline-bar-btn { flex-shrink: 0; min-height: var(--touch-target); padding: 0 var(--space-3); background: none; border: 1px solid var(--status-amber-fg); border-radius: var(--radius-sm); color: var(--status-amber-fg); font-family: var(--font-sans); font-size: var(--font-sm); font-weight: var(--font-weight-bold); cursor: pointer; }
    .offline-item { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) 0; border-bottom: 1px solid var(--input-border); text-align: left; }
    .offline-item-text { flex: 1; min-width: 0; display: grid; gap: var(--space-0-5); }
    .offline-item-text span { font-size: var(--font-sm); color: var(--label-color); }
    .offline-item.is-failed .offline-item-text span { color: var(--danger-text); }
    .offline-item button { width: auto; flex-shrink: 0; } /* Remove sits beside the words, it does not take the row */
    /* The piece coming next, so its music can be got ready (owner, 1 Oct 2026) - one line, cut short with ... */
    .session-bar-next { font-size: var(--font-xs); color: var(--label-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .session-strip { display: flex; gap: var(--space-0-5); height: var(--space-2); }
    .session-seg { flex: 1; border-radius: var(--radius-2xs); background: var(--control-border); }
    .session-seg.is-done { background: var(--primary-action); }
    /* The block you're on fills in from the left as its time goes (--seg-fill 0-1, set by renderPracticeRun),
       ringed so it shows even before it starts filling. */
    .session-seg.is-now { position: relative; overflow: hidden; box-sizing: border-box; border: 1px solid var(--primary-action-strong); }
    .session-seg.is-now::after { content: ''; position: absolute; inset: 0; background: var(--primary-action-strong); transform: scaleX(var(--seg-fill, 1)); transform-origin: left; }
    .session-block-time { flex-shrink: 0; min-width: var(--space-9); font-variant-numeric: tabular-nums; }

    /* Stepped practice sessions (ML-390) - see specs/components/practice-steps.md. One question a screen,
       a gold progress bar for the three steps, time as 5-minute blocks you can count, and the kinds of
       block (Warm-up, Scales, Skills, Pieces) as coloured tiles that always carry their icon (and name). */
    .steps-progress { display: flex; gap: var(--space-2); list-style: none; margin: 0 0 var(--space-4); padding: 0; }
    .steps-progress-step { flex: 1; display: grid; gap: var(--space-1); font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--label-color); }
    .steps-progress-step::before { content: ""; display: block; height: var(--space-1); border-radius: var(--radius-pill); background: var(--input-border); }
    .steps-progress-step.is-done::before, .steps-progress-step.is-now::before { background: var(--primary-action); }
    .steps-progress-step.is-now { color: var(--text-color); }
    /* ML-449: a journey of many steps (Quick entry's 13) as one bar - a piece a step, gold when done, strong gold
       for the one you are on, a wider gap where a stage ends - with Back and Next in a bar at the bottom of the
       screen (.step-foot on the edit screen's sticky bar: Back takes a third, Next the rest). */
    .step-bar { display: flex; gap: var(--space-0-5); margin: 0 0 var(--space-2); }
    .step-bar-piece { flex: 1; height: var(--space-2); border-radius: var(--radius-pill); background: var(--input-border); }
    .step-bar-piece.is-done { background: var(--primary-action); }
    .step-bar-piece.is-now { background: var(--primary-action-strong); }
    .step-bar-piece.ends-stage { margin-right: var(--space-1); }
    .step-foot .btn-cancel { flex: 1; }
    .step-foot .btn-submit { flex: 2; }
    #pieceOutlineView { padding-bottom: var(--bottom-bar-clearance-lg); }
    /* ML-420: a step already done is a button back to it. It looks like the other steps, in the link colour (the
       gold of its bar) - with a tick for done; no chevron or underline, which read as a stray link (owner, 4 Oct 2026); the underline
       comes on hover and keyboard focus. Its tap area is the shared 44px one (A6). Steps to come are never buttons. */
    .steps-progress-back { display: inline-flex; align-items: center; gap: var(--space-0-5); justify-self: start; padding: 0; border: 0; background: none; font: inherit; color: var(--link-color); cursor: pointer; }
    .steps-progress-back:hover, .steps-progress-back:focus-visible { text-decoration: underline; }
    .steps-progress-back .material-symbols-outlined { font-size: var(--icon-sm); } /* the tick: done */
    .step-question { font-size: var(--font-lg); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); margin: 0 0 var(--space-3); text-wrap: balance; }
    /* One strip across the width, a block per 5 minutes - more minutes, thinner blocks (owner, 1 Oct 2026: numbered
       squares looked like buttons). Display only. */
    .time-blocks { display: flex; gap: var(--space-1); height: var(--time-strip-height); margin: 0 0 var(--space-4); }
    /* ML-419: thin grey bars, like the strip on a running session's bar - they were gold boxes the height of a
       button, right above the gold Next button, and looked tappable. Gold stays for things you tap. */
    .time-block { flex: 1 1 0; min-width: 0; border-radius: var(--radius-pill); background: var(--control-border); box-sizing: border-box; }
    .time-block.is-new { animation: block-pop var(--duration-base) ease-out; }
    .time-block.is-more { border: 2px dashed var(--control-border); background: transparent; }
    @keyframes block-pop { from { transform: scale(0.4); opacity: var(--opacity-muted); } to { transform: scale(1); opacity: 1; } }
    .plan-card { flex: 1; min-width: 0; flex-direction: column; align-items: stretch; gap: var(--space-2); }
    .plan-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
    /* ML-424: Quick piece entry (specs/components/piece-outline.md). The steps bar, the question, the value
       boxes, the sections of bars (.paint-section / .paint-bars) and the pinned toolbox (.paint-toolbox) are
       the practice steps' and Prepare's own; these are what's new. */
    .outline { display: grid; gap: var(--space-3); }
    .outline p svg.notation { vertical-align: middle; } /* a sign drawn in a line of words (the pauses and signs steps' hints) sits on the line's middle */
    .outline-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); align-items: end; }
    .outline-three { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-2); align-items: end; }
    .outline-field { display: grid; gap: var(--space-1); }
    .outline-field input { text-align: center; font-weight: var(--font-weight-bold); }
    .outline-field-label, .outline-th { font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--label-color); }
    .outline-field-label { text-align: center; }
    .outline-list { font-size: var(--font-lg); line-height: var(--line-height-relaxed); }
    .outline-chips { display: flex; flex-wrap: wrap; gap: var(--space-1); }
    .outline-chip { padding: var(--space-0-5) var(--space-2); border: 1px solid var(--control-border); border-radius: var(--radius-pill); font-size: var(--font-xs); font-weight: var(--font-weight-bold); }
    /* A table you type down: bar, then what's there. Marks have two columns, speeds three. */
    .outline-table { display: grid; gap: var(--space-2); }
    .outline-row { display: grid; gap: var(--space-2); align-items: center; }
    .outline-row-marks { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
    .outline-row-speed { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    /* ML-425: the time signature table - from bar, the time (a box you type 3/4 into, with the list button beside it),
       then how long it lasts two ways - bars and to bar (ML-442). Four across a phone, so less padding like the rows below. */
    .outline-row-time { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) minmax(0, 2fr) minmax(0, 2fr); gap: var(--space-1); }
    .outline-row-time input, .outline-row-time .outline-cell-fixed { padding-left: var(--space-1); padding-right: var(--space-1); text-align: center; }
    /* ML-440: a box that stops its row being used is outlined in red (the note under the table says why) */
    .outline-row input[aria-invalid="true"] { border-color: var(--danger-text); }
    .outline-sig-cell { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-1); align-items: center; }
    .outline-sig-cell input { text-align: center; }
    /* ML-435: repeats (from, to, times, 1st ending, 2nd ending) and pauses (bar, beat, beats held, kind) as tables.
       Five boxes across a phone need the room, so these rows' boxes have less padding and centred numbers. */
    .outline-row-repeat { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--space-1); }
    .outline-row-pause { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-1); }
    .outline-row-repeat input, .outline-row-pause input { padding-left: var(--space-1); padding-right: var(--space-1); text-align: center; }
    .outline-cell-fixed, .outline-cell-btn { min-height: var(--touch-target); display: grid; place-items: center; padding: 0 var(--space-2); border-radius: var(--radius-lg); font: inherit; font-size: var(--font-sm); color: var(--label-color); box-sizing: border-box; }
    .outline-cell-fixed { border: 1px dashed var(--control-border); }
    .outline-cell-btn { border: 2px solid var(--control-border); background: var(--input-bg); color: var(--text-color); font-weight: var(--font-weight-bold); cursor: pointer; }
    .outline-sum { font-size: var(--font-xs); color: var(--label-color); }
    .outline-sum strong { color: var(--text-color); }
    /* An extra, in words; one that clashes is red with the bar editor's warning triangle */
    .outline-extra { width: 100%; min-height: var(--touch-target); display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--space-3); align-items: center; padding: var(--space-3); border: 1px solid var(--control-border); border-radius: var(--radius-lg); background: var(--input-bg); color: var(--text-color); font: inherit; text-align: left; cursor: pointer; }
    .outline-extra > .material-symbols-outlined { color: var(--label-color); }
    .outline-extra-text { display: grid; gap: var(--space-0-5); }
    .outline-extra.has-clash { border-color: var(--danger-text); background: var(--danger-tint); }
    .outline-extra-clash { display: flex; align-items: center; gap: var(--space-2); margin: 0; font-size: var(--font-sm); font-weight: var(--font-weight-semibold); color: var(--danger-text); }
    .outline-extra-clash .flow-tile-warning-icon { flex: none; width: var(--icon-md); height: var(--icon-md); }
    .outline-add { min-height: var(--touch-target); border: 2px dashed var(--control-border); border-radius: var(--radius-lg); background: transparent; color: var(--text-color); font: inherit; font-weight: var(--font-weight-bold); cursor: pointer; }
    /* ML-418: a plan in the Pick a plan pop-up - its row is the button; one of your own has a ⋮ inside the box, top right */
    .plan-row { position: relative; }
    .plan-row .list-item-menu-btn { position: absolute; top: var(--space-2); right: var(--space-2); }
    .plan-row.has-menu .flow-choice-option { padding-right: calc(var(--touch-target) + var(--space-3)); }
    /* the picked plan's blocks in the pop-up's footer: eight to a row, lower */
    .kind-strip-compact { grid-template-columns: repeat(8, minmax(0, 1fr)); }
    .kind-strip-compact .kind-block { min-height: var(--icon-lg); }
    .kind-strip-compact .kind-block .material-symbols-outlined { font-size: var(--icon-sm); }
    .kind-strip { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--space-1); }
    .kind-strip .kind-block { aspect-ratio: auto; min-height: var(--icon-xl); }
    /* The key under the plans on Pick a plan: each kind of block's symbol and its name. */
    /* ML-420: "Key:" and the kinds on one line (wrapping only with all five), with space above - it ran on from the blocks */
    .kind-key { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); margin: var(--space-4) 0; font-size: var(--font-xs); color: var(--label-color); }
    .kind-key-label { color: var(--text-color); }
    .kind-key-items { display: contents; }
    .kind-key-items > span, .kind-key > span:not(.kind-key-items) { display: inline-flex; align-items: center; gap: var(--space-1); }
    .kind-key .kind-block { width: var(--icon-lg); height: var(--icon-md); padding: 0; border-radius: var(--radius-xs); }
    .kind-key .kind-block .material-symbols-outlined { font-size: var(--icon-sm); }
    .kind-strip-more { display: grid; place-items: center; font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--label-color); }
    .kind-block { display: grid; place-items: center; align-content: center; gap: var(--space-0-5); box-sizing: border-box; border-radius: var(--radius-md); border: 2px solid var(--control-border); background: var(--input-bg); color: var(--text-color); font: inherit; padding: var(--space-1); }
    .kind-block .material-symbols-outlined { font-size: var(--icon-md); font-variation-settings: 'FILL' 1; }
    .kind-block-label { font-size: var(--font-xs); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); text-align: center; }
    .kind-block.is-empty { border-style: dashed; background: transparent; color: var(--label-color); }
    .kind-block.is-empty .material-symbols-outlined { font-variation-settings: 'FILL' 0; }
    .kind-warmup { border-color: var(--kind-warmup); background: var(--kind-warmup-tint); }
    .kind-warmup .material-symbols-outlined, .kind-warmup .kind-block-label, .kind-warmup .build-slot-time { color: var(--kind-warmup-text); }
    .kind-scales { border-color: var(--kind-scales); background: var(--kind-scales-tint); }
    .kind-scales .material-symbols-outlined, .kind-scales .kind-block-label, .kind-scales .build-slot-time { color: var(--kind-scales-text); }
    .kind-skills { border-color: var(--kind-skills); background: var(--kind-skills-tint); }
    .kind-skills .material-symbols-outlined, .kind-skills .kind-block-label, .kind-skills .build-slot-time { color: var(--kind-skills-text); }
    .kind-pieces { border-color: var(--kind-pieces); background: var(--kind-pieces-tint); }
    .kind-pieces .material-symbols-outlined, .kind-pieces .kind-block-label, .kind-pieces .build-slot-time { color: var(--kind-pieces-text); }
    .kind-theory { border-color: var(--kind-theory); background: var(--kind-theory-tint); } /* ML-418 */
    .kind-theory .material-symbols-outlined, .kind-theory .kind-block-label, .kind-theory .build-slot-time { color: var(--kind-theory-text); }
    .kind-block-icon { flex-shrink: 0; width: var(--touch-target); height: var(--touch-target); padding: 0; }
    .history-item .kind-block, .level-row-body .kind-block { flex-shrink: 0; width: var(--icon-xl); height: var(--icon-xl); padding: 0; }
    .build-slots { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-2); margin-bottom: var(--space-4); }
    .build-slot { position: relative; min-height: calc(var(--touch-target) + var(--space-5)); cursor: pointer; }
    .build-slot.is-new { animation: block-pop var(--duration-base) ease-out; }
    .build-slot-time { position: absolute; top: var(--space-0-5); left: var(--space-1); font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--label-color); font-variant-numeric: tabular-nums; }
    .build-palette { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-2); padding: var(--space-2); margin-bottom: var(--space-3); border: 1px solid var(--input-border); border-radius: var(--radius-lg); background: var(--input-bg); }
    .build-add { min-height: calc(var(--touch-target) + var(--space-3)); cursor: pointer; }
    .session-goal { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-1); padding: var(--space-2) var(--space-3); margin: 0 0 var(--space-3); border-radius: var(--radius-md); background: var(--primary-action-tint); font-size: var(--font-sm); font-weight: var(--font-weight-semibold); }
    .session-goal .level-chip, .session-goal-card .level-chip { width: var(--icon-lg); height: var(--icon-lg); font-size: var(--font-xs); }
    .session-piece { display: grid; gap: var(--space-1); padding: var(--space-2) 0; border-top: 1px solid var(--input-border); }
    .session-piece-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
    .session-goal-card { display: grid; gap: var(--space-1); margin-top: var(--space-4); padding: var(--space-3); border-radius: var(--radius-md); background: var(--primary-action-tint); }

    /* The 30-second rest between blocks (ML-390) - a calm panel of its own colours: the countdown ring (the
       Timer ring's pattern: --rest-left 0-1 from renderRest), one message with a picture - a breathing
       circle that grows over 4 seconds and shrinks over 6, three breaths in the rest - and Next up. */
    .rest-screen { display: grid; justify-items: center; gap: var(--space-4); padding: var(--space-5) var(--space-4); border-radius: var(--radius-xl); background: var(--rest-bg); color: var(--text-color); }
    .rest-ring { position: relative; width: calc(var(--touch-target) * 3); aspect-ratio: 1; }
    .rest-ring-svg { width: 100%; height: 100%; display: block; }
    .rest-ring-track, .rest-ring-left { fill: none; stroke-width: 9; }
    .rest-ring-track { stroke: var(--rest-track); }
    .rest-ring-left { stroke: var(--rest-ring); stroke-linecap: round; transform: rotate(-90deg); transform-origin: 60px 60px; stroke-dasharray: 100 100; stroke-dashoffset: calc(100 - var(--rest-left, 1) * 100); transition: stroke-dashoffset var(--duration-pulse) linear; }
    .rest-ring-num { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; font-size: var(--font-sm); color: var(--label-color); }
    .rest-ring-num strong { font-size: var(--font-2xl); line-height: var(--line-height-tight); color: var(--rest-text); font-variant-numeric: tabular-nums; }
    .rest-card { width: 100%; box-sizing: border-box; display: grid; justify-items: center; gap: var(--space-2); padding: var(--space-5) var(--space-4); border-radius: var(--radius-xl); background: var(--rest-surface); text-align: center; }
    .rest-art { min-height: calc(var(--touch-target) * 2); display: grid; place-items: center; }
    .rest-art .material-symbols-outlined { font-size: calc(var(--icon-xl) * 2); color: var(--rest-text); }
    .rest-kind { margin: 0; font-size: var(--font-xs); font-weight: var(--font-weight-bold); text-transform: uppercase; color: var(--rest-text); }
    .rest-title { margin: 0; font-size: var(--font-lg); line-height: var(--line-height-tight); text-wrap: balance; }
    .rest-body { margin: 0; max-width: 32ch; text-wrap: pretty; }
    .rest-breath { width: var(--touch-target); height: var(--touch-target); border-radius: var(--radius-circle); border: 3px solid var(--rest-ring); background: var(--rest-bg); display: grid; place-items: center; animation: rest-breathe var(--duration-breath) ease-in-out infinite; }
    .rest-breath-word { font-size: var(--font-xs); font-weight: var(--font-weight-bold); color: var(--rest-text); }
    @keyframes rest-breathe { 0% { transform: scale(1); } 40% { transform: scale(1.9); } 100% { transform: scale(1); } }
    .rest-next { width: 100%; box-sizing: border-box; display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3); border-radius: var(--radius-lg); background: var(--rest-surface); }

    /* A piece's path (ML-390) - three stops like a game map: done ones gold with a tick, the one now
       ringed, joined by a line. */
    .piece-path { list-style: none; margin: 0 0 var(--space-4); padding: 0; }
    .path-stage { position: relative; display: grid; grid-template-columns: var(--touch-target) 1fr; gap: var(--space-3); padding-bottom: var(--space-4); }
    .path-stage::before { content: ""; position: absolute; left: calc(var(--touch-target) / 2 - var(--space-0-5)); top: var(--touch-target); bottom: 0; width: var(--space-1); background: var(--input-border); }
    .path-stage:last-child::before { display: none; }
    .path-dot { width: var(--touch-target); height: var(--touch-target); border-radius: var(--radius-circle); border: 3px solid var(--input-border); background: var(--container-bg); color: var(--label-color); display: grid; place-items: center; box-sizing: border-box; }
    .path-stage.is-done .path-dot { background: var(--primary-action); border-color: var(--primary-action); color: var(--primary-action-text); }
    .path-stage.is-now .path-dot { border-color: var(--primary-action-strong); color: var(--primary-action-strong); box-shadow: var(--focus-ring); }
    .path-text { min-width: 0; padding-top: var(--space-2); }
    .path-steps { display: grid; gap: var(--space-1); margin-top: var(--space-2); }
    .path-step { display: flex; align-items: center; gap: var(--space-2); font-size: var(--font-sm); }
    .path-step .material-symbols-outlined { font-size: var(--icon-md); color: var(--label-color); }
    .path-step.is-done .material-symbols-outlined { color: var(--success-text); font-variation-settings: 'FILL' 1; }
    .focus-summary { display: flex; flex-wrap: wrap; gap: var(--space-1); margin: var(--space-2) 0; }

    /* Painting the bars (ML-390): each section's bars as big squares (the bar number small, the Level big -
       colour and number together), and the paint box pinned to the bottom of the screen. */
    .paint-section { display: grid; gap: var(--space-2); margin-bottom: var(--space-4); }
    .paint-section-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
    .paint-bars { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--touch-target), 1fr)); gap: var(--space-1); }
    .paint-bar { position: relative; aspect-ratio: 1; min-height: var(--touch-target); border-radius: var(--radius-md); border-width: 2px; font: inherit; font-size: var(--font-md); font-weight: var(--font-weight-bold); display: grid; place-items: center; cursor: pointer; padding: 0; }
    .paint-bar.lv-0 { border-width: 2px; }
    .paint-bar:not(.lv-0) { border: 2px solid transparent; }
    .paint-bar-num { position: absolute; top: var(--space-0-5); left: var(--space-1); font-size: var(--font-xs); font-weight: var(--font-weight-semibold); line-height: var(--line-height-tight); }
    .paint-bar.is-cut-before { box-shadow: var(--shadow-cut-line); }
    .paint-toolbox { position: sticky; bottom: 0; z-index: var(--z-sticky); display: grid; gap: var(--space-2); padding: var(--space-3) 0; background: var(--container-bg); border-top: 1px solid var(--input-border); }
    .paint-tools { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-2); }
    .paint-tool { min-height: var(--touch-target); display: grid; place-items: center; gap: var(--space-0-5); padding: var(--space-1); border: 2px solid var(--control-border); border-radius: var(--radius-md); background: var(--input-bg); color: var(--text-color); font: inherit; cursor: pointer; }
    .paint-tool[aria-pressed="true"] { border-color: var(--primary-action-strong); background: var(--primary-action-tint); }
    .paint-tool-label { font-size: var(--font-xs); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); }
    .paint-pots { margin-bottom: 0; }
    .cut-bit { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3); border: 1px solid var(--input-border); border-radius: var(--radius-md); }
    .cut-bit.is-too-long { border: 2px solid var(--danger-color); }

    /* "Did you nail it?" (ML-390): the Level pair (1 -> 2) and a short star celebration on a Yes. */
    .levelup { display: grid; justify-items: center; gap: var(--space-2); text-align: center; margin-bottom: var(--space-4); } /* room under the 1 -> 2 pair before "Played at..." (owner, 1 Oct 2026) */
    .levelup h2 { margin: 0; text-wrap: balance; }
    .levelup-stars { display: flex; gap: var(--space-1); color: var(--primary-action-strong); }
    .levelup-stars .material-symbols-outlined { font-variation-settings: 'FILL' 1; }
    .levelup.is-celebrating .levelup-stars .material-symbols-outlined { animation: block-pop var(--duration-slow) ease-out; }
    .levelup.is-celebrating .levelup-pair .level-chip:last-child { animation: block-pop var(--duration-slow) ease-out; }
    .levelup-pair { display: flex; align-items: center; gap: var(--space-2); color: var(--label-color); }
    .levelup-pair .level-chip { width: calc(var(--touch-target) + var(--space-2)); height: calc(var(--touch-target) + var(--space-2)); font-size: var(--font-xl); border-radius: var(--radius-lg); }

    /* A session's Warm-up block on a loop (ML-390) */
    .warmup-loop { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3); margin-bottom: var(--space-3); border: 2px solid var(--kind-warmup); border-radius: var(--radius-lg); background: var(--kind-warmup-tint); }
    .warmup-loop-icon { color: var(--kind-warmup-text); font-size: var(--icon-lg); }
    @media (prefers-reduced-motion: reduce) {
        .time-block.is-new, .build-slot.is-new, .levelup.is-celebrating .levelup-stars .material-symbols-outlined, .levelup.is-celebrating .levelup-pair .level-chip:last-child { animation: none; }
        .rest-breath { animation: none; transform: scale(1.6); }
        .rest-ring-left { transition: none; }
    }

    /* =========================================================================================
       ML-239: tablet and desktop. The phone layout above is untouched; everything here is added
       on top from 700px up. The rules are in specs/foundations/layout.md:
         - the ☰ menu becomes a rail down the left that folds to icons (specs/components/menu-rail.md)
         - a page is a card: narrow for forms, steps and reading; wide for Home, playing and lists
         - side-by-side groups stack by themselves when there isn't room (no extra breakpoints)
         - a list page keeps its list on the left and opens what you tap on the right
           (specs/components/two-pane.md; set by applyPanes in app.js from 1000px up)
       ========================================================================================= */

    /* ML-484: the brand's small mark - beside the app's name at the top of Home (phone) and in the head of the
       menu rail (specs/components/brand-mark.md). The picture is the brand's own artwork; only its size and
       corners are ours. Until the brand is known for the first time (public/brand.js sets data-brand-ready on
       <html>) the sign-in picture, the mark and the name are held back, so nobody sees one name then another. */
    .brand-mark { flex: 0 0 auto; width: var(--icon-xl); height: var(--icon-xl); margin-right: var(--space-2); border-radius: var(--radius-sm); }
    html:not([data-brand-ready]) body.app-shell :is(.splash-image, .brand-mark, .nav-rail-name) { visibility: hidden; }

    /* On a phone the side-by-side groups are not boxes at all, so those screens are exactly as they were. */
    .home-main, .home-tools, .play-side, .play-main { display: contents; }
    .nav-rail-head { display: none; }
    .history-item.pane-selected { border-left-color: var(--primary-action); background: var(--primary-action-tint); }

    @media (min-width: 700px) {
        /* --- the frame (the app page only: body.app-shell) --- */
        body.app-shell { align-items: stretch; --rail-w: var(--rail-width); }
        body.app-shell.rail-folded { --rail-w: var(--rail-width-folded); }
        body.app-shell #mainContainer { max-width: none; width: auto; margin-left: var(--rail-w); }
        body.app-shell :is(.top-bar, .session-bar, .offline-bar) { max-width: none; }
        body.app-shell .top-bar { padding: var(--space-2) var(--space-6); min-height: var(--rail-head-height); }
        body.app-shell .top-btn-burger { display: none; }
        body.app-shell .top-bar > .brand-mark { display: none; } /* ML-484: the rail's head carries the mark and the name */
        /* Back is a button you can see is a button: a square that lights up under the mouse (owner, 7 Oct 2026) */
        body.app-shell .top-btn-back { justify-content: center; width: var(--touch-target); height: var(--touch-target); padding: 0 0 var(--space-0-5); margin: 0 var(--space-3) 0 calc(-1 * var(--space-2)); border-radius: var(--radius-md); transition: background var(--duration-fast) ease; }
        body.app-shell .top-btn-back:hover { background: var(--primary-action-tint); }
        body.app-shell .nav-menu-backdrop { display: none; }
        body.app-shell .flow-edit-sticky-bar-wrap { left: var(--rail-w); width: auto; right: 0; }
        body.app-shell .flow-edit-sticky-bar { max-width: var(--page-max-narrow); }
        body.app-shell .toast { left: calc(50% + var(--rail-w) / 2); }

        /* --- a page is a card on the page colour: narrow unless it is one of the wide kinds --- */
        body.app-shell .container { width: calc(100% - 2 * var(--space-5)); max-width: var(--page-max-narrow); margin: var(--space-5) auto; padding: var(--space-6); border-radius: var(--radius-xl); box-shadow: var(--shadow-md); min-height: 0; }
        body.app-shell .container:has(> :is(#mainView, #flowPlayView):not(.hidden-group)) { max-width: var(--page-max-wide); }
        body.app-shell .container.two-pane { max-width: var(--page-max-panes); }

        /* --- the menu rail: the same menu, always there (.rail-demo shows its look on Admin -> Design) --- */
        body.app-shell #burgerDropdown { display: flex; position: fixed; top: 0; bottom: 0; left: 0; right: auto; width: var(--rail-w); max-height: none; }
        .rail-demo { width: var(--rail-width); }
        .rail-demo.is-folded { width: var(--rail-width-folded); }
        .rail-demo .nav-menu { display: flex; position: static; width: auto; }
        :is(body.app-shell, .rail-demo) .nav-menu { flex-direction: column; min-width: 0; max-width: none; box-sizing: border-box; padding: var(--space-2); border: none; border-right: 1px solid var(--input-border); border-radius: 0; box-shadow: none; }
        :is(body.app-shell, .rail-demo) .nav-rail-head { display: flex; align-items: center; gap: var(--space-2); min-height: var(--touch-target); padding: 0 0 var(--space-2) var(--space-2); }
        .nav-rail-head .brand-mark { margin-right: 0; }
        .nav-rail-name { flex: 1 1 auto; min-width: 0; font-size: var(--font-base); font-weight: var(--font-weight-bold); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .nav-rail-fold { flex: 0 0 auto; display: grid; place-items: center; width: var(--touch-target); height: var(--touch-target); padding: 0; border: none; border-radius: var(--radius-md); background: none; color: var(--label-color); cursor: pointer; }
        .nav-rail-fold:hover { background: var(--primary-action-tint); color: var(--text-color); }
        :is(body.app-shell, .rail-demo) .nav-menu .nav-panel { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
        :is(body.app-shell, .rail-demo) .nav-menu .nav-panel.hidden-group { display: none; }
        :is(body.app-shell, .rail-demo) .nav-menu .dropdown-item.nav-item:not(.hidden-group) { border-bottom: none; border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); }
        :is(body.app-shell, .rail-demo) .nav-menu .nav-divider { margin: var(--space-2); }
        :is(body.app-shell, .rail-demo) .nav-menu .nav-divider:has(+ [data-act="logout"]) { margin-top: auto; }
        :is(body.app-shell, .rail-demo) .nav-menu .nav-section-title { padding: var(--space-1) var(--space-3); }

        /* the Tools panel in the rail: two to a row, so the icons and names are a size you can read (owner, 7 Oct 2026) */
        :is(body.app-shell, .rail-demo) .nav-tools { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); padding: var(--space-1) var(--space-1) var(--space-3); }
        :is(body.app-shell, .rail-demo) .nav-tool { gap: var(--space-2); padding: var(--space-3) var(--space-1); }
        :is(body.app-shell, .rail-demo) .nav-tool .tool-icon-svg { width: var(--icon-xl); height: var(--icon-xl); }
        :is(body.app-shell, .rail-demo) .nav-tool .material-symbols-outlined { font-size: var(--icon-xl); }
        :is(body.app-shell, .rail-demo) .nav-tool-label { font-size: var(--font-sm); }

        /* folded to icons: the names are still there for a screen reader, and show as a tip on hover */
        :is(body.app-shell.rail-folded, .rail-demo.is-folded) .nav-rail-head { flex-direction: column; padding: var(--space-2) 0; justify-content: center; } /* the mark over the button */
        :is(body.app-shell.rail-folded, .rail-demo.is-folded) .nav-rail-head .brand-mark { margin-right: 0; }
        :is(body.app-shell.rail-folded, .rail-demo.is-folded) :is(.nav-rail-name, .nav-section-title, .nav-item-chevron, .nav-item-meta) { display: none; }
        :is(body.app-shell.rail-folded, .rail-demo.is-folded) .nav-menu .nav-item-text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } /* token-audit-ignore: standard screen-reader-only clip pattern */
        :is(body.app-shell.rail-folded, .rail-demo.is-folded) .nav-menu .dropdown-item.nav-item:not(.hidden-group) { justify-content: center; padding: var(--space-2) 0; }

        /* --- Home: the greeting and the big button on one side, favourite tools on the other --- */
        body.app-shell #mainView { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--pane-col-min)), 1fr)); column-gap: var(--space-8); align-items: start; }
        body.app-shell :is(.home-main, .home-tools, .play-side, .play-main) { display: block; min-width: 0; }
        body.app-shell .home-tools .tools-title { margin-top: 0; }
        body.app-shell .tool-icon-row { grid-template-columns: repeat(auto-fill, minmax(var(--tile-col-min), 1fr)); }
        body.app-shell .stats-page .dashboard-grid { grid-template-columns: repeat(auto-fill, minmax(var(--stat-col-min), 1fr)); }

        /* --- playing a piece: the tuner, where you are and the controls on one side, the piece on the other --- */
        body.app-shell #flowPlayView { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--play-col-min)), 1fr)); column-gap: var(--space-8); align-items: start; }
        body.app-shell .play-main > .section-title:first-child { margin-top: 0; }

        /* --- two panes: the list stays on the left, what you tapped opens on the right
               (.two-pane is only ever set on the app page's card, or on an Admin -> Design example) --- */
        .container.two-pane, .two-pane-demo { display: grid; grid-template-columns: var(--pane-list-width) minmax(0, 1fr); column-gap: var(--space-6); align-items: start; }
        :is(.container.two-pane, .two-pane-demo) > * { grid-column: 2; min-width: 0; }
        :is(.container.two-pane, .two-pane-demo) > .pane-list { grid-column: 1; grid-row: 1 / span 4; align-self: stretch; padding-right: var(--space-6); border-right: 1px solid var(--input-border); }
        .container.two-pane > .pane-list { min-height: 60vh; }
        :is(.container.two-pane, .two-pane-demo)::after { content: attr(data-pane-hint); grid-column: 2; align-self: center; justify-self: center; padding: var(--space-10) var(--space-5); color: var(--label-color); text-align: center; }
        :is(.container.two-pane, .two-pane-demo).has-detail::after { content: none; }
        .pane-list .section-title:first-child { margin-top: 0; }
    }
