/*
 * 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; }

#header-top {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.75rem;
  gap: 1rem;
}

#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;
}

#connect-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: flex-end;
}

#connect-bar label {
  display: flex;
  flex-direction: column;
  font-size: 0.75rem;
  color: var(--muted);
  gap: 0.25rem;
}

#connect-bar .label-row {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

#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;
}

button:hover {
  opacity: 0.88;
}

.btn-warning {
  background: var(--qy100);
  border-color: var(--qy100);
}

#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 {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  grid-template-rows: auto auto;
  gap: 1rem;
  padding: 1.5rem;
}

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;
}

#voice-file-controls button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

#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;
}

#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); }

#voice-list .voice-meta { color: var(--muted); font-size: 0.78rem; margin-left: auto; }

#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; }
.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; }

#param-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.75rem 0;
}


#param-list {
  max-height: 55vh;
  overflow-y: auto;
}

.param-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.1rem 0;
  padding: 0.15rem 0;
  border-bottom: 1px solid var(--border);
}

/* 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 {
  width: 12.5rem;
  flex: none;
  font-size: 0.85rem;
  line-height: 1.15;
}
.param-row .param-desc {
  flex: 1;
  min-width: 0;
  font-size: 0.72rem;
  color: var(--muted);
  text-align: left;
}

.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);
  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);
}

.info-icon, .attention-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 {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--muted);
  font-size: 0.65rem;
  font-style: italic;
}

.info-icon:hover {
  color: #ffffff;
  background: var(--accent);
  border-color: var(--accent);
  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;
}

/* Shared tooltip-box styling for both icon types, kept as one rule so their
   popups can't visually drift apart - custom tooltip instead of the native
   title attribute, which most browsers delay by ~1s before showing. Solid
   background (no alpha), regular weight, and the app's own font regardless
   of which icon triggered it. */
.info-icon::after, .attention-icon::after {
  content: attr(data-tooltip);
  position: absolute;
  z-index: 30;
  top: 140%;
  left: 0;
  width: max-content;
  max-width: 22rem;
  padding: 0.4rem 0.6rem;
  border-radius: 6px;
  background: var(--heading);
  color: #ffffff;
  font-style: normal;
  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;
}

.info-icon:hover::after {
  opacity: 1;
  visibility: visible;
}

/* This icon sits at the top-right of the page, right against the edge - the
   default left:0 anchor made its tooltip extend rightward off the viewport,
   where it ended up rendered underneath the page's own scrollbar instead of
   above it. Anchoring from the right edge instead keeps it fully on-screen. */
.header-info-icon::after {
  left: auto;
  right: 0;
}

/* attention-icon's tooltip visibility is driven by JS (mouseenter/mouseleave
   bound directly to the button, see app.js) instead of bare :hover - it
   sits right next to the MIDI In select inside the same <label>, and
   relying on :hover made the tooltip pop open while hovering the select or
   label text too. */
.attention-icon.tooltip-open::after {
  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; }

.toggle-switch {
  position: relative;
  display: inline-block;
  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);
}

.segmented-control {
  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 { grid-column: 2; max-height: 40vh; display: flex; flex-direction: column; }

#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;
}

.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;
}

.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);
}

/* ---- 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. */

@media (max-width: 900px) {
  main {
    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 {
    grid-column: auto;
    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;
  }

  header {
    padding: 0.75rem 1rem;
  }

  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,
  #param-file-controls,
  #param-controls {
    flex-direction: column;
    align-items: stretch;
  }

  #connect-bar label {
    width: 100%;
  }

  #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;
    flex-basis: 100%;
  }

  .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;
    flex: 1 1 auto;
    order: 1;
  }

  .param-row .param-desc {
    flex-basis: 100%;
    order: 3;
  }

  .knob-group {
    flex-basis: 100%;
    margin-left: 0;
    order: 4;
  }

  .ignore-toggle {
    order: 2;
    align-self: auto;
    writing-mode: horizontal-tb;
    transform: none;
    width: auto;
    padding: 0.2rem 0.5rem;
  }

  /* The tooltip popup's own box (22rem max-width) still counts toward the
     page's scrollable area on a narrow viewport even while hidden - cap it
     to the viewport so it can't force a horizontal scrollbar. */
  .info-icon::after, .attention-icon::after {
    max-width: calc(100vw - 3rem);
  }

  #site-footer {
    flex-direction: column;
    padding: 1rem;
  }

  #help-btn, #contact-btn {
    width: 100%;
    text-align: center;
  }
}
