/*
 * QY70/QY100 Web Console
 * Copyright (C) 2026 Doffu <https://qy100.doffu.net/>
 * Licensed under the GNU General Public License v3.0 or later. See LICENSE.
 * Support future development: <https://www.patreon.com/doffu>
 */

:root {
  color-scheme: light;
  --bg: #ffffff;
  --panel: #f6f7f8;
  --border: #e1e4e8;
  --text: #30373e;
  --heading: #23282d;
  --muted: #6b7280;
  --accent: #475260;
  --qy100: #ff5733;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: "Sora", Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
}

#brand-bar {
  height: 5px;
  background: linear-gradient(90deg, #ff5733, #ffc300, #33ff57, #33a1ff, #8a33ff, #ff33a8);
}

header {
  padding: 1rem 1.5rem;
  border-bottom: 1px solid var(--border);
}

h1 { margin: 0; font-size: 1.3rem; font-weight: 600; color: var(--heading); }
h2 { margin: 0 0 0.75rem; font-size: 1rem; font-weight: 600; color: var(--heading); text-transform: uppercase; letter-spacing: 0.04em; }

/* Plain h3s (subsection headings like "Animated Message"/"Graphics
   Library") had no rule of their own, so they fell back to the browser
   default - bold (700) and larger than body text - reading noticeably
   heavier than the h2 section title above them despite being one level
   down. :not(.app-dialog-title) excludes dialog titles, which already
   have their own sizing (see .app-dialog-title) and weren't part of
   this complaint. */
h3:not(.app-dialog-title) { margin: 0 0 0.5rem; font-size: 0.95rem; font-weight: 600; color: var(--heading); text-transform: uppercase; letter-spacing: 0.04em; }

/* Negative left/right margin cancels header's own 1.5rem side padding so
   this border-bottom reaches the viewport edges (full screen width)
   instead of stopping at header's padded content width; the matching
   padding restores the original inset for header-top's own content. */
#header-top {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  margin: 0 -1.5rem 0.75rem;
  padding: 0 1.5rem 0.75rem;
  border-bottom: 1px solid var(--border);
}

#header-brand {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

#site-logo {
  display: block;
  height: 3.125rem;
  width: auto;
}

/* Matches qy100.doffu.net's own "Join the exploration" button (same accent
   color, font, and radius the rest of this app already uses, plus its
   site's soft white glow) so the header reads as one piece with the site
   it's embedded in rather than a foreign widget. */
#join-btn {
  flex-shrink: 0;
  display: inline-block;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  background: var(--accent);
  color: #ffffff;
  font-family: "Sora", Helvetica, Arial, sans-serif;
  font-weight: 500;
  font-size: 0.9rem;
  text-decoration: none;
  box-shadow: 0 0 10px 2px rgba(255, 255, 255, 0.5);
}

#join-btn:hover {
  opacity: 0.88;
}

#header-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
  flex-wrap: wrap;
}

/* Same shape and font as #join-btn, outlined so the two header buttons
   read as a pair rather than two competing calls to action. */
#install-app-btn {
  flex-shrink: 0;
  padding: 9px 19px;
  border: 1px solid var(--accent);
  border-radius: 5px;
  background: transparent;
  color: var(--accent);
  font-family: "Sora", Helvetica, Arial, sans-serif;
  font-weight: 500;
  font-size: 0.9rem;
}

#install-app-btn:hover {
  background: var(--accent);
  color: #ffffff;
  opacity: 1;
}

/* Running as the installed app: there's nothing left to install, and only
   Explorer supporters can install it, so no support pitch either. */
@media (display-mode: standalone), (display-mode: minimal-ui), (display-mode: fullscreen), (display-mode: window-controls-overlay) {
  #install-app-btn, #join-btn { display: none; }
}

/* #join-btn sets its own display, which would otherwise beat [hidden]. */
#header-actions [hidden] {
  display: none;
}

/* Non-Chromium browsers only (install.js): which browsers to use instead. */
#browser-banner a {
  color: inherit;
  font-weight: 600;
}

#browser-banner .browser-banner-button {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0.2rem 0.6rem;
  border-radius: 5px;
  background: var(--accent);
  color: #ffffff;
  text-decoration: none;
  font-weight: 500;
}

#install-dialog-body {
  font-size: 0.88rem;
  line-height: 1.5;
  margin-bottom: 1.1rem;
}

#install-dialog-body p {
  margin: 0 0 0.65rem;
}

#install-dialog-body ul {
  margin: 0 0 0.65rem;
  padding-left: 1.2rem;
}

#install-dialog-body .install-dialog-verify {
  font-size: 0.8rem;
  color: var(--muted);
}

#install-dialog-body .install-dialog-verify a {
  color: inherit;
}

/* Patreon dialogs get the rainbow frame qy100.doffu.net uses for its
   console card and its Patreon pop-up. */
#install-dialog.patreon-dialog {
  border: 4px solid transparent;
  border-radius: 18px;
  background:
    linear-gradient(var(--bg), var(--bg)) padding-box,
    conic-gradient(from 0deg, #ff5733, #ffc300, #33ff57, #33a1ff, #8a33ff, #ff33a8, #ff5733) border-box;
}

/* A link that opens a page (Patreon) but looks like the dialog's buttons. */
.app-dialog-actions a.dialog-link-button {
  display: inline-block;
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--accent);
  border-radius: 5px;
  background: var(--accent);
  color: #ffffff;
  font-size: 0.9rem;
  font-weight: 500;
  text-decoration: none;
}

.app-dialog-actions a.dialog-link-button.btn-warning {
  background: var(--qy100);
  border-color: var(--qy100);
}

/* A 2-column flex layout (not a shared-row grid): the left row (Find
   Devices..Channel) is one line, the right side is two stacked rows
   (Tempo..Sections, then the Send XG/Messaging box). A grid with shared
   row tracks forced row 1's height to match the TALLER of the two columns
   (Tempo's knob), stretching the short left row's row down to match it
   and leaving a dead gap underneath - flex lets each column size to its
   own content instead, with nowrap keeping the two columns side by side
   (each still wraps its own fields internally via .connect-row) down to
   the 600px breakpoint below, which switches to a full column stack. */
/* One column at every width, top to bottom: devices + channel, the
   Connection Check box, the System On box, then tempo/transports/song/
   sections. (It used to be two side-by-side columns, which ran off the right
   edge at ~620-780px.) */
#connect-bar {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1rem;
}

.connect-left-column {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.connect-right-column {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1rem;
}

/* The System On box (row 2 in the markup) sits above Tempo/Transports/
   Song/Sections. */
.connect-row-2-right {
  order: -1;
}

/* Every block spans the full width: MIDI In/Out grow to share the first
   row, the Tempo/Transports/Song/Sections fields spread across theirs, and
   the System On box puts its buttons and its settings at either end.
   Above the phone breakpoint only: below it these rows stack vertically,
   where a flex-basis would stretch heights instead of widths. */
.messaging-box {
  flex: 1 1 auto;
}
@media (min-width: 601px) {
  .connect-row-1-left > label:has(#input-select),
  .connect-row-1-left > label:has(#output-select) {
    flex: 1 1 12rem;
  }
  .connect-row-1-left #input-select,
  .connect-row-1-left #output-select {
    width: 100%;
  }
  .connect-row-1-right {
    justify-content: space-between;
  }
  .messaging-box {
    justify-content: space-between;
  }
}

/* Tempo starts its row now, so its divider would just be a stray line. */
.connect-row-1-right > .connect-divider:first-child {
  border-left: none;
  padding-left: 0;
}

/* Each row above is itself a flex row of fields, wrapping within its own
   width only (never the width of a sibling row). */
.connect-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: flex-end;
}

/* .connect-field is the same shape as a plain <label> here, used instead
   of one where the field itself is already a <label> (the toggle switch
   below) - nesting a <label> inside a <label> is invalid HTML and can
   double-fire click-to-toggle behavior. */
#connect-bar label, #connect-bar .connect-field {
  display: flex;
  flex-direction: column;
  font-size: 0.75rem;
  color: var(--muted);
  gap: 0.25rem;
}

/* Separates Tempo/Transports/Messaging from whatever comes before each -
   .connect-row's own gap already provides the spacing, this just adds the
   visible rule down the middle of it. */
.connect-divider {
  border-left: 1px solid var(--border);
  padding-left: 1rem;
}

/* Its own bordered box (not a .connect-field - it holds two side-by-side
   items, Send XG System On and the Messaging field, rather than a single
   label+control column). */
.messaging-box {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  border: 2px solid var(--qy100);
  border-radius: 10px;
  padding: 0.5rem 0.75rem;
  /* With Continuous Send Rate added, a squeezed box used to crush Send
     GM/XG System On into 2 to 4 line stacks. Keeping every label on one
     line and letting whole items wrap to a second row reads far better. */
  flex-wrap: wrap;
  row-gap: 0.5rem;
}

.messaging-box button:not(.info-icon),
.messaging-box .label-row {
  white-space: nowrap;
}

/* The GM/XG buttons and the two settings (Messaging, Continuous Send Rate)
   each stay together as a unit, so a narrow box wraps into two clean rows
   instead of stranding one field on its own line. No divider before the
   settings group: when wrapped it would just be a stray line at the start
   of the second row; the group gap separates them well enough. */
.messaging-group {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.messaging-settings > .connect-field:first-child {
  border-left: none;
  padding-left: 0;
}

#connect-bar .label-row {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

/* Dial, Reset, value and Tap on one line: the tempo knob is laid out as a
   row (other knobs keep dial-on-top) so this field is no taller than the
   Transports beside it. */
#tempo-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

#tempo-knob .knob-wrap {
  flex-direction: row;
  gap: 0.4rem;
}

#tap-tempo-btn {
  font-size: 0.7rem;
  padding: 0.35rem 0.5rem;
}

#song-select-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

#song-select-input {
  width: 4rem;
}

#song-select-btn {
  font-size: 0.7rem;
  padding: 0.4rem 0.5rem;
}

.transport-controls {
  display: flex;
  gap: 0.35rem;
}

.transport-controls button {
  width: 2rem;
  height: 1.7rem;
  padding: 0;
  font-size: 0.8rem;
  line-height: 1;
  background: var(--panel);
  color: var(--muted);
  border: 1px solid var(--border);
}

.transport-controls button:hover {
  color: #ffffff;
  background: var(--accent);
  border-color: var(--accent);
}

#transport-play-btn {
  color: var(--accent);
}

/* Section Control buttons carry text labels (Main A, Fill AB, etc.) rather
   than the transport icons above, so they need their own width/padding
   instead of the fixed 2rem icon-button box. */
.section-controls {
  /* All seven on one line when there's room, wrapping only as the
     window narrows. */
  flex-wrap: wrap;
}

.section-controls .section-btn {
  width: auto;
  height: auto;
  padding: 0.3rem 0.5rem;
  font-size: 0.7rem;
}

/* Blank only applies in Song mode (unlike the other sections, which work
   in either mode) - the other section buttons are var(--panel) background
   on var(--muted) text; Blank swaps that pairing (var(--muted) background
   on var(--panel) text) so it reads as inverted rather than blending in
   as just another section button. */
.section-controls .section-btn[data-section="BLANK"] {
  background: var(--muted);
  color: var(--panel);
  border-color: var(--muted);
}

.section-controls .section-btn[data-section="BLANK"]:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: #ffffff;
}

#input-select, #output-select {
  width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

select, input[type="text"], input[type="number"], button {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 0.4rem 0.6rem;
  font-family: inherit;
  font-size: 0.9rem;
}

button {
  cursor: pointer;
  background: var(--accent);
  color: #ffffff;
  border-color: var(--accent);
  font-weight: 500;
  transition: transform 0.08s ease, box-shadow 0.08s ease, opacity 0.08s ease;
}

button:hover:not(:disabled) {
  opacity: 0.88;
}

/* A pressed-in look for every button in the app - nothing previously
   distinguished :active from :hover, so clicks had no tactile feedback.
   Shifting down + shrinking slightly plus an inset shadow reads as "being
   pushed", same idea as a physical button. */
button:active:not(:disabled) {
  transform: translateY(1px) scale(0.97);
  box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.35);
  opacity: 0.85;
}

/* Previously only #voice-file-controls and #graphics-controls buttons
   dimmed when disabled - any disabled button elsewhere (e.g. Stop
   Message before anything's playing) rendered at full opacity with no
   visual difference from an enabled one, despite doing nothing when
   clicked. Those two scoped rules are now redundant with this and have
   been removed. */
button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.btn-warning {
  background: var(--qy100);
  border-color: var(--qy100);
}

/* Matches #join-btn's own look (the header's "Support Future Projects"
   button) - same soft white glow and accent-blue background, rather
   than the orange .btn-warning family every other System On/reset-style
   button here uses. Distinguishes GM System On (rarely used, less
   consequential day-to-day) at a glance from XG System On right next to
   it (the app's primary/default mode, and everything else's target). */
#gm-on-btn {
  border: none;
  border-radius: 5px;
  background: var(--accent);
  color: #ffffff;
  box-shadow: 0 0 10px 2px rgba(255, 255, 255, 0.5);
}

#log-controls {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}

#log-enable-label {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.8rem;
  color: var(--muted);
}

#log #log-controls + .hint { margin-top: 0; }

#status {
  font-size: 0.85rem;
  color: var(--muted);
}

/* main now just stacks its three pieces vertically (tabs, the active
   tab's panel, MIDI Log) - the Voice Browser/Parameters 2-column grid
   that used to live directly on main moved to .tab-panel below, since
   MIDI Log is no longer part of that grid. */
main {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1.5rem;
}

#main-tabs {
  display: flex;
  gap: 0.5rem;
  border-bottom: 1px solid var(--border);
}

.main-tab {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  padding: 0.6rem 1rem;
  margin-bottom: -1px;
  font-family: inherit;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--muted);
  cursor: pointer;
}

.main-tab:hover {
  opacity: 1;
  color: var(--heading);
}

.main-tab.active {
  color: var(--heading);
  border-bottom-color: var(--accent);
}

.tab-panel {
  display: none;
}

.tab-panel.active {
  display: block;
}

/* Voice/Parameters is the only panel that needs the 2-column grid - a
   plain block would stack Voice Browser above Parameters instead of
   side by side. */
#tab-panel-voice-parameters.active {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  grid-template-rows: auto auto;
  gap: 1rem;
}

#main-divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 0;
}

section {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1rem;
}

#voice-browser { grid-row: 1 / 3; max-height: 80vh; display: flex; flex-direction: column; }

#voice-controls {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
  flex-wrap: wrap;
  align-items: flex-end;
}

#voice-controls label {
  display: flex;
  flex-direction: column;
  font-size: 0.75rem;
  color: var(--muted);
  gap: 0.25rem;
}

#voice-file-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.5rem 0 0.75rem;
}


#param-file-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0.5rem 0 0.75rem;
}

#parameters hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 0 0 1rem;
}

#fx-file-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0 0 0.75rem;
}

#fx-file-controls[hidden] {
  display: none;
}

#fx-preset-browser {
  margin: 0 0 1rem;
}

#fx-preset-source-toggle {
  margin: 0 0 0.5rem;
}

/* Exactly 5 rows (each measured at 34.4px) before scrolling kicks in. */
#fx-preset-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 10.75rem;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
}

#fx-preset-list li {
  padding: 0.4rem 0.6rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.88rem;
  border-bottom: 1px solid var(--border);
}

#fx-preset-list li:last-child { border-bottom: none; }
#fx-preset-list li:hover { background: rgba(71, 82, 96, 0.08); }
#fx-preset-list li.selected { background: rgba(71, 82, 96, 0.16); }

#fx-preset-list .fx-preset-name { flex: 1; }

#fx-preset-list li.fx-preset-empty {
  cursor: default;
  color: var(--muted);
  font-size: 0.8rem;
}
#fx-preset-list li.fx-preset-empty:hover { background: none; }

#search-box { flex: 1; min-width: 150px; }

#voice-list {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  flex: 1;
}

#voice-list li {
  padding: 0.4rem 0.6rem;
  border-radius: 5px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.88rem;
}

#voice-list li:hover { background: rgba(71, 82, 96, 0.08); }
#voice-list li.selected { background: rgba(71, 82, 96, 0.16); }

/* The name takes the free space and the bank/program/bank-LSB text sits in
   a fixed-width column just before Push, so it lines up row to row instead
   of floating to a different spot depending on the name's length (sized
   for the longest, "SFX Voice P128 B127"). */
#voice-list .voice-name {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
}
/* Dotted leader line from a voice's name across to its bank/program text,
   like a table of contents (main voice rows only; presets have no meta). */
#voice-list .voice-name::after {
  content: '';
  flex: 1 1 auto;
  min-width: 1rem;
  margin: 0 0.6rem;
  border-bottom: 1px dotted #b9bec6;
  transform: translateY(0.2rem);
}
@media (max-width: 600px) {
  /* The bank/program text moves under the name on phones - nothing to
     lead to. */
  #voice-list .voice-name::after { display: none; }
}
#voice-list .voice-meta {
  flex: 0 0 7rem;
  color: var(--muted);
  font-size: 0.62rem;
  white-space: nowrap;
}

#voice-list li.qy100-only { color: var(--qy100); }

#voice-list li.voice-category-divider {
  margin-top: 0.6rem;
  padding: 0.4rem 0.1rem 0.15rem;
  border-top: 2px solid var(--qy100);
  border-radius: 0;
  color: var(--qy100);
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  cursor: default;
}

#voice-list li.voice-category-divider:first-child { margin-top: 0; }
#voice-list li.voice-category-divider:hover { background: none; }

.preset-toggle, .preset-toggle-spacer {
  flex-shrink: 0;
  width: 1.2rem;
  text-align: center;
}

.preset-toggle {
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  padding: 0;
  font-size: 0.7rem;
}

.preset-toggle:hover { color: var(--text); opacity: 1; }

#voice-list li.preset-item {
  padding-left: 2.2rem;
  font-size: 0.82rem;
  color: var(--muted);
}

#voice-list li.preset-item:hover { color: var(--text); }
#voice-list li.preset-item.selected { color: var(--text); }

.push-voice-btn {
  flex-shrink: 0;
  margin-left: auto;
  padding: 0.15rem 0.5rem;
  font-size: 0.72rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg);
  color: var(--text);
  cursor: pointer;
}

.push-voice-btn:hover {
  background: var(--accent);
  color: #ffffff;
  border-color: var(--accent);
}

.qy100-badge {
  display: inline-block;
  margin-left: 0.5rem;
  padding: 0.05rem 0.35rem;
  border: 1px solid var(--qy100);
  border-radius: 4px;
  color: var(--qy100);
  font-size: 0.65rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  vertical-align: middle;
}

.hint { color: var(--muted); font-size: 0.8rem; margin-top: -0.5rem; }

#voice-filter-active {
  color: var(--qy100);
  font-size: 0.8rem;
  font-weight: 600;
  margin: -0.25rem 0 0.5rem;
}
.hint.required {
  color: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 5px;
  padding: 0.5rem 0.65rem;
  margin: 0.5rem 0;
  background: rgba(71, 82, 96, 0.06);
}

#drum-kit-hint {
  color: var(--muted);
  font-size: 0.8rem;
  margin: 0.5rem 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 0.5rem 0.65rem;
}

#drum-kit-hint summary {
  cursor: pointer;
  color: var(--text);
  font-weight: 500;
}

#drum-kit-hint p { margin: 0.5rem 0 0; line-height: 1.4; }
#drum-kit-hint p:last-child { margin-bottom: 0; }

/* Multi Part's "External XG Parameters" (rows the QY70/QY100 ignore), built
   in renderParamPanel - same framed look as #drum-kit-hint above. */
.external-xg {
  margin: 0.75rem 0 0.25rem;
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 0.5rem 0.65rem;
}

.external-xg summary {
  cursor: pointer;
  color: var(--text);
  font-weight: 600;
}

.external-xg-note {
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1.4;
  margin: 0.5rem 0 0.25rem;
}

.external-xg-heading {
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin: 0.9rem 0 0.1rem;
}

#param-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.75rem 0;
}


#param-list {
  max-height: 55vh;
  overflow-y: auto;
  /* Without this, overflow-x computes to auto too (leaving only overflow-y
     set makes both axes auto per spec) - a hidden info-icon tooltip's
     ::after box (22rem wide) then counts toward this container's own
     scrollable area even at opacity:0, producing a phantom horizontal
     scrollbar. Vertical-only scrolling is the only thing this list needs. */
  overflow-x: hidden;
}

.param-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.1rem 0;
  padding: 0.15rem 0;
  border-bottom: 1px solid var(--border);
  /* Toggle-widget rows (Key Assign, Rcv Note Off/On, etc.) are naturally
     much shorter than a knob row - without a floor here, the Insert/Active
     columns (which stretch to match this row's height) get too little
     vertical space for "REC-ARM INSERT" to wrap onto just 2 lines. */
  min-height: 4.5rem;
}

/* Greyed out to show it's not currently applicable (e.g. Variation Part
   while VariMode is System) - stays interactive since the value is still
   real and may matter again once the relevant mode is switched back. */
.param-row.dimmed {
  opacity: 0.4;
}

/* Unused-by-the-current-effect-Type slots stay fully interactive (the byte
   can still be sent and may do something on other XG gear) - just greyed. */
.param-row.unused-param {
  opacity: 0.5;
}

.param-row .param-name {
  /* Up to 12.5rem, but gives way (down to 6.5rem) before the knobs and
     the Insert/Active columns do, e.g. on two-knob MSB/LSB rows or with
     the pin star taking room. */
  flex: 0 1 12.5rem;
  min-width: 6.5rem;
  font-size: 0.85rem;
  line-height: 1.15;
  /* Long names ("CC 71 Harmonic Content (Resonance)", custom names) wrap
     inside the column instead of pushing the knobs around. */
  overflow-wrap: anywhere;
}
.param-row .cc-badge {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0 0.3rem;
  border: 1px solid var(--border);
  border-radius: 3px;
  font-size: 0.65rem;
  line-height: 1.4;
  color: var(--muted);
  vertical-align: 1px;
  cursor: help;
}
/* Support tag badge (XG / MIDI) on parameters the QY70/QY100 don't respond
   to - see PARAM_TAGS in app.js. Same shape as .cc-badge, but filled so it
   reads as "who this is for" rather than "how it's sent". */
.param-row .tag-badge {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0 0.3rem;
  border-radius: 3px;
  font-size: 0.65rem;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.03em;
  color: #fff;
  vertical-align: 1px;
  cursor: help;
}
.param-row .tag-badge--xg { background: #2a7f86; }
.param-row .tag-badge--midi { background: #6f58b8; }
.param-row .param-desc {
  flex: 1;
  min-width: 0;
  font-size: 0.72rem;
  color: var(--muted);
  text-align: left;
}

/* Reverb/Chorus/Variation Type - the select is a full effect-name picker,
   not a short readout, so it wraps onto its own full-width line above the
   MSB/LSB knobs instead of sharing the row with them. */
.param-row.effect-type-row {
  flex-wrap: wrap;
  row-gap: 0.35rem;
}

.param-row.effect-type-row .param-desc {
  flex-basis: 100%;
}

.voice-picker-select {
  flex: 1;
  min-width: 0;
  font-size: 0.82rem;
  padding: 0.2rem 0.4rem;
}

/* "Active"/"Ignored" mute switch, rightmost on every param row - text runs
   vertically so it stays compact in a dense list of knobs. */
.ignore-toggle {
  flex: none;
  align-self: stretch;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
  /* Fits "IGNORED", its longer label. */
  min-height: 4.2rem;
  padding: 0.3rem 0.15rem;
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  background: var(--panel);
  color: var(--muted);
  border: 1px solid var(--border);
}

.ignore-toggle.ignored {
  background: var(--qy100);
  color: #ffffff;
  border-color: var(--qy100);
}

/* The global button:active press effect (see button:active:not(:disabled)
   above) sets its own transform, which would otherwise replace this
   button's rotate(180deg) outright - since both target the same property,
   the click briefly un-rotates the vertical text back to horizontal and
   then snaps back, reading as a 180-degree flip instead of a press.
   Composing rotate(180deg) with a plain scale (no translateY - shifting
   "down" in rotated text space doesn't read as a press) keeps the text
   upright while still giving a pressed-in feel. */
.ignore-toggle:active:not(:disabled) {
  transform: rotate(180deg) scale(0.97);
}

/* "Punch Insert"/"Rec-Arm Insert" - each gets its own narrow column (icon
   stacked above its button) matching the Active/Ignored toggle's footprint,
   sized down (see .insert-btn below) since a full-size button here would
   push these already-tight rows into horizontal scroll. */
.insert-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  align-self: stretch;
  flex: none;
  gap: 0.2rem;
}

.insert-wrap .info-icon {
  flex: none;
  width: 0.8rem;
  height: 0.8rem;
  font-size: 0.55rem;
}

.insert-btn {
  flex: 1;
  /* Tall enough for the longer line ("REC-ARM") even when the row's line
     is short (on/off switches, Send buttons in the narrow-list layout). */
  min-height: 3rem;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  /* Always exactly two lines ("PUNCH / INSERT", "REC-ARM / INSERT", from the
     label's line break) at one fixed width, so a short row can't wrap the
     text onto 1-3 lines and make the buttons uneven or overlap. */
  white-space: pre;
  width: 1.5rem;
  line-height: 1.15;
  overflow: hidden;
  padding: 0.2rem 0;
  font-size: 0.48rem;
  font-weight: 600;
  text-transform: uppercase;
  background: var(--panel);
  color: var(--muted);
  border: 1px solid var(--border);
}

.insert-btn.sent {
  background: var(--accent);
  color: #ffffff;
  border-color: var(--accent);
}

/* Same fix as .ignore-toggle:active above, for the same reason - this
   button is also rotate(180deg) for vertical text. */
.insert-btn:active:not(:disabled) {
  transform: rotate(180deg) scale(0.97);
}

.info-icon, .attention-icon, .label-info-icon {
  position: relative;
  flex: none;
  width: 1.1rem;
  height: 1.1rem;
  padding: 0;
  border-radius: 50%;
  font-family: Georgia, serif;
  line-height: 1;
  cursor: help;
}

.info-icon, .label-info-icon {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--muted);
  font-size: 0.65rem;
  font-style: italic;
}

.info-icon:hover, .label-info-icon:hover {
  color: #ffffff;
  background: var(--accent);
  border-color: var(--accent);
  /* Overrides the global button:hover opacity: 0.88 below - without this,
     hovering the icon (needed to trigger its tooltip) dims the icon and its
     ::after tooltip box together, making the tooltip look translucent
     instead of the same solid background every other tooltip uses. */
  opacity: 1;
}

.attention-icon {
  border: 1px solid var(--qy100);
  background: var(--bg);
  color: var(--qy100);
  font-size: 0.7rem;
  font-weight: 700;
}

.attention-icon:hover {
  color: #ffffff;
  background: var(--qy100);
  border-color: var(--qy100);
  /* Overrides the global button:hover opacity: 0.88 below - without this,
     hovering the icon (needed to trigger its tooltip) dims the icon and its
     ::after tooltip box together, same as .info-icon:hover does. */
  opacity: 1;
}

/* #global-tooltip (a single shared element appended to <body> - see
   index.html) replaces what used to be each icon's own CSS ::after
   pseudo-element. That per-icon approach could only ever render inside
   whatever ancestor happened to contain the icon - if that ancestor
   scrolled or clipped overflow (e.g. #param-list's overflow-y: auto, or
   any of the scrollable hex-output/list boxes throughout the app), the
   tooltip got cut off right along with it, no matter how high its own
   z-index claimed to be; overflow clipping always wins over z-index.
   position: fixed here means every tooltip escapes ALL such ancestors
   and is positioned in plain viewport coordinates, computed fresh per
   hover in app.js (see showGlobalTooltip/positionGlobalTooltip) instead
   of the hardcoded per-icon left:0/right:0 anchors this replaces. */
#global-tooltip {
  position: fixed;
  z-index: 9999;
  max-width: 22rem;
  padding: 0.4rem 0.6rem;
  border-radius: 6px;
  background: var(--heading);
  color: #ffffff;
  font-weight: 400;
  font-family: "Sora", Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  line-height: 1.4;
  text-align: left;
  white-space: pre-line;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.05s linear;
}

#global-tooltip.visible {
  opacity: 1;
  visibility: visible;
}

.effect-type-select {
  width: 100%;
  font-size: 0.78rem;
  padding: 0.25rem 0.4rem;
}

.knob-group {
  display: flex;
  gap: 0.5rem;
  flex-shrink: 0;
  margin-left: auto;
}

.knob-group .knob-wrap + .knob-wrap {
  padding-left: 0.5rem;
  border-left: 1px solid var(--border);
}

.knob-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  flex-shrink: 0;
}

.knob-caption {
  font-size: 0.62rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.knob-dial {
  width: 2rem;
  height: 2rem;
  cursor: ns-resize;
  touch-action: none;
  user-select: none;
}

.knob-track {
  fill: var(--bg);
  stroke: var(--border);
  stroke-width: 2;
}

.knob-wrap.dragging .knob-track { stroke: var(--accent); }

.knob-indicator {
  stroke: var(--accent);
  stroke-width: 2.5;
  stroke-linecap: round;
}

.knob-controls {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.knob-value {
  width: 3.75rem;
  text-align: center;
  padding: 0.15rem;
  font-size: 0.7rem;
  background: var(--bg);
  -moz-appearance: textfield;
}

.knob-value::-webkit-inner-spin-button,
.knob-value::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.reset-btn {
  font-size: 0.65rem;
  padding: 0.2rem 0.3rem;
  flex-shrink: 0;
  background: var(--bg);
  color: var(--accent);
}

.reset-btn:disabled {
  visibility: hidden;
}

.toggle-wrap { justify-content: center; }

/* flex-shrink:0 matters here specifically because the switch's only
   "content" (.toggle-track and its ::before knob) is position:absolute,
   so the switch itself has no in-flow content to give it a min-content
   width floor. Without this, a flex row squeezed by a narrow viewport
   (Onion Skin/Loop/Ping-Pong/Reverse toggles, the header Messaging
   toggle, etc.) would shrink the switch's rendered width right down
   toward 0 instead of just wrapping it onto its own line - collapsing
   the track into a thin sliver with the knob sitting outside it. */
.toggle-switch {
  position: relative;
  display: inline-block;
  flex-shrink: 0;
  width: 2.4rem;
  height: 1.3rem;
  margin: 0.35rem 0;
}

.toggle-switch input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.toggle-track {
  position: absolute;
  inset: 0;
  background: var(--border);
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.15s;
}

.toggle-track::before {
  content: '';
  position: absolute;
  height: 1rem;
  width: 1rem;
  left: 0.15rem;
  top: 0.15rem;
  background: #ffffff;
  border-radius: 50%;
  transition: transform 0.15s;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}

.toggle-switch input:checked + .toggle-track {
  background: var(--accent);
}

.toggle-switch input:checked + .toggle-track::before {
  transform: translateX(1.1rem);
}

/* :not([hidden]) rather than a bare selector - same reason as
   #graphics-load-progress-row's own comment above: an author-stylesheet
   class always beats the UA stylesheet's [hidden] { display: none } at
   equal specificity, regardless of source order, so a plain `display:
   flex` here would keep a hidden segmented-control visible. Only
   #marquee-orbit-direction-toggle actually toggles `hidden` today, but
   this is the shared rule for every .segmented-control. */
.segmented-control:not([hidden]) {
  display: flex;
  border: 1px solid var(--border);
  border-radius: 5px;
  overflow: hidden;
  margin: 0.35rem 0;
}

.segment-btn {
  border: none;
  border-radius: 0;
  background: var(--bg);
  color: var(--text);
  font-size: 0.65rem;
  padding: 0.25rem 0.4rem;
  cursor: pointer;
}

.segment-btn + .segment-btn {
  border-left: 1px solid var(--border);
}

.segment-btn.active {
  background: var(--accent);
  color: #ffffff;
}

#log { max-height: 40vh; display: flex; flex-direction: column; }
/* #log's own display:flex above (an ID selector) outranks the UA
   stylesheet's [hidden]{display:none} (an attribute selector), so the
   hidden attribute set from JS while the Diagnostics tab is open would
   otherwise be silently overridden and the section would stay visible. */
#log[hidden] { display: none; }

#log-output {
  flex: 1;
  overflow-y: auto;
  font-family: "Cascadia Code", Consolas, monospace;
  font-size: 0.78rem;
  color: var(--muted);
  white-space: pre-wrap;
  margin: 0.5rem 0 0;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 0.5rem;
}

#raw-sysex-controls {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 0.75rem;
}

#raw-sysex-controls label {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 20rem;
  font-size: 0.75rem;
  color: var(--muted);
  gap: 0.25rem;
}

#raw-sysex-input-row {
  display: flex;
  align-items: stretch;
  gap: 0.4rem;
}

/* Fixed F0/F7 badges instead of a hint sentence explaining they're added
   automatically - shows the framing is already handled at a glance,
   right where the bytes get typed. */
.raw-sysex-frame-byte {
  position: relative;
  display: flex;
  align-items: center;
  padding: 0 0.6rem;
  font-family: "Cascadia Code", Consolas, monospace;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--muted);
  background: var(--bg);
  border: 1px solid var(--border);
  cursor: help;
  border-radius: 5px;
}

/* A <textarea> instead of <input type="text"> so a long message (like the
   placeholder) can wrap and grow taller on a narrow screen instead of
   scrolling horizontally out of view - app.js grows/shrinks its height to
   fit content on input. resize:none/overflow:hidden hide the manual
   resize handle and scrollbar that would otherwise fight that JS sizing;
   the other #raw-sysex-input-row children (border/background/etc match
   the shared select/input/button rule below, since that selector doesn't
   include textarea). */
#raw-sysex-input {
  flex: 1;
  /* A flex item's default min-width is auto (its content's min-content
     size), not 0 - for a <textarea> that floor is wide enough that on a
     narrow phone screen, the F0/F7 badges plus this plus the Send
     button together didn't all fit and overflowed the row instead of
     the textarea just shrinking to make room like it's supposed to. */
  min-width: 0;
  resize: none;
  overflow: hidden;
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 0.4rem 0.6rem;
  font-family: "Cascadia Code", Consolas, monospace;
  font-size: 0.9rem;
}

#diagnostics .connect-field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

#diagnostics .label-row, #graphics .label-row, #display-text .label-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--heading);
}

#diagnostics-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem 3rem;
  margin: 1rem 0;
}

.diagnostics-filter-toggles {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  max-width: 30rem;
}

/* A grid instead of flex-wrap - flex-wrap breaks wherever items stop
   fitting the container's width, which doesn't reliably land on 8 per
   row; a fixed 8-column grid guarantees channels 1-8 on one row and
   9-16 on the next regardless of container width. */
#diag-channel-filters {
  display: grid;
  grid-template-columns: repeat(8, 1.8rem);
  gap: 0.5rem;
}

.diag-type-toggle {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  font-size: 0.8rem;
  font-weight: normal;
  color: var(--text);
}

/* A smaller variant of .toggle-switch (below) for this dense list of 9
   filters - the default size (used standalone, e.g. the Messaging toggle
   in the connect bar) reads as oversized packed this tightly. */
.toggle-switch--sm {
  width: 1.9rem;
  height: 1.05rem;
}

.toggle-switch--sm .toggle-track::before {
  height: 0.75rem;
  width: 0.75rem;
}

.toggle-switch--sm input:checked + .toggle-track::before {
  transform: translateX(0.85rem);
}

.channel-filter-btn {
  width: 1.8rem;
  height: 1.8rem;
  padding: 0;
  font-size: 0.7rem;
  background: var(--panel);
  color: var(--muted);
  border: 1px solid var(--border);
}

.channel-filter-btn.active {
  background: var(--accent);
  color: #ffffff;
  border-color: var(--accent);
}

#diagnostics-log-controls {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0.5rem 0;
}

#diag-log-output {
  max-height: 40vh;
  overflow-y: auto;
  font-family: "Cascadia Code", Consolas, monospace;
  font-size: 0.78rem;
  color: var(--muted);
  white-space: pre-wrap;
  margin: 0;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 0.5rem;
}

#graphics-controls {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.graphics-controls-divider {
  width: 1px;
  align-self: stretch;
  background: var(--border);
}

.graphics-transform-btn {
  width: 2rem;
  height: 1.7rem;
  padding: 0;
  font-size: 1rem;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--panel);
  color: var(--muted);
  border: 1px solid var(--border);
}

.graphics-transform-btn:hover {
  color: #ffffff;
  background: var(--accent);
  border-color: var(--accent);
}

/* Persistent "this is the selected tool" state, unlike the flip/rotate
   buttons sharing .graphics-transform-btn's base look - those are
   one-shot actions with no ongoing state to show. */
.graphics-tool-btn.active {
  color: #ffffff;
  background: var(--accent);
  border-color: var(--accent);
}

/* Mimics the QY70/QY100's own Bitmap Window display - a 16x16 pixel
   image, worked out from real-hardware corner testing (see
   buildBitmapWindow in sysex.js). Cells are sized for a comfortable
   click-and-drag icon canvas rather than 1:1 with the underlying data
   layout. */
#graphics-canvas-row {
  display: flex;
  gap: 1.5rem;
  align-items: flex-start;
  flex-wrap: wrap;
}

#graphics-grid {
  display: flex;
  gap: 1px;
  width: fit-content;
  max-width: 100%;
  overflow-x: auto;
  background: var(--border);
  /* No padding - the outer edge is exactly this 1px border, the same
     thickness as the 1px inter-cell gaps above (padding here previously
     made the outer edge read as a thicker line than the gridlines
     between cells). */
  border: 1px solid var(--border);
  user-select: none;
}

#graphics-frames-panel {
  flex: 1;
  min-width: 16rem;
}

.graphics-frames-count {
  font-weight: 400;
  color: var(--muted);
}

#graphics-frames-toolbar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0.5rem 0;
}

#graphics-frames-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  max-height: 21.5rem;
  overflow-y: auto;
}

.graphics-frame-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.4rem 0.5rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  cursor: pointer;
}

.graphics-frame-item:hover {
  border-color: var(--accent);
}

.graphics-frame-number {
  flex: 1;
  font-size: 0.8rem;
  color: var(--text);
}

.graphics-frame-item button {
  font-size: 0.7rem;
  padding: 0.3rem 0.5rem;
}

.graphics-frame-move-group {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.graphics-frame-move-btn {
  width: 1.4rem;
  height: 0.95rem;
  padding: 0 !important;
  font-size: 0.55rem !important;
  line-height: 1;
  background: var(--panel);
  color: var(--muted);
  border: 1px solid var(--border);
}

.graphics-frame-move-btn:hover:not(:disabled) {
  color: #ffffff;
  background: var(--accent);
  border-color: var(--accent);
}

.graphics-frame-move-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

/* The frame currently mirroring the canvas live (see graphicsSelectedFrameId
   in app.js) - a colored left edge instead of a full border change, so it
   doesn't compete visually with the button colors in the same row. */
.graphics-frame-item.selected {
  border-color: var(--accent);
  border-left-width: 3px;
}

#graphics-onion-skin-row, #graphics-onion-skin-row-2 {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.5rem 0;
}

#graphics-onion-skin-depth-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

#graphics-onion-skin-depth {
  width: 6rem;
}

#graphics-onion-skin-depth:disabled {
  opacity: 0.45;
}

#graphics-onion-skin-depth-value {
  color: var(--muted);
  font-size: 0.85rem;
  min-width: 3.6rem;
}

.graphics-sysex-panel {
  margin-top: 1rem;
}

#graphics-animation-loop-label, #graphics-animation-pingpong-label {
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 400;
}

#graphics-hex-output, #graphics-animation-hex-output, #display-text-hex-output, #marquee-hex-output, #split-hex-output {
  margin: 0.4rem 0 0;
  font-family: "Cascadia Code", Consolas, monospace;
  font-size: 0.78rem;
  color: var(--muted);
  white-space: pre-wrap;
  word-break: break-all;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 0.5rem;
}

#graphics-animation-hex-output, #marquee-hex-output, #split-hex-output {
  max-height: 16rem;
  overflow-y: auto;
}

/* Extra bottom margin (well beyond the usual 0.5rem row spacing) - the
   Message label's own info icon tooltip drops down far enough to
   otherwise overlap the Message SysEx panel's icon directly below it,
   which could pop its own tooltip open mid-hover and visually clobber
   this one. */
#display-text-controls {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin: 0.5rem 0 4rem;
}

#display-text-controls .label-row {
  align-self: flex-start;
}

#display-text-input {
  width: 100%;
  min-height: 2.4rem;
  background: var(--bg);
  border-radius: 0;
  font-family: "Cascadia Code", Consolas, monospace;
  font-size: 0.9rem;
  line-height: 1.4;
}

#display-text-count {
  align-self: flex-end;
  color: var(--muted);
  font-size: 0.85rem;
}

/* Column layout (label, then a full-width textarea, then the counter
   below-right) rather than a single flex row like the static Message
   field above - a textarea that grows to fit longer marquee text needs
   its own line to grow into, and full width so it stays usable/responsive
   at any panel width instead of being capped to a fixed max-width. */
#marquee-text-controls {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin: 0.5rem 0 3rem;
}

#marquee-text-controls .label-row {
  align-self: flex-start;
}

/* resize:none + overflow:hidden because height is driven by JS
   (auto-grow to scrollHeight on input - see the marquee-text-input
   listener in app.js) rather than the browser's own resize handle or
   scrollbar, so the box grows to fit typed text instead of scrolling
   internally or being manually dragged. */
#marquee-text-input {
  width: 100%;
  min-height: 2.4rem;
  resize: none;
  overflow: hidden;
  font-family: "Cascadia Code", Consolas, monospace;
  font-size: 0.9rem;
  line-height: 1.4;
}

#marquee-text-count {
  align-self: flex-end;
  color: var(--muted);
  font-size: 0.85rem;
}

#marquee-loop-label {
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 400;
}

/* :not([hidden]) for the same reason as #graphics-load-progress-row and
   .segmented-control above - an author-stylesheet display value beats
   the UA stylesheet's [hidden] { display: none } at equal specificity
   regardless of source order. */
#marquee-orbit-direction-row:not([hidden]) {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

#marquee-orbit-direction-label {
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 400;
  min-width: 1.8rem;
}

/* Split Message (Display Text tab) - same column layout (label, then a
   full-width growable textarea, then the counter below-right) as
   #marquee-text-controls above, just as a reusable class since there
   are two of these (Line 1, Line 2) rather than one. Extra bottom
   margin for the same reason as #marquee-text-controls too - the label's
   own info icon tooltip otherwise reaches down into whatever's below it
   (here, the OTHER line's row, or the mode segmented control). */
.split-line-row {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin: 0.5rem 0 3rem;
}

.split-line-row .label-row {
  align-self: flex-start;
}

.split-line-input {
  width: 100%;
  min-height: 2.4rem;
  resize: none;
  overflow: hidden;
  font-family: "Cascadia Code", Consolas, monospace;
  font-size: 0.9rem;
  line-height: 1.4;
}

.split-line-count {
  align-self: flex-end;
  color: var(--muted);
  font-size: 0.85rem;
}

/* :not([hidden]) on the directions row - it starts hidden and is
   toggled by setSplitMode in app.js - for the same reason as
   #marquee-orbit-direction-row above: an author-stylesheet display value
   beats the UA stylesheet's [hidden] { display: none } at equal
   specificity regardless of source order. #split-shared-wait-row is
   never hidden, so it doesn't strictly need the guard, but it's kept
   consistent with its sibling here. */
#split-shared-wait-row:not([hidden]), #split-marquee-directions-row:not([hidden]) {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 0.5rem 0 3rem;
}

#split-loop-label, .split-direction-label {
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 400;
}

.graphics-col {
  display: flex;
  flex-direction: column;
  gap: 1px;
  flex-shrink: 0;
}

/* The real QY70/QY100 LCD's own Bitmap Window pixels aren't square - the
   display reads them wider than tall, so square cells here made a drawn
   image look squashed compared to how it actually renders on the
   device. 1.6:1 wide:tall (adjusted down from an initial 2:1 estimate). */
.graphics-cell {
  width: 1.92rem;
  height: 1.2rem;
  padding: 0;
  background: var(--bg);
  border: none;
  border-radius: 0;
  cursor: pointer;
}

/* Custom per-tool cursors over the canvas itself, not just the toolbar
   button - a small inline SVG glyph (accent-colored fill, white outline
   so it reads on both light and dark cell backgrounds) with its hotspot
   (the two numbers before the fallback keyword) placed at the tip/pour
   point, matching where the click actually lands. Falls back to
   crosshair/cell if the browser can't load the data URI for any reason. */
#graphics-grid.graphics-tool-pencil .graphics-cell {
  cursor:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><polygon points='3,21 6,21 20,7 17,4 3,18' fill='%23475260' stroke='white' stroke-width='1' stroke-linejoin='round'/></svg>")
    3 21,
    crosshair;
}

#graphics-grid.graphics-tool-bucket .graphics-cell {
  cursor:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path d='M6 9 L18 9 L16 19 L8 19 Z' fill='%23475260' stroke='white' stroke-width='1'/><path d='M8 9 Q12 3 16 9' fill='none' stroke='white' stroke-width='1.5'/><circle cx='4' cy='21' r='2' fill='%23475260' stroke='white' stroke-width='1'/></svg>")
    4 21,
    cell;
}

.graphics-cell:hover {
  opacity: 1;
  background: var(--border);
}

.graphics-cell.active {
  background: var(--accent);
}

/* Grid cells are real <button> elements, so they'd otherwise pick up the
   global button:active press effect (shift + shrink + inset shadow) -
   fine for a single button, but distracting on a pixel-art grid where a
   click-and-drag paint stroke passes over many cells in a row, and it's
   redundant besides: .active above already shows which pixels are on. */
.graphics-cell:active:not(:disabled) {
  transform: none;
  box-shadow: none;
  opacity: 1;
}

/* Onion skin ghost - a pixel that's on in an earlier frame but off in the
   current one. Only applied when the cell isn't already .active (see
   renderGraphicsOnionSkin in app.js), so a genuinely-drawn pixel always
   reads as solid, never as a faint ghost. --onion-opacity is set per-cell
   in JS, fading with how many frames back the ghost came from. */
.graphics-cell.onion {
  background: rgba(71, 82, 96, var(--onion-opacity, 0.35));
}

/* Future onion skin ghost - same idea as .onion above, but for a pixel
   that's on in a LATER frame, shown in light orange so past and future
   ghosts stay visually distinct on the same canvas. If a cell is ghosted
   by both directions at once, this rule is declared after .onion so the
   future ghost wins. */
.graphics-cell.onion-future {
  background: rgba(230, 149, 64, var(--onion-future-opacity, 0.35));
}

#graphics-library-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin: 1rem 0 0.75rem;
}

#graphics-library-controls h3 {
  margin: 0;
  font-size: 0.85rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--heading);
}

/* Load from File and Save to Library grouped together with their own
   small gap, so justify-content:space-between above only pushes this
   whole group away from the h3 - not the two buttons apart from
   each other too. */
#graphics-library-buttons {
  display: flex;
  gap: 0.5rem;
}

#graphics-library-source-toggle {
  margin-bottom: 0.75rem;
}

/* Roughly 6 rows tall before scrolling - a long library shouldn't push
   the rest of the page down indefinitely. */
#graphics-library-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-height: 21.5rem;
  overflow-y: auto;
}

/* Not .hint - that class's negative margin-top (meant to pull hint text
   up under a label elsewhere) instead clipped this empty-state message
   against the scrollable list's own top edge here. */
.graphics-library-empty {
  color: var(--muted);
  font-size: 0.8rem;
}

.graphics-library-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
}

/* A tiny 16x16 read-only rendering of the saved image, same 1.6:1 pixel
   proportions as the main canvas but far smaller - just the pixels
   themselves (no gap between them, so it reads as a picture rather than
   a grid) inside a single thin frame, enough to recognize an entry at a
   glance without opening it. */
.graphics-library-preview {
  flex-shrink: 0;
  display: grid;
  grid-template-columns: repeat(16, 3.2px);
  grid-template-rows: repeat(16, 2px);
  border: 1px solid var(--border);
}

.graphics-library-preview span {
  width: 3.2px;
  height: 2px;
  background: var(--panel);
}

.graphics-library-preview span.active {
  background: var(--accent);
}

.graphics-library-name {
  flex: 1;
  font-size: 0.85rem;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#data-filer-controls {
  margin: 0.5rem 0 1rem;
}

#data-filer-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-height: 21.5rem;
  overflow-y: auto;
}

.data-filer-empty {
  color: var(--muted);
  font-size: 0.8rem;
}

.data-filer-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
}

.data-filer-name {
  flex: 1;
  min-width: 0;
  font-size: 0.85rem;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.data-filer-meta {
  flex-shrink: 0;
  font-size: 0.75rem;
  color: var(--muted);
}

.data-filer-item button {
  font-size: 0.75rem;
  padding: 0.35rem 0.6rem;
}


#data-filer-device-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 0.5rem 0;
}

#qy-memory-controls,
#qy-backup-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0.5rem 0;
}

.qy-backup-divider {
  align-self: stretch;
  width: 1px;
  min-height: 1.75rem;
  background: var(--border);
}

#qy-memory-lists:not([hidden]) {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1rem;
  margin-top: 0.5rem;
}

.qy-memory-col-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}

.qy-memory-col-head h4 {
  margin: 0;
}

.qy-memory-col-head button,
.qy-memory-item button {
  font-size: 0.72rem;
  padding: 0.25rem 0.5rem;
}

.qy-memory-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 20rem;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 5px;
}

.qy-memory-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0.5rem;
  font-size: 0.8rem;
  border-bottom: 1px solid var(--border);
}

.qy-memory-item:last-child {
  border-bottom: none;
}

.qy-memory-item.empty {
  color: var(--muted);
}

.qy-memory-num {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.qy-memory-name {
  flex: 1;
  min-width: 0;
  font-family: ui-monospace, Consolas, monospace;
  white-space: pre;
  overflow: hidden;
  text-overflow: ellipsis;
}

.qy-memory-size {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

#data-filer-pull-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 0.5rem 0;
}

#data-filer-pull-number {
  width: 4.5rem;
}

#data-filer-pull-status {
  min-height: 1.2em;
}

.app-dialog {
  max-width: 26rem;
  width: calc(100% - 2rem);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1.25rem;
  background: var(--bg);
  color: var(--text);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25);
}

.app-dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
}

.app-dialog-title {
  margin: 0 0 0.6rem;
  font-size: 1.05rem;
  color: var(--heading);
}

#confirm-dialog-message {
  margin: 0 0 1.1rem;
  font-size: 0.88rem;
  line-height: 1.5;
  color: var(--text);
  white-space: pre-line;
}

.confirm-note {
  color: var(--qy100);
}

#save-as-message {
  margin: 0 0 0.9rem;
  font-size: 0.8rem;
  line-height: 1.4;
  white-space: pre-line;
}

/* Wider than the base .app-dialog (26rem) - shared by dialogs that carry
   headings/lists/images rather than a single short message, and need the
   room. Each one's .dialog-body is independently scrollable so the title
   and actions stay visible even when the content alone is taller than
   the viewport. */
.wide-dialog {
  max-width: 34rem;
}

.dialog-body {
  max-height: 60vh;
  overflow-y: auto;
  margin: 0 0 1rem;
  padding-right: 0.25rem;
}

/* Fixed (not max-) height, unlike the general .dialog-body above - this one
   switches between tab panels of very different lengths (Connect vs. MIDI
   Log), and letting the dialog box itself resize on every tab click made
   it visibly jump around. Holding it at the longest panel's height keeps
   the dialog steady; a shorter panel just leaves blank space below, and a
   panel that somehow exceeds it scrolls within this box instead of
   growing it. */
#help-dialog .dialog-body {
  height: 22.5rem;
  max-height: 60vh;
}

.dialog-body p {
  margin: 0 0 0.85rem;
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--text);
}

.dialog-body h4 {
  margin: 1.1rem 0 0.5rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--heading);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.dialog-body ul {
  margin: 0 0 0.85rem;
  padding-left: 1.1rem;
}

.dialog-body li {
  margin-bottom: 0.5rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--text);
}

.dialog-body strong {
  color: var(--heading);
}

.dialog-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0 0 1rem;
}

.dialog-tab {
  padding: 0.35rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--text);
  font-size: 0.78rem;
  font-weight: 500;
}

.dialog-tab.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #ffffff;
}

.dialog-tab-panel p:last-child {
  margin-bottom: 0;
}

.dialog-body code {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.05rem 0.3rem;
  font-family: "Cascadia Code", Consolas, monospace;
  font-size: 0.82em;
}

#site-footer {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.75rem;
  padding: 1.25rem 1.5rem;
  border-top: 1px solid var(--border);
}

#contact-btn {
  display: inline-block;
  padding: 0.4rem 0.6rem;
  border-radius: 5px;
  background: var(--accent);
  color: #ffffff;
  font-family: "Sora", Helvetica, Arial, sans-serif;
  font-weight: 500;
  font-size: 0.9rem;
  text-decoration: none;
}

#contact-btn:hover {
  opacity: 0.88;
}

#support-dialog-image {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  margin: 0.25rem 0 1rem;
}

.app-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}

.app-dialog label {
  display: flex;
  flex-direction: column;
  font-size: 0.75rem;
  color: var(--muted);
  gap: 0.25rem;
  margin: 0 0 0.9rem;
}

/* The rule above sets display, which would otherwise beat the UA [hidden]
   rule for dialog rows that are shown only sometimes (slot dialog's Item
   and Device rows, the MIDI export Song row). */
.app-dialog label[hidden] {
  display: none;
}

.app-dialog label.checkbox-row {
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.85rem;
  color: var(--text);
  margin-bottom: 0.4rem;
}

/* showConfirm's optional tick box + info icon (e.g. "Load External XG
   Parameters"); the icon sits outside the label so clicking it doesn't
   toggle the box. */
.confirm-checkbox-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 0.9rem;
}

.confirm-checkbox-row[hidden] {
  display: none;
}

.confirm-checkbox-row label.checkbox-row {
  margin: 0;
}

.app-dialog input[type="text"],
.app-dialog select {
  width: 100%;
}

#progress-dialog-bar {
  width: 100%;
  height: 0.6rem;
  margin: 0 0 0.6rem;
  accent-color: var(--accent);
}

#progress-dialog-status {
  margin: 0;
  font-size: 0.82rem;
  color: var(--muted);
}

/* Inline (non-modal) progress feedback for Load Image/Load Animation -
   unlike the progress-dialog above (a blocking modal for MIDI-send
   operations that shouldn't be interrupted), file decoding here can
   still need a per-file overwrite confirmation dialog mid-loop, so this
   stays a plain row in the page flow rather than a <dialog> that would
   have to stack with that confirm. */
/* :not([hidden]) rather than a bare selector - an ID selector's
   specificity otherwise beats the UA stylesheet's [hidden] { display:
   none }, so setting row.hidden = true in JS would toggle the attribute
   correctly but never actually hide the element on screen. */
#graphics-load-progress-row:not([hidden]) {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.6rem 0;
}

#graphics-load-progress-bar {
  flex: 1;
  min-width: 0;
  height: 0.6rem;
  accent-color: var(--qy100);
}

#graphics-load-progress-status {
  flex: none;
  font-size: 0.82rem;
  color: var(--muted);
  white-space: nowrap;
}

/* ---- Responsive ----
   The three-panel grid and the fixed-width param rows assume real
   horizontal room. Below tablet width the grid drops to a single column
   (Voice Browser, then Parameters, then MIDI Log); below phone width the
   header/toolbar rows and each param row also switch from a single wide
   line to a stacked layout so nothing gets clipped or forces horizontal
   scrolling. */

/* Single column until there's room for Voice Browser and Parameters side
   by side: a Parameters row needs ~530px, which two columns couldn't give
   it between 900 and ~980px (the panel pushed past the window edge). */
@media (max-width: 1000px) {
  #tab-panel-voice-parameters.active {
    grid-template-columns: 1fr;
    grid-template-rows: none;
  }

  /* height, not max-height: these are flex columns whose voice-list/
     log-output child uses flex:1 + overflow-y:auto to fill the rest and
     scroll internally. On desktop that child gets a definite height to
     grow into because the grid stretches this section across two rows;
     single-column mobile removes that stretch, so without an explicit
     height here the section just shrink-wraps to its non-list content and
     the flex:1 child collapses to 0 instead of filling/scrolling. */
  #voice-browser {
    grid-row: auto;
    height: 65vh;
  }

  #log {
    height: 30vh;
  }
}

@media (max-width: 600px) {
  /* A tooltip popup (::after on .info-icon/.attention-icon) is anchored to
     its icon's own position and can extend past the right edge on a narrow
     screen - its box still counts toward the page's scrollable area even
     while hidden (opacity/visibility, not display:none), which forced a
     phantom horizontal scrollbar with nothing visibly overflowing. Safe to
     clip here since every real, visible element already fits at this width. */
  html, body {
    overflow-x: hidden;
  }

  /* The 5 main tabs (Voice/Parameters, Data Filer, Diagnostics + Raw
     SysEx, Graphics, Display Text) don't fit on one line below phone
     width - wrapping keeps every tab reachable with a tap instead of
     silently pushing the later ones off the right edge (the general
     html/body overflow-x:hidden above would otherwise just clip them,
     making Display Text unreachable). */
  #main-tabs {
    flex-wrap: wrap;
  }

  .main-tab {
    padding: 0.5rem 0.7rem;
    font-size: 0.82rem;
  }

  header {
    padding: 0.75rem 1rem;
  }

  /* Matches header's own reduced side padding at this width, so the
     negative margin still cancels it exactly (see the desktop rule's
     comment) instead of over-canceling and overflowing the viewport. */
  #header-top {
    margin: 0 -1rem 0.75rem;
    padding: 0 1rem 0.75rem;
  }

  main {
    padding: 0.75rem;
    gap: 0.75rem;
  }

  h1 {
    font-size: 1.1rem;
  }

  #site-logo {
    height: 2.5rem;
  }

  #join-btn {
    padding: 0.45rem 0.8rem;
    font-size: 0.8rem;
  }

  section {
    padding: 0.75rem;
  }

  #connect-bar {
    flex-direction: column;
    align-items: stretch;
  }

  .connect-right-column {
    margin-left: 0;
    /* Full width like the device pickers above, so the stacked rows'
       divider lines and the Connection Check box line up edge to edge. */
    align-items: stretch;
  }

  .connect-left-column {
    align-self: stretch;
  }

  #connection-check {
    width: auto;
  }

  .connect-row {
    flex-direction: column;
    align-items: stretch;
  }

  .connect-row-1-right,
  .connect-row-2-right {
    justify-content: flex-start;
  }

  #param-file-controls,
  #fx-file-controls,
  #param-controls {
    flex-direction: column;
    align-items: stretch;
  }

  #connect-bar label, #connect-bar .connect-field {
    width: 100%;
  }

  /* .toggle-switch (the Messaging toggle) is itself a <label>, and its
     nested Messaging field is a .connect-field - both get caught by the
     blanket width:100% rule above, stretching the small toggle pill into
     a giant bar and forcing the field wrapper to claim the full width of
     .messaging-box's row, squeezing/wrapping Send XG System On instead of
     sitting beside it. Restoring their normal fixed sizing here keeps
     Messaging matching every other fixed-size control in the connect bar. */
  #connect-bar .toggle-switch {
    width: 2.4rem;
  }

  .messaging-box .connect-field {
    width: auto;
  }

  /* Send GM System On + its info icon + Send XG System On + its info
     icon + the Messaging toggle field together are just wide enough to
     overflow a phone-width screen with nowrap - wrapping lets Messaging
     drop to its own line instead of pushing past the right edge. */
  .messaging-box {
    flex-wrap: wrap;
  }

  /* A Data Filer row wraps its controls onto extra lines here; giving the
     file name its own full line keeps it readable instead of squeezing it
     down to a few characters beside the stats. */
  .data-filer-name {
    flex-basis: 100%;
  }

  /* Phone layout stacks every item on its own, exactly as before the
     grouping above existed. The groups only matter for desktop wrapping. */
  .messaging-group {
    display: contents;
  }

  /* A left border reads as a vertical divider in the desktop row layout -
     once #connect-bar stacks to a column above, that same border just runs
     down the left edge of a full-width block, which doesn't read as a
     separator at all. A top border does the equivalent job stacked. */
  #connect-bar .connect-divider {
    border-left: none;
    padding-left: 0;
    border-top: 1px solid var(--border);
    padding-top: 0.6rem;
  }

  #input-select, #output-select, #channel-select,
  #param-controls select {
    width: 100%;
  }

  /* Stacking these one-per-row (like the rest of the toolbar rows above)
     ate most of Voice Browser's height budget on its own, leaving the
     actual voice list with no room to show anything - see the comment on
     #voice-browser below. A 2-up grid keeps every control full-height and
     touch-sized while using about half the vertical space. */
  #voice-controls,
  #voice-file-controls {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem;
  }

  #voice-controls label {
    width: 100%;
  }

  #voice-controls select, #voice-controls input {
    width: 100%;
  }

  /* height, not max-height - see the matching comment in the 900px query
     above for why. Voice Browser gets the biggest share on phone because
     it's the primary panel and its list needs real room to be usable;
     Parameters and MIDI Log are reached by scrolling the page further. */
  #voice-browser {
    height: 68vh;
  }

  #log {
    height: 25vh;
  }

  /* A voice-list row's fixed pieces (toggle spacer, meta, Push button) no
     longer fit beside a full-width name on a phone-width line, so the row
     wraps - the meta text moves under the name (indented to align with it)
     and the Push button moves to its own line, flush right. */
  #voice-list li {
    flex-wrap: wrap;
  }

  #voice-list .voice-meta {
    margin-left: 1.6rem;
    /* Its own line under the name; allowed to shrink so the indent doesn't
       push it past the row (the desktop rule fixes it at 8.5rem). */
    flex: 1 1 100%;
    min-width: 0;
  }

  .push-voice-btn {
    margin-left: auto;
  }

  /* A param row's fixed-width name column plus its description and knob
     group can't share one line below tablet width - each piece drops to
     its own full-width line instead: name (with its info icon and the
     Active/Ignored toggle, both small enough to stay inline with it),
     then description, then the knob(s). */
  .param-row {
    flex-wrap: wrap;
    row-gap: 0.35rem;
  }

  .param-row .param-name {
    width: auto;
    /* Always shares the first line with the pin star and info icon (it
       takes the rest of that line and wraps inside itself), so a long or
       custom name doesn't drop onto a line of its own. */
    flex: 1 1 calc(100% - 4rem);
    min-width: 0;
    order: 1;
  }

  .param-row .param-desc {
    flex-basis: 100%;
    order: 3;
  }

  .knob-group {
    flex-basis: 100%;
    margin-left: 0;
    order: 4;
  }

  .insert-wrap,
  .ignore-toggle {
    order: 2;
    align-self: auto;
  }

  .insert-wrap {
    flex-direction: row;
    align-items: center;
  }

  .insert-btn,
  .ignore-toggle {
    writing-mode: horizontal-tb;
    transform: none;
    width: auto;
    padding: 0.2rem 0.5rem;
    /* One line here: nowrap also turns the label's line break into a space. */
    white-space: nowrap;
    line-height: normal;
    min-height: 0;
  }

  /* Text is horizontal at this width (no rotate(180deg) base transform
     to compose with here, unlike the wide-layout :active rules above),
     so the press effect just needs a plain scale. */
  .insert-btn:active:not(:disabled),
  .ignore-toggle:active:not(:disabled) {
    transform: scale(0.97);
  }

  /* #global-tooltip's default 22rem max-width can be wider than a narrow
     viewport itself - the JS positioning (positionGlobalTooltip in
     app.js) clamps its left edge to stay on-screen, but that alone can't
     stop a too-wide box from still overflowing the right edge, so the
     box itself needs a viewport-relative cap too. */
  #global-tooltip {
    max-width: calc(100vw - 3rem);
  }

  #site-footer {
    flex-direction: column;
    padding: 1rem;
  }

  #help-btn, #contact-btn {
    width: 100%;
    text-align: center;
  }

  /* The Graphics canvas's 16 columns are sized in fixed rem at desktop
     (see .graphics-cell's own comment on the 1.6:1 aspect ratio) - below
     phone width that fixed size no longer fits, so #graphics-grid fell
     back to its max-width:100%; overflow-x:auto and made the canvas
     itself require horizontal scrolling to draw on or even see in full.
     A calc() against 100vw here would need to guess how much padding/
     border/scrollbar width stands between the viewport and the grid's
     actual available space, which is fragile (a vertical scrollbar
     alone silently eats ~15-17px that 100vw doesn't account for). Flex
     percentages sidestep that entirely: stretching #graphics-grid to
     fill 100% of whatever width its container actually laid out for it,
     then letting all 16 .graphics-col flex equally within that (with
     min-width:0 so they're actually allowed to shrink), means the total
     is exactly right by construction - no arithmetic to get wrong.
     aspect-ratio keeps each cell's own 1.6:1 proportions as the now-
     fluid width changes. */
  #graphics-grid {
    width: 100%;
  }

  .graphics-col {
    flex: 1 1 0;
    min-width: 0;
  }

  .graphics-cell {
    width: 100%;
    height: auto;
    aspect-ratio: 1.6 / 1;
  }

  /* Both toolbar rows (drawing tools/undo/flip/rotate/clear, and the
     Image Presets/User Images/Animation Presets/User Animations source
     picker) are wide enough on their own to overflow a phone-width
     screen with flex-wrap left at its nowrap default - wrapping keeps
     every control reachable instead of pushing the later ones off the
     right edge. */
  #graphics-controls,
  #graphics-library-controls,
  #graphics-library-buttons,
  #graphics-frames-toolbar,
  #raw-sysex-input-row {
    flex-wrap: wrap;
  }

  /* #raw-sysex-controls label's desktop min-width:20rem (enough room for
     a comfortable one-line Raw SysEx label+textarea+button row) is wider
     than the viewport itself below phone width - overriding it lets the
     label (and #raw-sysex-input inside it, already min-width:0) actually
     shrink to fit instead of forcing the whole row past the right edge. */
  #raw-sysex-controls label {
    min-width: 0;
  }

  /* The 16 channel-filter buttons' fixed 8-column x 1.8rem grid (286px
     total, sized for comfortable desktop click targets) is wider than
     the available width below phone width - shrinking both the grid's
     columns/gap and the buttons themselves together keeps all 8 columns
     on one row instead of the grid overflowing past the right edge. */
  #diag-channel-filters {
    grid-template-columns: repeat(8, 1.5rem);
    gap: 0.35rem;
  }

  .channel-filter-btn {
    width: 1.5rem;
    height: 1.5rem;
  }
}

/* ---- Pins (My Controls), custom names, Send buttons, search ---- */
.param-row .pin-btn {
  flex-shrink: 0;
  background: none;
  border: none;
  padding: 0 0.1rem;
  font-size: 1rem;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
}
.param-row .pin-btn.pinned { color: #e0a100; }
.param-row .pin-btn:hover { color: #e0a100; }

.param-row .rename-btn {
  background: none;
  border: none;
  margin-left: 0.15rem;
  padding: 0 0.15rem;
  font-size: 0.8rem;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
  vertical-align: 1px;
}
.param-row .rename-btn:hover { color: var(--text); }

.param-row .param-name-sub {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.68rem;
  line-height: 1.25;
  color: var(--muted);
}

.param-row .action-send-btn {
  min-width: 4.5rem;
}

.param-group-heading:first-child { margin-top: 0.25rem; }

#param-search-wrap {
  position: relative;
  flex: 1 1 14rem;
  min-width: 0;
}
/* Phones stack #param-controls vertically, where a 14rem flex-basis would
   become a 14rem-tall gap under the search box. */
@media (max-width: 600px) {
  #param-search-wrap { flex: 0 0 auto; }
}
#param-search { width: 100%; }
#param-search-results {
  position: absolute;
  z-index: 20;
  left: 0;
  right: 0;
  top: calc(100% + 2px);
  max-height: 22rem;
  overflow-y: auto;
  margin: 0;
  padding: 0.25rem 0;
  list-style: none;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
}
#param-search-results button {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  padding: 0.35rem 0.6rem;
  background: none;
  border: none;
  border-radius: 0;
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
  text-align: left;
  cursor: pointer;
}
#param-search-results button:hover,
#param-search-results button:focus-visible { background: var(--panel); }
#param-search-results .tag-badge {
  padding: 0 0.3rem;
  border-radius: 3px;
  font-size: 0.65rem;
  font-weight: 600;
  color: #fff;
}
#param-search-results .tag-badge--xg { background: #2a7f86; }
#param-search-results .tag-badge--midi { background: #6f58b8; }
.param-search-section {
  margin-left: auto;
  font-size: 0.72rem;
  color: var(--muted);
}
.param-search-empty {
  padding: 0.35rem 0.6rem;
  font-size: 0.85rem;
  color: var(--muted);
}

@keyframes search-flash {
  0%, 30% { background: rgba(224, 161, 0, 0.28); }
  100% { background: transparent; }
}
.param-row.search-flash { animation: search-flash 1.6s ease-out; }

#pinned-controls,
#channel-section-hint,
#section-note { flex-basis: 100%; }
#pinned-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
#pinned-controls[hidden] { display: none; }

/* ---- XG Address (Diagnostics + Raw SysEx) ---- */
#xg-address-controls { margin-top: 0.9rem; }
#xg-address-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.5rem;
  margin-top: 0.35rem;
}
#xg-address-row label {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.75rem;
  color: var(--muted);
}
#xg-address-row input { width: 3.2rem; font-family: monospace; }
#xg-address-row #xg-addr-data { width: 9rem; }
#xg-addr-preview { margin-top: 0.4rem; font-family: monospace; }
.param-row .info-icon-spacer { flex: none; width: 1.1rem; }
/* .hint's negative top margin would pull this above #param-list's
   scrolling edge, where it gets clipped. */
#param-list .param-list-empty { margin: 0.75rem 0; }

/* ---- Connection Check (Probe button under MIDI In / MIDI Out) ---- */
/* Same box shape as .messaging-box (Send GM/XG System On), outlined in
   the site's navy instead of orange. */
#connection-check {
  margin-top: 0.6rem;
  border: 2px solid var(--accent);
  border-radius: 10px;
  padding: 0.5rem 0.75rem;
}
.connection-check-row { display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; }
.connection-check-label { font-size: 0.8rem; color: var(--muted); }
#probe-results {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  font-size: 0.8rem;
  line-height: 1.35;
}
#probe-results[hidden] { display: none; }
#probe-results li { display: flex; gap: 0.45rem; align-items: flex-start; margin: 0.25rem 0; }
#probe-results .probe-icon {
  flex: none;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 1.1rem;
  text-align: center;
  color: #fff;
}
#probe-results .probe-ok .probe-icon { background: #2e8b57; }
#probe-results .probe-warn .probe-icon { background: #d48a00; }
#probe-results .probe-fail .probe-icon { background: var(--qy100); }
#probe-results .probe-info .probe-icon { background: var(--accent); font-family: Georgia, serif; font-style: italic; }
/* Hints under the Parameters buttons (channel note, section note, Drum
   Setup tip): .hint's negative top margin would butt them against the
   Push/Reset buttons. */
#param-controls .hint { margin: 0.25rem 0 0; }

/* Continuous Send Rate: slider plus an exact-value box (1-300 ms). */
.send-rate-control {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.send-rate-control input[type="range"] {
  flex: 1 1 8rem;
  min-width: 6rem;
  accent-color: var(--accent);
}
.send-rate-control input[type="number"] {
  width: 4rem;
}
.send-rate-unit {
  font-size: 0.75rem;
  color: var(--muted);
}

/* Transports takes the Tempo row's spare width and its four buttons stretch
   evenly across it (Sections shares .transport-controls but keeps its
   natural-width text buttons). */
@media (min-width: 601px) {
  /* Not on phones: there the row stacks vertically and a flex-basis would
     stretch its height instead. */
  .connect-row-1-right > .connect-field:has(> .transport-controls:not(.section-controls)) {
    flex: 1 1 10rem;
  }
}
.transport-controls:not(.section-controls) button {
  flex: 1 1 0;
  width: auto;
  min-width: 2rem;
}

/* Parameter rows when the list itself is narrow (whatever the window size,
   e.g. the Parameters panel beside the Voice Browser): the readout moves to
   its own line under the name instead of being squeezed to nothing beside
   wide controls (Part Mode's 4-way switch, MSB/LSB knob pairs). Phones keep
   their own stacked layout (the max-width: 600px rules). */
#param-list { container-type: inline-size; container-name: param-list; }
@media (min-width: 601px) {
  @container param-list (max-width: 780px) {
    .param-row { flex-wrap: wrap; row-gap: 0.1rem; }
    /* A small basis so the name gives way (and wraps its own text) before
       anything else on the line wraps; it still grows into spare room. */
    .param-row .param-name { flex: 1 1 6.5rem; }
    .param-row .param-desc {
      order: 10;
      flex: 1 1 100%;
      /* Lines up under the name (after the pin star and info icon). */
      padding-left: calc(2.2rem + 1.2rem);
      min-height: 0;
    }
    .param-row .param-desc:empty { display: none; }
  }

  /* Very narrow lists (e.g. ~1000-1150px windows with the Voice Browser
     beside it): two knobs plus the name and send buttons can't share a
     line, so stack like phones do: name + send buttons, readout, knobs. */
  @container param-list (max-width: 520px) {
    .param-row { row-gap: 0.35rem; }
    .param-row .param-name { flex: 1 1 calc(100% - 4rem); min-width: 0; order: 1; }
    .param-row .param-desc { order: 3; padding-left: 0; }
    .param-row .knob-group { flex-basis: 100%; margin-left: 0; order: 4; }
    .param-row .insert-wrap,
    .param-row .ignore-toggle { order: 2; align-self: auto; }
    .param-row .insert-wrap { flex-direction: row; align-items: center; }
    .param-row .insert-btn,
    .param-row .ignore-toggle {
      writing-mode: horizontal-tb;
      transform: none;
      width: auto;
      padding: 0.2rem 0.5rem;
      white-space: nowrap;
      line-height: normal;
      min-height: 0;
    }
    .param-row .insert-btn:active:not(:disabled),
    .param-row .ignore-toggle:active:not(:disabled) { transform: scale(0.97); }
  }
}

/* The smallest phones (~320-360px): Punch Insert, Rec-Arm Insert and
   Active need slightly tighter padding and gaps to share one line. */
@media (max-width: 360px) {
  .param-row { column-gap: 0.4rem; }
  .param-row .insert-btn,
  .param-row .ignore-toggle { padding: 0.2rem 0.35rem; }
  .param-row .insert-wrap { gap: 0.15rem; }
}
@media (max-width: 340px) {
  /* Hover-only tooltips, which touch screens can't show anyway; hiding them
     lets Punch Insert, Rec-Arm Insert and Active share one line. */
  .param-row .insert-info-icon { display: none; }
  .external-xg { padding-left: 0.35rem; padding-right: 0.35rem; }
  .param-row { column-gap: 0.25rem; }
  .param-row .insert-btn,
  .param-row .ignore-toggle { padding: 0.2rem 0.25rem; }
}
