
    /* 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.5c25278c.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.3100e775.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.34b9c504.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.20881b15.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.d4b7113f.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.de2c73d9.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.56764a27.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.ebfa2276.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.84050809.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.0441bc21.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.b534a0b8.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); font-weight: var(--font-weight-bold); line-height: 20px; text-align: center; }
    /* ML-557: a run of bars on "Bar by bar" - where it started, an arrow, where it is now */
    .level-change { display: inline-flex; align-items: center; gap: var(--space-1); flex-shrink: 0; color: var(--label-color); }
    /* ML-557: how far a practice list's weekly plan has come - the blocks done out of done + still to do (--pct) */
    .plan-track { display: block; height: var(--space-2); border-radius: var(--radius-pill); background: var(--input-bg); border: 1px solid var(--control-border); overflow: hidden; }
    .plan-track-fill { display: block; height: 100%; width: var(--pct, 0%); background: var(--primary-action); }
    .notifications-toolbar { display: flex; justify-content: flex-end; gap: var(--space-3); margin-bottom: var(--space-4); }
    /* ML-556: a notification's × clears it out of the list for good - bare (low emphasis), top right, a full
       touch target; the notification leaves room for it so the title never runs underneath */
    .notification-row { position: relative; }
    .notification-row.has-clear .notification-item { padding-right: calc(var(--touch-target) + var(--space-1)); }
    /* ML-556: a cleared notification under "Show cleared" - a dashed edge says it's out of the list; Restore puts it back */
    .notification-item.is-cleared { border-style: dashed; cursor: default; }
    .notification-restore { display: inline-flex; align-items: center; min-height: var(--touch-target); margin-top: var(--space-1); padding: 0; border: none; background: none; color: var(--primary-action-strong); font: inherit; font-weight: var(--font-weight-bold); text-decoration: underline; cursor: pointer; }
    .notification-clear-btn { position: absolute; top: var(--space-1); right: var(--space-1); width: var(--touch-target); height: var(--touch-target); padding: 0; border: none; border-radius: var(--radius-circle); background: transparent; color: var(--label-color); display: flex; align-items: center; justify-content: center; cursor: pointer; }
    .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); }
    /* ML-534: a band's logo - an organiser's picture, shown whole (never cropped) on a plain ground */
    .band-logo { flex: none; display: block; object-fit: contain; border-radius: var(--radius-sm); background: var(--container-bg); border: 1px solid var(--input-border); box-sizing: border-box; }
    .band-logo-sm { width: var(--band-logo-sm); height: var(--band-logo-sm); }
    .band-logo-lg { width: var(--band-logo-lg); height: var(--band-logo-lg); }
    /* ML-659: in Edit the band the logo is the button - a pencil badge (.info-row-edit-icon) on its corner, or a +
       on the empty box; a tap opens a small .dropdown-menu just under it (.band-logo-menu) */
    .band-logo-edit { position: relative; width: fit-content; }
    .band-logo-btn { position: relative; display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer; color: var(--label-color); }
    .band-logo-btn img { width: 100%; height: 100%; object-fit: contain; border-radius: inherit; }
    .band-logo-btn .band-logo-empty-icon { font-size: var(--icon-xl); }
    .band-logo-badge { position: absolute; right: calc(-1 * var(--space-3)); bottom: calc(-1 * var(--space-3)); }
    .band-logo-menu { left: 0; right: auto; top: calc(100% + var(--space-4)); width: max-content; }
    .band-row-main { display: flex; align-items: center; gap: var(--space-3); min-width: 0; } /* a My bands row: the logo (if any) beside the name */
    /* 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); }
    /* 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; }

    /* ML-595: the one Filter on every list (filters.js has the rules, app.js's FILTERS draws it). The button says how
       many filters are on and how many things show; the filters on are chips under it (.filter-chip), each with an x
       that takes just that one off - they wrap, so a long band name never runs off the screen. The pop-up lists every
       way of filtering: a group's choices are pills that wrap (.filter-panel-choices). See specs/components/filter-bar.md. */
    .filter-bar { margin: var(--space-3) 0 var(--space-4); }
    .filter-bar-row { display: flex; align-items: center; gap: var(--space-2); }
    .filter-bar .metroBlk-ctrl-value-btn { flex: 1 1 auto; min-width: 0; flex-direction: row; justify-content: flex-start; gap: var(--space-3); padding: 0 var(--space-4); text-align: left; }
    .filter-bar-row .btn-text { flex: 0 0 auto; width: auto; }
    .filter-bar-text { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; }
    .filter-bar-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
    .filter-chip { flex-shrink: 1; min-width: 0; white-space: normal; text-align: left; padding-right: var(--space-2); }
    .filter-chip .material-symbols-outlined, .filter-panel-choices .filter-pill .material-symbols-outlined { font-size: var(--font-md); }
    .filter-panel-choices { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-2); }
    .filter-panel-choices .filter-pill { flex-shrink: 1; white-space: normal; text-align: left; }

    .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. */

    .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; }
    /* ML-650: an icon is a word the icon font turns into a picture - until the font is here (display-prefs.js sets
       data-icons on <html> while it loads, and "none" if it never comes), each icon keeps its one-icon space and
       shows nothing, never its name. Visibility, not colour: many icons set their own colour. */
    html[data-icons] .material-symbols-outlined { visibility: hidden; width: 1em; overflow: hidden; }
    /* 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; }


    /* 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. */
    /* ML-687: as wide as its words need, up to the screen less a margin each side (--toast-max-width on a big one). With
       left: 50% and no width, a fixed box shrinks to the half of the screen right of the middle, so its words wrapped there. */
    .toast { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); width: max-content; max-width: min(var(--toast-max-width), calc(100vw - 2 * var(--space-4))); box-sizing: border-box; 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; }
    /* ML-594: closing, a toast drops straight down off the screen (closeToast in app.js takes .show off when it's
       done). Far enough from either resting place - 30px up, or --toast-above-bottom-bar over a pinned bar. */
    .toast.is-leaving { transform: translate(-50%, calc(100% + var(--toast-above-bottom-bar))); transition: transform var(--duration-base) var(--ease-standard); pointer-events: none; }
    .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-691: a value box across the whole grid row, its answer wrapping onto a second line when it needs to (a long
       instrument name in Warm-ups' and Scales' settings); the boxes after it keep their own size, two to a row */
    .metro-transport-grid .metroBlk-ctrl-value-btn.is-wide { grid-column: 1 / -1; height: auto; min-height: 60px; padding-top: var(--space-2); padding-bottom: var(--space-2); }
    .metroBlk-ctrl-value-btn.is-wide strong { white-space: normal; overflow-wrap: anywhere; text-align: center; }
    /* 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 */
    /* ML-685: the pop-up has its title beside the X now, so the row no longer keeps clear of it */
    .timer-inline-controls { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); margin: var(--space-2) 0 0 0; }
    /* .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; }
    /* ML-107: closed, its buttons are out of reach of the keyboard and a screen reader too - it now sits (closed)
       on screens like the 30-second rest, where an unseen button would be a surprise. */
    .metroBlk-mini-tuner:not(.metroBlk-mini-tuner-open) { visibility: hidden; }
    .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-entry-choice:disabled:not([aria-busy="true"]) { opacity: var(--opacity-disabled); cursor: not-allowed; } /* add-piece spec: none left to use (not while busy reading) */
    .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; }
    /* ML-550: a value too long for the box (the repeat's 101–124) - smaller, on two lines */
    .metroBlk-ctrl-value-btn strong.is-two-line { font-size: var(--font-sm); line-height: var(--line-height-tight); }
    /* 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; }
    /* ML-529: the heading over the first bar of each movement of a piece in movements (Rehearse) - across the
       whole row of tiles in every layout, closer to its tiles than a page section-title. */
    .flow-movement-head { grid-column: 1 / -1; margin: var(--space-3) 0 0 0; }
    .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); }
    /* ML-626: the bar playing, last on the line - in the body text colour so it reads at a glance */
    .flow-bar-now { color: var(--text-color); font-weight: var(--font-weight-bold); }
    /* ML-638: playing along, the count-down in the bar line - big, as the ring's number is, but on one line that fits the row (the cog sets its height), so nothing moves */
    .flow-bar-count { font-size: var(--font-2xl); line-height: var(--line-height-tight); font-weight: var(--font-weight-bold); color: var(--text-color); font-variant-numeric: tabular-nums; vertical-align: middle; margin: 0 var(--space-1); }
    /* 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; }
    /* ML-602: a pop-up's title never runs under its X - a long one ("My range - B♭ Euphonium (4 valves)") wraps before it.
       The X is 48px wide, --space-3 in from the edge, over the --space-5/6 padding: --space-9 keeps the title clear of it. */
    .modal-close-x + h2 { padding-right: var(--space-9); }

    /* 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); }
    .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); }
    /* ML-627: what a piece has to play along to - its videos and recordings as small icons ("x2" for more than one), at
       the end of its time-and-bars line on the play screen. Read only, in the line's own text (the owner, 28 Oct 2026:
       choosing is the cog on the card). */
    .flow-media-have { display: inline-flex; align-items: center; gap: var(--space-1); vertical-align: middle; }
    .flow-media-have .material-symbols-outlined { font-size: var(--icon-sm); }
    /* ML-635: under a recording with nothing marked, "Timing of the video still to be confirmed" - one line, the help text's size, and for whoever can change the piece itself the link to Edit -> Media */
    .flow-along-hint-link { display: inline-flex; align-items: center; min-height: var(--touch-target); padding: 0; border: none; background: none; font: inherit; font-size: var(--font-sm); color: var(--link-color); text-decoration: underline; text-align: left; cursor: pointer; }
    .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: "Import from MusicXML" (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; }

    /* The play screen's card (ML-166 made it the first of a swipeable carousel; ML-627 made it the only one). */
    .flow-media-slide { 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-530: while Rehearse's Movement value box shows, it sits on the left and the layout switch on the right */
    .flow-play-layout-bar:has(> .flow-movement-btn:not(.hidden-group)) { justify-content: space-between; }

    /* 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); }
    /* ML-606: with big bar numbers ("Bars 473–487" and a mark of 473) the header ran out of room on a phone - the title never
       shrank, so the mark was squeezed to "4…" and the next arrow and the X were pushed off the edge. The mark keeps its whole
       width; the title shrinks and, if it must, wraps ("Bars" over "473–487"). */
    .flow-bar-popup-title { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; }
    .flow-bar-popup-title .flow-block-name { white-space: normal; overflow-wrap: anywhere; line-height: var(--line-height-tight); } /* anywhere: only a range too long even on its own line (4-digit bars) breaks inside it */
    .flow-bar-popup-header .flow-block-mark-box { flex-shrink: 0; }
    .flow-bar-popup-pos { font-size: var(--font-xs); color: var(--label-color); }
    .flow-bar-popup-close { margin-left: var(--space-2); } /* ML-606: was --space-4; with the gap it is still --space-4 clear of the next arrow */
    /* 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); }
    /* ML-158: the Tuning fork - the range picker's stave with sound waves either side of it, which light and
       ripple outwards while the note sounds (still, and simply lit, with reduced motion). */
    .fork { display: grid; gap: var(--space-3); }
    .fork-sound { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--space-2); }
    .fork-sound .range-stave-btn { margin: 0; }
    .fork-waves { display: flex; align-items: center; gap: var(--space-0-5); color: var(--input-border); }
    .fork-waves-left { transform: scaleX(-1); }
    .fork-wave { width: var(--space-2); border: 3px solid transparent; border-right-color: currentColor; border-radius: var(--radius-circle); }
    .fork-wave-1 { height: var(--space-5); }
    .fork-wave-2 { height: var(--space-7); }
    .fork-wave-3 { height: var(--space-9); }
    .fork.is-sounding .fork-waves { color: var(--primary-action-strong); }
    .fork.is-sounding .fork-wave { animation: fork-wave var(--duration-pulse) ease-in-out infinite; }
    .fork.is-sounding .fork-wave-2 { animation-delay: var(--duration-fast); }
    .fork.is-sounding .fork-wave-3 { animation-delay: var(--duration-base); }
    @keyframes fork-wave { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
    .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-586: the title (a button - it opens the list) beside the one Settings button */
    .warmups-head { display: flex; align-items: flex-start; gap: var(--space-3); padding: 0 var(--space-2) var(--space-2); }
    .warmups-heading { flex: 1; min-width: 0; }
    .warmups-title-btn { display: block; width: 100%; min-height: var(--touch-target); padding: 0; border: none; background: none; color: var(--text-color); font: inherit; text-align: left; cursor: pointer; }
    .warmups-sub { display: flex; align-items: center; gap: var(--space-0-5); margin-top: var(--space-0-5); font-size: var(--font-sm); font-weight: var(--font-weight-normal); color: var(--label-color); }
    .warmups-settings-btn { flex: none; min-width: 0; padding: 0 var(--space-2); }
    /* Every warm-up at the same music size: each line is drawn at its own width (app.js scales the svg), never
       stretched to fill the screen; a line wider than the screen shrinks to fit. Scrolled to sits under the top bar. */
    .warmups-staff { display: flex; flex-direction: column; gap: var(--space-2); }
    .warmups-row { scroll-margin-top: calc(var(--header-h, var(--space-0)) + var(--space-3)); }
    .warmups-row svg { display: block; max-width: 100%; height: auto; }
    .warmups-staff .is-now { fill: var(--primary-action-strong); }
    /* The bar at the bottom of the screen: it stays in view while the page scrolls (sticky, so on a short warm-up
       it sits under the music), with how far through the list, what's playing and what's next, then the buttons. */
    .warmups-dock { position: sticky; bottom: var(--space-0); z-index: var(--z-sticky); display: flex; flex-direction: column; gap: var(--space-2); margin: var(--space-0) calc(-1 * var(--space-4)); padding: var(--space-3) var(--space-4) var(--space-4); background: var(--container-bg); border-top: 1px solid var(--input-border); box-shadow: var(--shadow-top); }
    .warmups-progress { height: var(--space-1); border-radius: var(--radius-pill); background: var(--input-border); overflow: hidden; }
    .warmups-progress-fill { width: var(--pct, 0%); height: 100%; background: var(--primary-action); }
    .warmups-status { display: flex; justify-content: space-between; gap: var(--space-3); min-height: 1.4em; margin: 0; font-size: var(--font-sm); color: var(--label-color); }
    .warmups-status span:first-child { color: var(--text-color); font-weight: var(--font-weight-semibold); }
    .warmups-status span:last-child { text-align: right; }
    .warmups-dock-row { display: flex; gap: var(--space-2); }
    .warmups-dock-row > button { padding: var(--space-0) var(--space-1); }
    .warmups-dock-side { flex: 0 0 calc(var(--touch-target) * 1.6); }
    .warmups-dock-main { flex: 1; flex-direction: row; gap: var(--space-2); font-size: var(--font-md); font-weight: var(--font-weight-bold); }
    /* The count-in, in Play's place: the beats left, big, and a dot per beat that lights as it's heard (tap: stop). */
    .warmups-countin { flex: 1; height: 60px; display: flex; align-items: center; justify-content: center; gap: var(--space-3); border: 2px solid var(--primary-action); border-radius: var(--radius-md); background: var(--primary-action-tint); color: var(--text-color); cursor: pointer; }
    .warmups-countin strong { font-size: var(--font-3xl); line-height: var(--line-height-tight); }
    .warmups-countin-words { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1); font-size: var(--font-sm); font-weight: var(--font-weight-semibold); }
    .warmups-countin-dots { display: flex; gap: var(--space-1); }
    .warmups-countin-dot { width: var(--space-2); height: var(--space-2); border-radius: var(--radius-circle); border: 2px solid var(--primary-action-strong); box-sizing: border-box; }
    .warmups-countin-dot.is-done { background: var(--primary-action-strong); }
    /* Settings: two headed groups of value boxes, two across, and the on/off rows between them */
    .warmups-settings-head { margin: var(--space-4) 0 var(--space-2); font-size: var(--font-base); color: var(--label-color); }
    .warmups-settings-grid { margin: var(--space-2) 0 var(--space-3); }
    .warmups-settings-row { min-height: var(--touch-target); margin-bottom: var(--space-3); }
    /* 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; opacity: var(--opacity-disabled); } /* ML-627: greyed, as any disabled control */
    /* 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); }
        /* ML-701 (owner, 4 Nov 2026): on a phone the ☰ menu hangs from the top bar - flush with the screen's right edge,
           its top on the bar's bottom edge, only the bottom-left corner rounded. It hung from the ☰ button, a gap in
           from the right and a few pixels over the bar. As tall as it needs; it scrolls if longer than the screen. */
        .top-bar { position: relative; }
        .top-bar > .pos-relative:has(#burgerDropdown) { position: static; }
        #burgerDropdown.nav-menu { top: 100%; right: 0; max-width: 100vw; max-height: calc(100dvh - 100%); border-top: none; border-right: none; border-radius: 0 0 0 var(--radius-md); }
        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); }
        .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 / ML-603 - reduce motion: 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. html[data-motion="reduce"] is the member's Reduce motion switch or the
       device's own setting (display-prefs.js sets it for both); the media query is the same rule for the pages
       that don't run display-prefs.js (the admin panel, the style guide). */
    @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%); }
    }
    :root[data-motion="reduce"] *, :root[data-motion="reduce"] *::before, :root[data-motion="reduce"] *::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; }
    :root[data-motion="reduce"] :is(.metro-dot-note.lit, .metro-dot-note.accent.lit, .metro-dot-sub.lit) { transform: translate(-50%, -50%); }
    /* Countdowns still count down: the toast's bar steps once a second (--countdown-steps, setToastCountdown)
       instead of gliding - cut to nothing, it would empty the moment the toast showed. */
    :root[data-motion="reduce"] .toast-countdown-bar.is-running { transition-duration: var(--countdown-ms) !important; transition-timing-function: steps(var(--countdown-steps), end); }
    /* What shows the music keeps moving (owner, 24 Oct 2026): the metronome's follow-the-beat pan keeps its
       steady, beat-long glide - it is the tool, not decoration. */
    :root[data-motion="reduce"] .metro-display-content { transition-duration: var(--scroll-dur, 0s) !important; }

    /* =========================================================================================
       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 one for a row
       of 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); }

    /* About: the bulleted change list under each release. */
    .release-change-list { margin: var(--space-1) 0 0 0; padding-left: var(--space-5); }

    /* =========================================================================================
       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; }
    /* ML-581: the band leader's summary - a column per bar in .level-map's rows of 10 (specs/components/band-summary.md).
       Levels 1-5 run bottom to top, a fifth each: the shading from the lowest Level to the highest (--lo, --hi), a
       line at the average (--avg) - set at run time. A bar without 3 players' Levels is a dashed, empty column. */
    .band-summary-cell { position: relative; height: var(--band-summary-cell-height); border-radius: var(--radius-2xs); background: var(--input-bg); border: 1px solid var(--level-unset-border); box-sizing: border-box; overflow: hidden; }
    .band-summary-cell.is-empty { background: transparent; border-style: dashed; }
    .band-summary-range { position: absolute; inset-inline: 0; bottom: calc((var(--lo, 1) - 1) * 20%); top: calc((5 - var(--hi, 1)) * 20%); border-block: 1px solid var(--level-unset-border); }
    .band-summary-avg { position: absolute; inset-inline: 0; bottom: calc((var(--avg, 1) - 0.5) * 20%); height: var(--band-summary-line); background: var(--text-color); }
    .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; } }
    /* ML-608: the number is how many bars (or scales) - two or three digits need a wider part before they show */
    @container (max-width: 22px) { .level-bar-num[data-digits="2"] { display: none; } }
    @container (max-width: 30px) { .level-bar-num:is([data-digits="3"], [data-digits="4"]) { display: none; } }
    .flow-card .level-legend:last-child { margin-bottom: 0; } /* ending a card: the card's own padding is the space. Ending a list (a practice list's pieces) it keeps its margin, so the next button isn't on top of it */
    .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); 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-0-5); } /* tight, so the key fits one line on a 360px phone */
    .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; }
    /* ML-495: a banner for a planned outage - across the top of every screen while its notice is live, and of the
       sign-in screen. Blue (the "scheduled" pair) so it isn't mistaken for the amber offline bar or an error.
       The words are a button opening the whole message; the X hides it until next time. */
    .notice-banner { width: 100%; max-width: var(--app-max-width); box-sizing: border-box; display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-1) 0 var(--space-5); min-height: var(--touch-target); background: var(--status-blue-bg); color: var(--status-blue-fg); font-size: var(--font-sm); font-weight: var(--font-weight-semibold); }
    .notice-banner-icon { flex-shrink: 0; }
    .notice-banner-text { flex: 1; min-width: 0; min-height: var(--touch-target); padding: 0; border: none; background: none; color: inherit; font: inherit; text-align: left; text-decoration: underline; cursor: pointer; }
    .notice-banner-close { flex-shrink: 0; width: var(--touch-target); height: var(--touch-target); padding: 0; border: none; border-radius: var(--radius-circle); background: none; color: inherit; display: flex; align-items: center; justify-content: center; cursor: pointer; }
    .splash-screen .notice-banner { position: absolute; top: 0; left: 0; right: 0; max-width: none; }
    .splash-screen:has(.notice-banner:not(.hidden-group)) { padding-top: calc(var(--touch-target) + var(--space-5)); } /* the sign-in picture starts under it */
    .notice-banner-full .notification-body { display: block; -webkit-line-clamp: unset; }
    .notice-banner-full h2 { padding-right: var(--touch-target); } /* a long title stops short of the X */
    /* ML-564: a new version is ready - ML-667: one line at every width: the words, Reload, the X. */
    .update-banner { padding-top: var(--space-1); padding-bottom: var(--space-1); } /* ML-647: room above and below Reload */
    .update-banner-title { flex: 1; min-width: 0; }
    /* ML-647: Reload is filled - the strip's own colours the other way round, so it reads as the button (and dark mode
       turns it round again by itself) */
    .update-banner-reload { flex-shrink: 0; min-height: var(--touch-target); padding: 0 var(--space-4); background: var(--status-blue-fg); border: 1px solid var(--status-blue-fg); border-radius: var(--radius-sm); color: var(--status-blue-bg); font: inherit; font-weight: var(--font-weight-bold); cursor: pointer; }
    .update-banner-reload:hover { text-decoration: underline; }
    #noticeBannerModal { z-index: var(--z-over-splash); } /* opened from the sign-in screen too, which sits on top of everything */
    .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; }
    /* ML-535: Prepare's steps have no step row above the question, so it gets room of its own under the top bar
       (not :first-child - the mini tuner is moved in ahead of it, hidden until it's opened) */
    :is(#prepareRunView, #levelsPaintView) > .step-question { margin-top: var(--space-4); }
    /* 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, last 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.
       ML-460: a repeat row ends in a button - how many endings it has - that opens them in a pop-up. */
    .outline-row-repeat { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--space-1); }
    .outline-row-repeat .outline-cell-btn { padding: 0 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; }
    /* a list row in the column (set-up's instruments): the column's gap spaces it, not the row's own margin */
    .outline > .history-item.settings-link { margin-bottom: 0; }
    /* 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); }
    /* ML-677: a whole run-through longer than a block, as wide as its 5-minute blocks (--block-span, up to a row of 6) */
    .kind-strip .kind-block.is-long { grid-column: span var(--block-span, 2); }
    /* 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; }
    /* ML-608: step 3 (What goes in each block?) - every block the same .history-item.settings-link row, its kind's colour
       down the edge and on its name (the -text colour: 4.5:1 on the row). .history-item.: a button row's own border colour
       (button.history-item) would win otherwise. specs/components/practice-steps.md. */
    .history-item.kind-edge-warmup { border-left-color: var(--kind-warmup); }
    .kind-edge-warmup .settings-link-title { color: var(--kind-warmup-text); }
    .history-item.kind-edge-scales { border-left-color: var(--kind-scales); }
    .kind-edge-scales .settings-link-title { color: var(--kind-scales-text); }
    .history-item.kind-edge-skills { border-left-color: var(--kind-skills); }
    .kind-edge-skills .settings-link-title { color: var(--kind-skills-text); }
    .history-item.kind-edge-pieces { border-left-color: var(--kind-pieces); }
    .kind-edge-pieces .settings-link-title { color: var(--kind-pieces-text); }
    .history-item.kind-edge-theory { border-left-color: var(--kind-theory); }
    .kind-edge-theory .settings-link-title { color: var(--kind-theory-text); }
    /* the line of what goes in wraps rather than being cut off - it is the only place it is said on the page */
    :is(.kind-edge-warmup, .kind-edge-scales, .kind-edge-skills, .kind-edge-pieces, .kind-edge-theory) .settings-link-sub { white-space: normal; }
    .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); }
    /* ML-684: the order of a list - a small drop-down at the right above it ("Show A-Z"). An order is a minor thing, so it
       takes none of the room a setting's value box and pop-up do. See specs/components/form-field.md. */
    .list-order { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-3); }
    /* ML-690: a practice list's play order pop-up - each piece a row: its place, its name, then up and down, a tap each
       (nothing is dragged, so there is no gesture to give a tap way to) */
    .play-order-list { list-style: none; margin: var(--space-3) 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
    .play-order-row { display: flex; align-items: center; gap: var(--space-2); border: 1px solid var(--input-border); border-radius: var(--radius-md); padding: var(--space-1) var(--space-1) var(--space-1) var(--space-3); }
    .play-order-num { min-width: var(--space-6); font-weight: var(--font-weight-bold); }
    .play-order-title { flex: 1; min-width: 0; overflow-wrap: anywhere; }
    .play-order-move { width: var(--touch-target); height: var(--touch-target); flex-shrink: 0; border-radius: var(--radius-md); border: 1px solid var(--control-border); background: var(--input-bg); color: var(--text-color); display: flex; align-items: center; justify-content: center; cursor: pointer; }
    .play-order-move:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
    .list-order select { width: auto; min-height: var(--touch-target); padding: var(--space-1) var(--space-3); font-size: var(--font-sm); }
    .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; }
    /* ML-542: the big count-in - the rest ring on the rest panel's colour, with the beats left in it, covering the
       controls (Warm-ups' and Scales' metronome card, the play screen's metronome) while the count-in clicks. It
       never covers the music: the stave and the bars sit outside its box. Taps go through to the buttons under it. */
    .scales-metronome-card, .flow-media-slide { position: relative; }
    .count-in-cover { position: absolute; inset: 0; z-index: var(--z-sticky); display: grid; place-items: center; border-radius: inherit; background: var(--rest-bg); pointer-events: none; }
    .count-in-cover .rest-ring-num strong { font-size: var(--font-3xl); }
    .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; }
    /* ML-669: the start of Prepare - the piece's name on the page, one line, an example and two short rows */
    .path-title { font-size: var(--font-lg); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); margin: var(--space-4) 0 var(--space-1); text-wrap: balance; }
    .prepare-lead { font-size: var(--font-md); font-weight: var(--font-weight-bold); line-height: var(--line-height-snug); margin: 0 0 var(--space-4); }
    .prepare-example { display: grid; gap: var(--space-3); margin-bottom: var(--space-1); }
    .prepare-example .paint-bar { cursor: default; }
    .prepare-key { display: flex; align-items: center; gap: var(--space-3); margin: 0; font-size: var(--font-base); line-height: var(--line-height-snug); }
    .prepare-key-line { flex-shrink: 0; width: var(--icon-xl); height: var(--space-1); background: var(--control-border); }

    /* 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(6, minmax(0, 1fr)); gap: var(--space-2); } /* ML-536: 6 across on a phone, like the tools. ML-670: room for the lines */
    @media (min-width: 700px) { .paint-bars { grid-template-columns: repeat(auto-fill, minmax(var(--touch-target), 1fr)); } } /* a wider page fits more */
    .paint-bar { position: relative; box-sizing: border-box; width: 100%; 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); }
    /* ML-670: bars played together are joined by a line through the gap. At the end of a row it runs into the page's
       gutter and picks up on the next row. --space-0-5 is the bar's own border. */
    .paint-bar.joins-prev::before, .paint-bar.joins-next::after { content: ''; position: absolute; top: 50%; width: var(--space-2); height: var(--space-1); margin-top: calc(var(--space-1) / -2); background: var(--control-border); }
    .paint-bar.joins-prev::before { left: calc(-1 * (var(--space-2) + var(--space-0-5))); }
    .paint-bar.joins-next::after { right: calc(-1 * (var(--space-2) + var(--space-0-5))); }
    /* A focus section that won't fit: a red border as well as red lines, so it shows as a shape and not only a colour */
    .paint-bar.is-too-long:not(.lv-0) { border-color: var(--danger-color); }
    .paint-bar.is-too-long::before, .paint-bar.is-too-long::after { background: var(--danger-color); }
    /* A split: the box's own left edge, the shape of a list row's, so it follows the corners. Dark on a light page and
       light in dark mode. It comes after is-too-long, so a split in a long section keeps its dark edge. */
    .paint-bar.is-cut-before:not(.lv-0) { border-left: 6px solid var(--text-color); }
    .paint-bar.is-range-start { box-shadow: var(--focus-ring); } /* ML-536: the Range tool's first bar, waiting for the last */
    .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); box-shadow: 0 0 0 var(--space-3) var(--container-bg); clip-path: inset(0 calc(-1 * var(--space-3))); } /* ML-670: the shadow covers the gutter, where the lines between bars run out at the end of a row */
    .paint-tools { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--space-1); } /* ML-536: five tools since ML-676 */
    .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:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; } /* Undo or Redo with nothing to do */
    .paint-actions { display: flex; gap: var(--space-2); } /* ML-670: Undo, Redo and Done on the last row */
    .paint-actions .paint-tool { flex-shrink: 0; width: var(--touch-target); padding: 0; }
    .paint-actions .btn-submit { flex: 1; min-width: 0; margin-bottom: 0; }
    .paint-tool-label { text-align: center; font-size: var(--font-xs); font-weight: var(--font-weight-bold); line-height: var(--line-height-tight); }
    .paint-pots { grid-template-columns: repeat(6, minmax(0, 1fr)); margin-bottom: 0; } /* ML-676: the five Levels and Remove */
    .paint-pot-remove { border: 2px dashed var(--level-unset-border); background: transparent; color: var(--text-color); } /* ML-676: looks like a bar with no level */
    .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); }
    .cut-bit .paint-tool { flex-shrink: 0; padding: var(--space-1) var(--space-3); } /* the toolbox's Split, which it picks */

    /* "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); }

    /* Reduce motion (A7 / ML-603): a new block or slot just appears, the Level-up stars show without the pop, the
       breathing circle stays still and the rest ring steps once a second */
    :root[data-motion="reduce"] :is(.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; }
    :root[data-motion="reduce"] .rest-breath { animation: none; transform: scale(1.6); }
    :root[data-motion="reduce"] .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)
       ========================================================================================= */

    /* The app'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 app's own artwork; only its size and corners
       are set here. */
    .brand-mark { flex: 0 0 auto; width: var(--icon-xl); height: var(--icon-xl); margin-right: var(--space-2); border-radius: var(--radius-sm); }

    /* 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; }
    /* Playing a piece: where you are and the controls stay pinned under the top bar (--header-h, measured by
       app.js) and the piece's tiles scroll beneath, the one that is playing brought to the top
       (flowKeepLitTileInView; --play-pin-h is the top bar's height plus the pinned part's, set by app.js). Not on a screen too
       short to leave room for the tiles. */
    @media (min-height: 600px) {
        #flowPlayView > .play-side { display: block; position: sticky; top: var(--header-h, var(--space-0)); z-index: var(--z-sticky); background: var(--container-bg); margin-top: calc(-1 * var(--space-3)); padding: calc(var(--space-3) * 2) 0 var(--space-2); } /* the tuner starts --space-3 above its place (its own negative margin): the top padding, taken back by the margin, keeps that inside the pinned box instead of under the top bar. ML-549: and --space-3 more, so the metronome / video switch isn't squeezed up against the top bar (or the session bar) */
    }
    #flowPlayTiles > * { scroll-margin-top: calc(var(--play-pin-h) + var(--space-2)); }
    .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, .notice-banner) { max-width: none; }
        /* ML-647: the strips run the full width, their words over the page in the middle */
        body.app-shell :is(.offline-bar, .notice-banner) { padding-left: max(var(--space-5), calc((100% - var(--app-max-width)) / 2 + var(--space-5))); padding-right: max(var(--space-1), calc((100% - var(--app-max-width)) / 2 + var(--space-1))); }
        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; } /* 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:not(.two-pane):has(> :is(#mainView, #flowPlayView):not(.hidden-group)) { max-width: var(--page-max-wide); } /* not beside a list: there the two-pane width below is the one that counts */
        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; }
        /* ML-590: the fold button first, flush with the menu's own edge - so it sits in the same place open and folded (centred in the folded rail) */
        :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) 0; }
        .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 */
        /* ML-590: folded, the head is just the button, where it was - the mark and the name go */
        :is(body.app-shell.rail-folded, .rail-demo.is-folded) :is(.nav-rail-head .brand-mark, .nav-rail-name, .nav-section-title, .nav-item-chevron, .nav-item-meta) { display: none; }
        /* ML-590: a count (Notifications, Administration) is a small badge on the icon's top right corner, so the icon stays centred */
        :is(body.app-shell.rail-folded, .rail-demo.is-folded) .nav-menu .nav-item:has(.notif-count) { position: relative; }
        :is(body.app-shell.rail-folded, .rail-demo.is-folded) .nav-menu .notif-count { position: absolute; top: var(--space-0-5); left: calc(50% + var(--space-1)); min-width: var(--space-4); margin: 0; padding: 0 var(--space-1); font-size: var(--font-2xs); line-height: var(--space-4); box-sizing: border-box; }
        :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; }

        /* --- ML-601: playing a piece from Rehearse (and, ML-627, My music), the list folded away (.play-wide, set by applyPanes from 1000px):
               the controls keep about one column's width, the piece's tiles get the other two - with more tiles across
               in every layout (small: as many as fit; detail: as many as fit; full: two when there is room) --- */
        body.app-shell .container.play-wide #flowPlayView { grid-template-columns: minmax(var(--play-col-min), 1fr) minmax(0, 2fr); }
        .container.play-wide #flowPlayTiles.metroBlk-tile-strip { grid-template-columns: repeat(auto-fill, minmax(var(--play-tile-min), 1fr)); }
        .container.play-wide #flowPlayTiles.flow-bar-grid-2 { grid-template-columns: repeat(auto-fill, minmax(var(--play-tile-detail-min), 1fr)); }
        .container.play-wide #flowPlayTiles.flow-bar-list-1 { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--play-col-min), 1fr)); align-items: start; }
    }
    /* ML-242: the public page (welcome.html) and the pricing page (pricing.html), read signed out. A summary a visitor
       takes in at a glance - dark bands top and bottom, cards that open for the detail (<details>, no script), the
       Level colours as the picture - designed with the owner on a Claude Design canvas (22 Oct 2026). Full-width bands
       with the content inside --page-max-public. specs/components/public-page.md */
    body.public-body { display: block; background: var(--container-bg); }
    .public-body main { display: block; }
    .public-wrap { max-width: var(--page-max-public); margin: 0 auto; padding-left: var(--space-5); padding-right: var(--space-5); box-sizing: border-box; }
    .public-band { background: var(--public-band-bg); color: var(--public-band-text); }
    .public-tint { background: var(--bg-color); }
    .public-section { padding-top: var(--space-10); padding-bottom: var(--space-8); }
    .public-body a:not([class]) { color: var(--link-color); }
    .public-band a:not([class]) { color: var(--public-band-accent); }
    .public-top { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4); flex-wrap: wrap; padding-top: var(--space-2); padding-bottom: var(--space-2); }
    .public-top-brand { display: inline-flex; align-items: center; min-height: var(--touch-target); color: var(--public-band-text); font-size: var(--font-md); font-weight: var(--font-weight-bold); text-decoration: none; }
    .public-top-links { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-1); }
    .public-band .public-top-links a { display: inline-flex; align-items: center; min-height: var(--touch-target); padding: 0 var(--space-3); color: var(--public-band-muted); font-size: var(--font-base); font-weight: var(--font-weight-semibold); text-decoration: none; border-radius: var(--radius-md); }
    .public-band .public-top-links a[aria-current="page"] { color: var(--public-band-text); text-decoration: underline; text-underline-offset: var(--space-1); }
    .public-top-links .public-top-login { margin-left: var(--space-1); color: var(--public-band-accent); border: 2px solid var(--public-band-accent); }
    .public-hero { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-9); padding-top: var(--space-8); padding-bottom: var(--space-10); }
    .public-hero-text { flex: 1 1 var(--pane-col-min); min-width: 0; }
    .public-kicker { margin: 0 0 var(--space-4); color: var(--public-band-accent); font-size: var(--font-base); font-weight: var(--font-weight-semibold); }
    .public-hero-title { color: inherit; margin: 0; font-size: var(--font-3xl); line-height: var(--line-height-tight); font-weight: var(--font-weight-bold); }
    .public-hero-sub { margin: var(--space-4) 0 var(--space-7); color: var(--public-band-muted); font-size: var(--font-xl); font-weight: var(--font-weight-semibold); line-height: var(--line-height-snug); }
    .public-hero-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-5); }
    .public-hero-note { color: var(--public-band-muted); font-size: var(--font-base); }
    .public-cta { display: inline-flex; align-items: center; justify-content: center; width: auto; margin: 0; padding: 0 var(--space-7); text-decoration: none; }
    .public-cta-outline { display: inline-flex; align-items: center; justify-content: center; min-height: var(--touch-target); padding: 0 var(--space-6); color: var(--public-band-accent); border: 2px solid var(--public-band-accent); border-radius: var(--radius-md); font-size: var(--font-md); font-weight: var(--font-weight-bold); text-decoration: none; box-sizing: border-box; }
    .public-cta-outline.is-on-light { width: 100%; color: var(--text-color); border-color: var(--text-color); }
    /* The phone: a drawing of the app's bars by Level - a picture, read out by its label */
    .public-phone { flex: 0 1 360px; min-width: 0; padding: var(--space-5); background: var(--public-band-raised); border: 1px solid var(--public-band-edge); border-radius: var(--radius-xl); box-shadow: var(--shadow-xl); box-sizing: border-box; } /* token-audit-ignore: the width of a phone */
    .public-phone-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); font-size: var(--font-md); }
    .public-phone-head span, .public-phone-caption { color: var(--public-band-muted); font-size: var(--font-sm); }
    .public-phone-caption { margin: var(--space-1) 0 var(--space-4); }
    .public-bars { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--space-1); }
    .public-bars span { height: var(--space-8); border-radius: var(--radius-xs); }
    .public-bars .is-now { outline: 2px solid var(--public-band-text); outline-offset: 1px; }
    .public-phone-now { display: grid; gap: var(--space-1); margin-top: var(--space-4); padding: var(--space-3) var(--space-4); background: var(--public-band-bg); border-radius: var(--radius-lg); }
    .public-phone-now span:first-child { color: var(--public-band-muted); font-size: var(--font-sm); }
    .public-phone-now strong { font-size: var(--font-lg); }
    .public-goes { display: flex; gap: var(--space-1); margin-top: var(--space-1); }
    .public-goes span { width: var(--space-6); height: var(--space-1); border-radius: var(--radius-pill); background: var(--public-band-edge); }
    .public-goes .is-done { background: var(--public-band-accent); }
    /* The Level colours - the app's own (--level-1 to --level-5) */
    :is(.public-bars, .public-strip, .public-key) .is-l1 { background: var(--level-1); }
    :is(.public-bars, .public-strip, .public-key) .is-l2 { background: var(--level-2); }
    :is(.public-bars, .public-strip, .public-key) .is-l3 { background: var(--level-3); }
    :is(.public-bars, .public-strip, .public-key) .is-l4 { background: var(--level-4); }
    :is(.public-bars, .public-strip, .public-key) .is-l5 { background: var(--level-5); }
    .public-h2 { color: inherit; margin: 0 0 var(--space-2); font-size: var(--font-2xl); line-height: var(--line-height-tight); font-weight: var(--font-weight-bold); }
    .public-h3 { color: inherit; margin: 0; font-size: var(--font-xl); line-height: var(--line-height-snug); font-weight: var(--font-weight-bold); }
    .public-lede { margin: var(--space-2) 0 var(--space-7); color: var(--label-color); font-size: var(--font-md); max-width: var(--page-max-narrow); }
    .public-note { margin: var(--space-4) 0 var(--space-7); color: var(--label-color); font-size: var(--font-base); }
    /* A card that opens for the detail: the summary is the button, and "Read more" turns to "Show less" */
    .public-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--pane-col-min)), 1fr)); gap: var(--space-5); align-items: start; }
    .public-card { padding: var(--space-6); background: var(--bg-color); border-radius: var(--radius-xl); }
    .public-card p { margin: var(--space-3) 0 0; line-height: var(--line-height-relaxed); }
    .public-body summary { list-style: none; cursor: pointer; }
    .public-body summary::-webkit-details-marker { display: none; }
    .public-card summary { display: grid; gap: var(--space-2); }
    .public-card-kicker { color: var(--link-color); font-size: var(--font-sm); font-weight: var(--font-weight-bold); text-transform: uppercase; }
    .public-card-title { font-size: var(--font-lg); font-weight: var(--font-weight-bold); line-height: var(--line-height-snug); }
    .public-card-sum { color: var(--label-color); }
    .public-more { display: inline-flex; align-items: center; gap: var(--space-2); min-height: var(--touch-target); color: var(--link-color); font-weight: var(--font-weight-semibold); }
    .public-more::after { content: ""; width: var(--space-2); height: var(--space-2); border: solid currentColor; border-width: 0 2px 2px 0; transform: translateY(-25%) rotate(45deg); }
    details[open] > summary .public-more::after { transform: translateY(25%) rotate(-135deg); }
    details[open] > summary .public-more-open, details:not([open]) > summary .public-more-close { display: none; }
    .public-detail { margin-top: var(--space-6); padding-top: var(--space-2); border-top: 1px solid var(--input-border); }
    .public-detail ul, .public-detail p { max-width: var(--page-max-narrow); line-height: var(--line-height-relaxed); }
    /* The strip: a piece's bars by Level - the picture no other app has */
    .public-strip-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: var(--space-3) var(--space-7); margin-bottom: var(--space-5); }
    .public-key { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); list-style: none; margin: 0; padding: 0; color: var(--label-color); font-size: var(--font-sm); }
    .public-key li { display: inline-flex; align-items: center; gap: var(--space-2); }
    .public-key span { width: var(--space-4); height: var(--space-4); border-radius: var(--radius-xs); }
    .public-strip { display: grid; grid-template-columns: repeat(16, minmax(0, 1fr)); gap: var(--space-1); }
    .public-strip span { height: var(--space-10); border-radius: var(--radius-xs); }
    .public-tools { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-5) var(--space-9); padding-top: var(--space-8); padding-bottom: var(--space-8); }
    .public-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); flex: 1 1 var(--pane-col-min); list-style: none; margin: 0; padding: 0; }
    .public-chips li { padding: var(--space-2) var(--space-4); background: var(--container-bg); border: 1px solid var(--input-border); border-radius: var(--radius-pill); font-weight: var(--font-weight-semibold); }
    .public-chips .public-chips-more { background: none; border-color: transparent; color: var(--label-color); font-weight: var(--font-weight-normal); }
    .public-promises { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--stat-col-min)), 1fr)); gap: var(--space-8); }
    .public-promises h3 { margin: var(--space-3) 0 var(--space-1); font-size: var(--font-lg); }
    .public-promises p { margin: 0; color: var(--label-color); }
    .public-icon { width: var(--icon-xl); height: var(--icon-xl); fill: none; stroke: var(--link-color); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; } /* token-audit-ignore: the line weight of an icon */
    .public-story { max-width: var(--page-max-narrow); margin: 0 auto; padding: var(--space-10) var(--space-5); box-sizing: border-box; }
    .public-quote { margin: 0; font-size: var(--font-2xl); font-weight: var(--font-weight-bold); line-height: var(--line-height-snug); }
    .public-person { display: flex; align-items: center; gap: var(--space-3); margin-top: var(--space-5); }
    .public-person > span:last-child { display: grid; }
    .public-person > span:last-child span { color: var(--label-color); font-size: var(--font-base); }
    .public-person-initials { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: var(--touch-target); height: var(--touch-target); border-radius: var(--radius-circle); background: var(--public-band-bg); color: var(--public-band-accent); font-weight: var(--font-weight-bold); }
    .public-join { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-6); padding-top: var(--space-9); padding-bottom: var(--space-9); }
    .public-join > div:first-child { flex: 1 1 var(--pane-col-min); min-width: 0; }
    .public-join p { margin: 0; color: var(--public-band-muted); }
    .public-join-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-5); }
    .public-foot-band { border-top: 1px solid var(--public-band-edge); }
    .public-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 0 var(--space-5); padding-top: var(--space-3); padding-bottom: var(--space-3); color: var(--public-band-muted); font-size: var(--font-sm); }
    .public-foot a, .public-foot span { display: inline-flex; align-items: center; min-height: var(--touch-target); }
    .public-band .public-foot a { color: var(--public-band-muted); }
    /* Pricing: the two plans as cards, everything else behind "Compare everything" */
    .public-pricing-title { margin-top: var(--space-2); }
    .pricing-plans { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--pane-col-min)), 1fr)); gap: var(--space-5); }
    .pricing-plan { display: flex; flex-direction: column; padding: var(--space-7); border: 2px solid var(--input-border); border-radius: var(--radius-xl); }
    .pricing-plan.is-premium { background: var(--public-band-bg); color: var(--public-band-text); border-color: var(--public-band-bg); }
    .pricing-plan-name { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3); margin: 0; font-size: var(--font-xl); }
    .pricing-badge { padding: var(--space-0-5) var(--space-3); border: 1px solid var(--public-band-accent); border-radius: var(--radius-pill); color: var(--public-band-accent); font-size: var(--font-sm); font-weight: var(--font-weight-semibold); }
    .pricing-plan-price { margin: var(--space-1) 0 var(--space-5); font-size: var(--font-lg); font-weight: var(--font-weight-semibold); }
    .pricing-plan.is-premium .pricing-plan-price, .pricing-plan-plus { color: var(--public-band-muted); }
    .pricing-plan-plus { margin: 0 0 var(--space-3); font-size: var(--font-base); }
    .pricing-plan-list { display: grid; gap: var(--space-3); flex: 1 1 auto; margin: 0 0 var(--space-6); padding: 0; list-style: none; }
    .pricing-plan-list li { position: relative; padding-left: var(--space-7); }
    .pricing-plan-list li::before { content: ""; position: absolute; left: var(--space-1); top: var(--space-1); width: var(--space-2); height: var(--space-3); border: solid var(--link-color); border-width: 0 var(--space-1) var(--space-1) 0; transform: rotate(45deg); }
    .pricing-plan.is-premium .pricing-plan-list li::before { border-color: var(--public-band-accent); }
    .pricing-scroll { overflow-x: auto; }
    .pricing-table { width: 100%; min-width: var(--pane-col-min); border-collapse: collapse; margin: var(--space-3) 0; font-size: var(--font-base); }
    .pricing-table th, .pricing-table td { padding: var(--space-3) var(--space-2); border-bottom: 1px solid var(--input-border); vertical-align: middle; }
    .pricing-table thead th { font-size: var(--font-md); text-align: center; border-bottom: 2px solid var(--text-color); }
    .pricing-table tbody th { text-align: left; font-weight: var(--font-weight-normal); }
    .pricing-table td { width: 22%; text-align: center; }
    .pricing-plan-note { display: block; font-size: var(--font-sm); font-weight: var(--font-weight-normal); color: var(--label-color); }
    .pricing-group th { padding-top: var(--space-6); color: var(--link-color); font-size: var(--font-sm); font-weight: var(--font-weight-bold); text-transform: uppercase; text-align: left; }
    /* A tick for "included", a dash for "not included" - drawn, so no icon font is loaded on a public page;
       the words are there for a screen reader (.visually-hidden inside the cell). */
    .pricing-table td.is-yes::before { content: ""; display: inline-block; width: var(--space-2); height: var(--space-4); border: solid var(--link-color); border-width: 0 var(--space-1) var(--space-1) 0; transform: rotate(45deg); }
    .pricing-table td.is-no::before { content: "\2013"; color: var(--label-color); }
    .pricing-table td.is-coming { font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--label-color); }
    /* ML-580: the "Ask to join" form on the public page - a light card on the dark join band, the app's own fields
       (.form-group, label, input, select, .toggle-switch) inside it. specs/components/public-page.md */
    .public-join-form { flex: 1 1 var(--pane-col-min); min-width: 0; padding: var(--space-6); background: var(--container-bg); color: var(--text-color); border-radius: var(--radius-xl); box-sizing: border-box; }
    .public-join-fields[hidden] { display: none; }
    .public-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--stat-col-min)), 1fr)); gap: 0 var(--space-4); }
    .public-form-optional { color: var(--label-color); font-weight: var(--font-weight-normal); }
    .public-join-form p.public-form-hint { margin: var(--space-2) 0 0; color: var(--label-color); font-size: var(--font-sm); line-height: var(--line-height-snug); }
    .public-check { display: flex; align-items: center; gap: var(--space-3); min-height: var(--touch-target); margin: 0 0 var(--space-4); font-weight: var(--font-weight-normal); cursor: pointer; }
    .public-join-form .btn-submit { margin: var(--space-5) 0 0; }
    .public-join-form p.public-join-status { margin: var(--space-3) 0 0; color: var(--text-color); font-weight: var(--font-weight-semibold); }
    .public-join-form a:not([class]) { color: var(--link-color); } /* the card is light: not the dark band's gold */
    .public-join-form p.public-join-status:empty { display: none; }
    .public-join-form p.public-join-status.is-done { margin: 0; font-size: var(--font-lg); }
    /* ML-625: "Did you mean one of these bands?" under the band box when one was typed rather than picked - the
       directory bands it most likely means as the app's own choice rows (.flow-choice-option), then "None of these" */
    .public-band-match { margin: var(--space-3) 0 0; }
    .public-band-match[hidden] { display: none; }
    .public-join-form p.public-band-match-title { margin: 0 0 var(--space-2); color: var(--text-color); font-weight: var(--font-weight-semibold); } /* the card is light: not the dark band's words */
    /* The field only robots fill in: off the page, never focused, hidden from a screen reader */
    .public-trap { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; } /* token-audit-ignore: the standard off-screen pattern */
    @media (min-width: 700px) {
        .public-h2 { font-size: var(--font-3xl); }
    }

    /* ML-645: the waiting screen while a photo or PDF is read - a turning ring with the camera in it, what it is doing, a
       bar (the upload's own % while sending; an estimate while reading), the time so far and how long it usually takes,
       the member's reads left and when they start again, and Cancel. In the app's own gold (owner, 28 Oct 2026). Reduce
       motion stops the ring like every animation (html[data-motion]). specs/components/read-wait.md */
    .read-wait { text-align: center; }
    .read-wait-ring { position: relative; width: var(--read-wait-ring); height: var(--read-wait-ring); margin: var(--space-4) auto var(--space-5); }
    .read-wait-ring::before { content: ''; position: absolute; inset: 0; border-radius: var(--radius-circle); border: var(--space-2) solid var(--primary-action-tint); border-top-color: var(--primary-action); animation: read-wait-turn var(--duration-pulse) var(--ease-linear) infinite; }
    .read-wait-ring .material-symbols-outlined { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; width: auto; font-size: var(--icon-xl); color: var(--primary-action-strong); }
    @keyframes read-wait-turn { to { transform: rotate(360deg); } }
    .read-wait-stage { font-size: var(--font-lg); font-weight: var(--font-weight-bold); margin: 0 0 var(--space-1); }
    .read-wait-sub { font-size: var(--font-sm); color: var(--label-color); margin: 0 0 var(--space-5); }
    .read-wait-bar { height: var(--space-3); border-radius: var(--radius-pill); background: var(--input-border); overflow: hidden; }
    .read-wait-bar-fill { display: block; height: 100%; width: var(--read-wait-done, 0%); background: var(--primary-action-strong); border-radius: var(--radius-pill); transition: width var(--duration-slow) var(--ease-standard); }
    .read-wait-nums { display: flex; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; font-size: var(--font-sm); margin: var(--space-2) 0 var(--space-5); font-variant-numeric: tabular-nums; }
    .read-wait-allow { font-size: var(--font-sm); background: var(--input-bg); border: 1px solid var(--control-border); border-radius: var(--radius-md); padding: var(--space-3); text-align: left; margin: 0 0 var(--space-4); }
    .read-wait-stay { font-size: var(--font-xs); color: var(--label-color); margin: 0 0 var(--space-4); }

    /* ML-639 / ML-655: a piece read from a photo - one thing at a time, in the app's own gold (no new colour - owner,
       28 Oct 2026). The first screen opens with .read-start: what the read means in words, the steps with something to
       look at (.read-start-list) and what has no step of its own; red (danger tint) when something doesn't add up. Each
       step has its own checks at the top (.read-checks), each a question with one answer box (the value box) - Next stays
       the only primary button. The progress bar is the plain one. specs/components/piece-outline.md, "Checks from a read" */
    .read-start { border-radius: var(--radius-md); background: var(--primary-action-tint); padding: var(--space-3); margin-bottom: var(--space-5); }
    .read-start.is-low { background: var(--danger-tint); }
    .read-start p { margin: 0 0 var(--space-2); font-size: var(--font-sm); line-height: var(--line-height-base); }
    .read-start-head { display: flex; align-items: center; gap: var(--space-2); font-weight: var(--font-weight-bold); }
    .read-start-head .material-symbols-outlined { font-size: var(--icon-md); color: var(--primary-action-strong); }
    .read-start.is-low .read-start-head .material-symbols-outlined { color: var(--danger-text); }
    .read-start-list { margin: 0 0 var(--space-2); padding-left: var(--space-5); font-size: var(--font-sm); }
    .read-start-list li { margin-bottom: var(--space-1); }
    .read-start-list strong { font-weight: var(--font-weight-semibold); }
    .read-start .read-checks { margin: var(--space-3) 0 0; background: var(--container-bg); }
    .read-checks { border: 1px solid var(--primary-action-strong); border-radius: var(--radius-lg); padding: var(--space-3); margin-bottom: var(--space-5); }
    .read-checks-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
    .read-checks-title { display: flex; align-items: center; gap: var(--space-1); font-size: var(--font-sm); font-weight: var(--font-weight-bold); color: var(--primary-action-strong); }
    .read-agree-all { min-height: var(--touch-target); padding: 0 var(--space-1); background: none; border: none; color: var(--link-color); text-decoration: underline; font: inherit; font-size: var(--font-sm); font-weight: var(--font-weight-semibold); cursor: pointer; }
    .read-check { border-radius: var(--radius-md); background: var(--primary-action-tint); padding: var(--space-3); margin-top: var(--space-2); border: 1px solid transparent; }
    .read-check.is-done { background: none; border-color: var(--control-border); }
    .read-check-text { font-size: var(--font-sm); line-height: var(--line-height-base); margin: 0 0 var(--space-2); }
    .read-check-moved { display: flex; align-items: center; gap: var(--space-1); font-size: var(--font-xs); color: var(--label-color); margin: var(--space-2) 0 0; }
    .read-open-list { list-style: none; padding: 0; margin: 0 0 var(--space-5); display: flex; flex-direction: column; gap: var(--space-2); max-height: var(--read-open-list-max); overflow: auto; }
    .read-open-list li { background: var(--primary-action-tint); border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); font-size: var(--font-sm); }
    .read-open-list li strong { display: block; font-size: var(--font-xs); color: var(--primary-action-strong); }
    /* ML-657: "Before it's read" - a photo that may not read well, its thumbnail beside what is wrong in words, with
       Turn it / Retake as text buttons of their own width */
    .read-page-list { list-style: none; padding: 0; margin: 0 0 var(--space-5); display: flex; flex-direction: column; gap: var(--space-3); }
    .read-page-row { display: flex; gap: var(--space-3); align-items: flex-start; background: var(--primary-action-tint); border-radius: var(--radius-md); padding: var(--space-3); }
    .read-page-thumb { flex: none; width: var(--read-page-thumb); height: var(--read-page-thumb); object-fit: contain; border-radius: var(--radius-sm); background: var(--container-bg); border: 1px solid var(--input-border); }
    .read-page-text { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; font-size: var(--font-sm); line-height: var(--line-height-base); }
    .read-page-actions { display: flex; flex-wrap: wrap; gap: var(--space-4); }
    .read-page-actions .btn-text { display: inline-block; width: auto; padding: 0; text-align: left; }
    /* Save with something still open: each line has a way straight to its step (a text button, its own width) */
    .read-open-list .read-go { display: inline-block; width: auto; min-height: var(--touch-target); padding: 0; text-align: left; }
