/* ============================================================
   PEDALINHO CHROMA DESIGN TOKENS — Web port
   Source: Packages/PedalinhoUI/Sources/PedalinhoUI/Theme/
     ChromaHardwarePalette.swift (oklch values, literal)
     ChromaBrandFonts.swift (Barlow Condensed / IBM Plex Mono / Inter)
     Components/Chroma/ChromaKnob.swift (knob geometry)
   Purpose: faithful look-and-feel transfer to HTML/CSS for
   Pedalinho Web (ME-90 + Pocket Master editors, issue #75).
   ============================================================ */

:root {
  /* ── Chassis surfaces (oklch → CSS oklch() — modern browsers) ── */
  --page-bg:            oklch(0.14 0.006 250);   /* pageBackground */
  --page-gradient-top:  oklch(0.20 0.020 250);   /* pageGradientTop */
  --card-top:           oklch(0.26 0.015 250);   /* cardTop */
  --card-bottom:        oklch(0.19 0.010 250);   /* cardBottom */
  --tile-top:           oklch(0.27 0.014 250);   /* tileTop */
  --tile-bottom:        oklch(0.18 0.010 250);   /* tileBottom */
  --panel-top:          oklch(0.20 0.012 250);   /* panelTop */
  --panel-bottom:       oklch(0.13 0.007 250);   /* panelBottom */
  --titlebar:           oklch(0.20 0.010 250);   /* titleBar */
  --toolbar:            oklch(0.18 0.010 250);   /* toolbar */
  --sidebar:            oklch(0.16 0.008 250);   /* sidebar */
  --well-center:        oklch(0.30 0.020 250);   /* wellCenter */
  --well-edge:          oklch(0.16 0.010 250);   /* wellEdge */
  --cell-bg:            oklch(0.22 0.012 250);   /* mmCellBackground */
  --knob-body-center:   oklch(0.40 0.015 250);   /* knobBodyCenter */
  --knob-body-edge:     oklch(0.20 0.010 250);   /* knobBodyEdge */
  --knob-track:         oklch(0.30 0.010 250);   /* knobTrack */

  /* ── Borders / text ── */
  --border:             oklch(0.32 0.010 250);   /* border */
  --border-subtle:      oklch(0.26 0.010 250);   /* borderSubtle */
  --text-primary:       oklch(0.90 0.010 250);   /* textPrimary */
  --text-secondary:     oklch(0.72 0.010 250);   /* textSecondary */
  --text-muted:         oklch(0.55 0.010 250);   /* textMuted */
  --text-muted-dim:     oklch(0.50 0.010 250);   /* textMutedDim */

  /* ── Connection LEDs ── */
  --led-disconnected:   oklch(0.68 0.220 25);    /* LED.disconnected */
  --led-connected:      oklch(0.75 0.190 150);   /* LED.connected */

  /* ── Accent family (L ~0.72, C ~0.18, hue varies) ── */
  --accent-blue:        oklch(0.72 0.180 200);   /* Accent.blue  — GAIN */
  --accent-green:       oklch(0.72 0.180 165);   /* Accent.green — VOLUME / connected */
  --accent-red:         oklch(0.72 0.180 25);    /* Accent.red   — BASS */
  --accent-amber:       oklch(0.72 0.180 80);    /* Accent.amber — MID */
  --accent-violet:      oklch(0.72 0.180 320);   /* Accent.violet— TREBLE / PRESENCE */
  --accent-purple:      oklch(0.72 0.180 300);   /* Accent.purple */

  /* Accent LED / fill variants */
  --accent-blue-led:    oklch(0.75 0.200 200);
  --accent-green-led:   oklch(0.75 0.200 165);
  --accent-red-led:     oklch(0.75 0.200 25);
  --accent-amber-led:   oklch(0.75 0.200 80);
  --accent-violet-led:  oklch(0.75 0.200 320);
  --accent-purple-led:  oklch(0.75 0.200 300);
  --accent-blue-fill:   oklch(0.30 0.100 200);
  --accent-green-fill:  oklch(0.30 0.100 165);
  --accent-red-fill:    oklch(0.30 0.100 25);
  --accent-amber-fill:  oklch(0.30 0.100 80);
  --accent-violet-fill: oklch(0.30 0.100 320);
  --accent-purple-fill: oklch(0.30 0.100 300);

  /* ── Pocket Master module accents (issue #75 comment 906,
     Packages/PedalinhoPocketMaster*, hex -- not oklch, ported as-given) ── */
  --pm-nr:  #39FF14;
  --pm-fx1: #00B2FF;
  --pm-drv: #FF9500;
  --pm-amp: #AF52DE;
  --pm-ir:  #ED4899;
  --pm-eq:  #5AC8FA;
  --pm-fx2: #30D158;
  --pm-dly: #FFD60A;
  --pm-rvb: #64D2FF;

  /* ── Typography ──
     Barlow Condensed — display/labels (500/600/700)
     IBM Plex Mono   — numeric/status readouts (400/500/600)
     Inter           — body copy (400/500)
     Bundled in the app; use Google Fonts equivalents (SIL OFL). */
  --font-display: "Barlow Condensed", "Arial Narrow", sans-serif;
  --font-mono:    "IBM Plex Mono", ui-monospace, Menlo, monospace;
  --font-body:    "Inter", system-ui, sans-serif;

  /* ── Knob geometry (ChromaKnob — size 76 is the design base) ──
     body diameter = size * 52/76
     ring radius   = size * 32/76
     ring stroke   = max(2, size * 4/76)
     knob travel   = 270° arc, dead zone at bottom (7:30→4:30)
     label font    = Barlow Medium, size * 12/76
     value font    = Plex Mono SemiBold, size * 13/76, accent color */
  --knob-size: 76px;
  --knob-body: 52px;      /* = 76 * 52/76 */
  --knob-ring-r: 32px;    /* = 76 * 32/76 */
  --knob-ring-w: 4px;     /* = 76 * 4/76 */

  /* ── Spacing / radii (from screenshots + view layout) ── */
  --radius-card: 10px;
  --radius-pill: 999px;
  --gap: 16px;
  --gap-sm: 8px;
}

/* ════════════════════════════════════════════════════════════
   BASE
   ════════════════════════════════════════════════════════════ */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  background:
    radial-gradient(120% 90% at 50% 0%, var(--page-gradient-top) 0%, var(--page-bg) 60%);
  color: var(--text-primary);
  font-family: var(--font-body);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

/* ════════════════════════════════════════════════════════════
   TITLE BAR / TOOLBAR  (ChromaHW.titleBar / .toolbar)
   ════════════════════════════════════════════════════════════ */
.titlebar {
  background: linear-gradient(180deg, var(--titlebar), var(--toolbar));
  border-bottom: 1px solid var(--border-subtle);
  padding: 10px 20px;
  display: flex; align-items: center; gap: var(--gap);
  position: sticky; top: 0; z-index: 10;
}
.titlebar .brand {
  font-family: var(--font-display);
  font-weight: 700; font-size: 1.35rem; letter-spacing: 0.06em;
  color: var(--text-primary);
}
.titlebar .brand span { color: var(--accent-blue); }
.titlebar .buildstamp {
  font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-muted);
  letter-spacing: 0; margin-left: 12px; opacity: 0.85;
}
.titlebar .status {
  margin-left: auto; display: flex; align-items: center; gap: 8px;
  font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted);
}
.led {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--led-disconnected);
  box-shadow: 0 0 6px var(--led-disconnected);
}
.led.connected { background: var(--led-connected); box-shadow: 0 0 8px var(--led-connected); }

/* ════════════════════════════════════════════════════════════
   TABS  (EditorTab: LIBRARY / EDITOR / DETAIL — Barlow labels)
   ════════════════════════════════════════════════════════════ */
.tabs { display: flex; gap: 4px; padding: 0 20px; background: var(--toolbar); border-bottom: 1px solid var(--border-subtle); }
.tab {
  font-family: var(--font-display); font-weight: 600; font-size: 0.85rem;
  letter-spacing: 0.08em; color: var(--text-muted);
  padding: 10px 18px; cursor: pointer; border: none; background: none;
  border-bottom: 2px solid transparent;
}
.tab:hover { color: var(--text-secondary); }
.tab.active { color: var(--accent-blue); border-bottom-color: var(--accent-blue); }

/* ════════════════════════════════════════════════════════════
   PANEL / CARD  (ChromaHW.cardTop→cardBottom gradient)
   ════════════════════════════════════════════════════════════ */
.panel {
  background: linear-gradient(135deg, var(--card-top), var(--card-bottom));
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--gap);
}
.panel-title {
  font-family: var(--font-display); font-weight: 600; font-size: 0.9rem;
  letter-spacing: 0.1em; color: var(--text-muted-dim);
  margin-bottom: 8px;
}

/* ════════════════════════════════════════════════════════════
   KNOB  (ChromaKnob faithful port)
   SVG ring: 270° arc from 135°→405° (7:30→4:30), stroke round cap.
   Value arc: same arc, stroke-dasharray = normalized*0.75 of circumference.
   Indicator: white bar near top rim, rotates -135°→+135° with value.
   ── sizes: 30 (chain) / 40 (broad) / 44 (param overview) / 56 (param detail) / 76 (amp) ──
   ════════════════════════════════════════════════════════════ */
.knob { display: inline-flex; flex-direction: column; align-items: center; gap: 3px; transition: opacity 0.15s ease, filter 0.15s ease; }
.knob.writable { cursor: ns-resize; touch-action: none; }
.knob.writable:hover .k-label { color: var(--text-secondary); }
/* A read-only tile (no drag-to-write handle) isn't "alive" -- muted,
   matching ChromaPedalCard.swift's own "color only where there is glow"
   principle (issue #76 comment 1044). */
.knob:not(.writable) { opacity: 0.62; filter: grayscale(0.35); }
.footswitch:not(.writable) { opacity: 0.62; filter: grayscale(0.35); cursor: default; }
.knob svg { display: block; }
.knob .k-label {
  font-family: var(--font-display); font-weight: 500; font-size: 0.7rem;
  letter-spacing: 0.08em; color: var(--text-muted); text-transform: uppercase;
}
.knob .k-value {
  font-family: var(--font-mono); font-weight: 600; font-size: 0.75rem;
}

/* ════════════════════════════════════════════════════════════
   DROPDOWN TILE  (real named options, e.g. GP-5's 100 real patches --
   Alex, 2026-09-01: "patch select doesn't make sense to be a knob...
   it's a dropdown on katana")
   ════════════════════════════════════════════════════════════ */
/* Read-only text tile (textTile.js) -- a label and a real value, for
   names and descriptions that were previously forced through a knob's
   readout (or, worse, through its numeric VALUE, rendering "NaN"). Same
   label typography as every other tile so a card reads as one thing. */
.text-tile { display: inline-flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1 1 100%; }
.text-tile .t-value {
  font-family: var(--font-mono); font-size: 0.9rem; line-height: 1.35;
  overflow-wrap: anywhere;
}
.param-group.neutral .text-tile { flex: 0 1 auto; }
/* #310 (G5n patch list rendered one character per line): a text-only tile
   in a neutral group has no knob siblings to hold its width open, and
   `min-width: 0` + `overflow-wrap: anywhere` let flex shrink it to a
   sliver. A real floor, and wrapping that keeps whole words together. */
.text-tile { min-width: 11rem; }
.text-tile .t-value { overflow-wrap: break-word; }
/* 2026-09-22, Alex's live bench (Valeton GP-5 PATCH NAME): the neutral
   group's row is `nowrap` (rule above), so the writable variant's own
   `flex: 1 1 100%` (meant for a tile alone in its own wrapped row) fights
   every other sibling for space and, with the input's own `min-width: 0`,
   can shrink to an unusable sliver -- exactly what made the edit field
   invisible next to the PATCH dropdown and PATCH VOLUME knob. The
   read-only variant just above doesn't need this: static text can shrink
   or wrap without hiding a control nobody can then use. */
.param-group.neutral .text-tile.writable { flex: 0 0 220px; min-width: 180px; }

/* Editable text tile (textTile.js with an onWrite) -- a field plus an
   explicit SET, never a per-keystroke write, because these go to a real
   device and sometimes to its permanent storage. */
.text-input-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
/* #547: the NAME field clipped a 28-character name (the pedal's own field size) to `this should be me…`,
   so the one place a user checks their work could not show it. Wrapping gives the input the full width when
   the row is tight instead of squeezing it. */
.text-input { min-width: 22ch; }
.text-input {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--font-mono); font-size: 0.85rem;
  color: var(--text-primary);
  background: var(--card-bottom);
  border: 1px solid color-mix(in oklch, var(--input-accent) 45%, transparent);
  border-radius: 6px; padding: 5px 8px;
}
.text-input:focus {
  outline: none;
  border-color: var(--input-accent);
  box-shadow: 0 0 6px color-mix(in oklch, var(--input-accent) 40%, transparent);
}
.text-input-apply {
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.08em;
  color: var(--text-secondary);
  background: none; border: 1px solid var(--border); border-radius: 6px;
  padding: 5px 10px; cursor: pointer;
}
.text-input-apply:hover { color: var(--text-primary); border-color: var(--input-accent); }

.dropdown-tile { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 6px; min-width: 140px; }
.dropdown-select {
  font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-primary);
  background: var(--card-top); border: 1px solid color-mix(in oklch, var(--dropdown-accent) 45%, var(--border));
  border-radius: 6px; padding: 5px 8px; width: 100%; cursor: pointer;
}
.dropdown-select:disabled { cursor: default; opacity: 0.62; }
.dropdown-tile:not(.writable) { opacity: 0.62; filter: grayscale(0.35); }

/* ════════════════════════════════════════════════════════════
   VERTICAL BAR / FADER  (real graphic-EQ look -- Alex, 2026-09-01:
   "equalizer parameters would be better on vertical bars... something
   cool for Web AND App" -- same visual language as .knob: accent fill +
   glow when writable, muted/grayscale when read-only)
   ════════════════════════════════════════════════════════════ */
.vbar { display: inline-flex; flex-direction: column; align-items: center; gap: 3px; transition: opacity 0.15s ease, filter 0.15s ease; }
.vbar-track {
  position: relative; border-radius: 7px; background: var(--knob-track);
  border: 1px solid rgba(255,255,255,0.08); overflow: visible;
  display: flex; align-items: flex-end;
}
.vbar.writable .vbar-track { cursor: ns-resize; touch-action: none; }
/* Absolute, not a flex child: a bipolar bar fills BETWEEN the zero line and
   the value, so it needs a real `bottom` as well as a height. */
.vbar-fill { position: absolute; left: 0; width: 100%; border-radius: 7px; transition: height 0.05s linear, bottom 0.05s linear; }
/* Zero datum for bipolar (EQ) bars -- reading cut-vs-boost needs a line to
   read against. */
.k-label:empty { display: none; }
.vbar-zero {
  position: absolute; left: -2px; right: -2px; height: 1px;
  background: rgba(255,255,255,0.30); pointer-events: none;
}
.vbar-handle {
  position: absolute; left: 50%; transform: translate(-50%, 50%);
  width: 22px; height: 6px; border-radius: 3px;
  background: #fff; opacity: 0.92; box-shadow: 0 0 2px rgba(255,255,255,0.4);
}
.vbar:not(.writable) { opacity: 0.62; filter: grayscale(0.35); }

/* ════════════════════════════════════════════════════════════
   FOOTSWITCH  (bypass LED + label)
   ════════════════════════════════════════════════════════════ */
.footswitch {
  display: inline-flex; flex-direction: column; align-items: center; gap: 4px;
  cursor: pointer; user-select: none;
}
.footswitch .fs-led {
  width: 26px; height: 26px; border-radius: 50%;
  border: 2px solid var(--border);
  background: radial-gradient(circle at 35% 30%, var(--well-center), var(--well-edge));
  transition: all 0.15s ease;
}
.footswitch.on .fs-led {
  background: radial-gradient(circle at 35% 30%, var(--accent-blue-led), var(--accent-blue));
  border-color: var(--accent-blue);
  box-shadow: 0 0 10px var(--accent-blue-led);
}
.footswitch .fs-label {
  font-family: var(--font-display); font-weight: 600; font-size: 0.65rem;
  letter-spacing: 0.1em; color: var(--text-muted);
}

/* ════════════════════════════════════════════════════════════
   SEGMENTED CONTROL  (ChromaEnumSegmentedControl — discrete options)
   ════════════════════════════════════════════════════════════ */
.segmented { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-pill); overflow: hidden; }
.segmented button {
  font-family: var(--font-display); font-weight: 500; font-size: 0.7rem;
  letter-spacing: 0.05em; color: var(--text-muted);
  background: var(--cell-bg); border: none; padding: 5px 12px; cursor: pointer;
}
.segmented button.active { background: var(--accent-blue-fill); color: var(--accent-blue); }

/* Family group labels (connect screen: tested vs. community/untested) --
   same caption style as .id-row .k, reused here since it's the same
   "small uppercase muted heading" role. */
.fam-group-label {
  color: var(--text-muted); text-transform: uppercase; font-size: 0.7rem;
  letter-spacing: 0.08em; display: block; margin: 14px 0 6px; text-align: center;
}
.fam-group-label:first-of-type { margin-top: 0; }
.fam-group-label.fam-group-tested { color: var(--accent-green); }

/* ════════════════════════════════════════════════════════════
   CONNECTION SCREEN  (ChromaKatanaConnectionView pattern)
   ════════════════════════════════════════════════════════════ */
.conn-screen { display: flex; align-items: center; justify-content: center; min-height: 70vh; }
.conn-card { max-width: 440px; text-align: center; }
.conn-card h2 { font-family: var(--font-display); font-weight: 700; font-size: 1.6rem; letter-spacing: 0.04em; margin-bottom: 8px; }
.conn-card p { color: var(--text-muted); font-size: 0.9rem; margin-bottom: 16px; }
.conn-card select {
  width: 100%; padding: 10px 12px; font-size: 0.9rem;
  background: var(--cell-bg); color: var(--text-primary);
  border: 1px solid var(--border); border-radius: 8px; margin-bottom: 12px;
  font-family: var(--font-mono);
}

/* Buttons — hardware-inspired */
.btn {
  font-family: var(--font-display); font-weight: 600; font-size: 0.85rem;
  letter-spacing: 0.08em; text-transform: uppercase;
  border: 1px solid var(--border); border-radius: 8px;
  background: linear-gradient(180deg, var(--card-top), var(--card-bottom));
  color: var(--text-primary); padding: 10px 20px; cursor: pointer;
  transition: all 0.12s ease;
}
.btn:hover { border-color: var(--accent-blue); color: var(--accent-blue); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn.primary { background: var(--accent-blue); color: #0c0f14; border-color: var(--accent-blue); }
.btn.primary:hover { filter: brightness(1.1); color: #0c0f14; }

/* Hex log + readout — IBM Plex Mono */
.hexlog {
  font-family: var(--font-mono); font-size: 0.72rem; line-height: 1.6;
  background: oklch(0.10 0.006 250); border: 1px solid var(--border-subtle);
  border-radius: 8px; padding: 10px; color: var(--text-secondary);
  white-space: pre-wrap; word-break: break-all; max-height: 160px; overflow: auto;
}

/* Amp/effect section — the "amp face" look */
.amp-face {
  background:
    radial-gradient(80% 60% at 50% 0%, var(--well-center) 0%, var(--well-edge) 100%);
  border: 1px solid var(--border); border-radius: var(--radius-card);
  padding: 20px;
}

/* ════════════════════════════════════════════════════════════
   EDITOR-SPECIFIC LAYOUT (Pedalinho Web app shell, not in the
   design-token port above)
   ════════════════════════════════════════════════════════════ */
/* 2026-09-22, Alex's live bench: "the app width shouldn't be limited... fill
   the entire available width" -- was capped at 980px and centered; individual
   cards (.conn-card, .translate-grid, .tone-wizard, ...) already carry their
   own sensible max-widths, so removing this outer cap only gives wide rows
   (like the patch library grid below) more room, it doesn't stretch every
   card edge-to-edge. */
.container { width: 100%; margin: 0 auto; padding: 20px; display: flex; flex-direction: column; gap: var(--gap); }
.lang-toggle {
  font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-muted);
  background: none; border: 1px solid var(--border-subtle); border-radius: 6px;
  padding: 4px 8px; cursor: pointer; margin-left: 8px;
}
/* Page links at the top right (2026-10-13): Report a problem always; Device/Settings/Livesets only while connected.
   The old tab bar is dev-only now. */
.title-nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.title-link {
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase; text-decoration: none;
  color: var(--text-muted); background: none; border: 1px solid transparent; border-radius: 6px; padding: 4px 8px; cursor: pointer;
}
.title-link:hover { color: var(--text-secondary); border-color: var(--border-subtle); }
.title-link.active { color: var(--accent, var(--text-secondary)); border-color: var(--border-subtle); }
/* The dev nav is GONE (Alex, 2026-10-13): "The right shape is LIBRARY in the top bar, next to DEVICE
   and TUNER, with the librarian folded inside it -- and the dev nav gone entirely." LIBRARY moved to the
   top bar, the librarian is reachable from inside LIBRARY, and everything else here was retired above.
   The buttons stay in the DOM because main.js drives mode switching through them -- they are handles
   now, not navigation, so no one ever sees them, dev mode included. */
#modeNav { display: none !important; }
/* Modes Alex retired from the dev nav, 2026-10-13: "there are two menus on the top... the bottom one
   seems redundant to me. there are several parts... not to be displayed, such as tonex, chocolate,
   move tone, guitar, and others, which are to be merged into the livesets menu."
   DEVICE / SETTINGS / LIVESETS are duplicates -- all three are buttons in the top bar. TRANSLATE is
   the item labelled "MOVE TONE" in Portuguese. TONEX, CHOCOLATE, GUITAR have no working surface.
   SETLISTS is the same idea as LIVESETS and belongs inside it.
   Hidden rather than deleted: every mode section still exists in editor.html, so un-hiding one line
   here brings a mode back.

   PLAY-ALONG hidden too, same day: "play along doesnt even work as a functionality, so hide it."

   ** LIBRARY IS THE CATCH-ALL, by the owner's decision: "library should be the catchall for livesets,
   favorites, etc etc". So LIVESETS, favorites and anything else that ends up in that family belong
   INSIDE LIBRARY -- not the other way round. This supersedes the earlier reading that the other modes
   merge into LIVESETS. The mode sections are still here to be folded in; that fold is the work. */
#modeDevice, #modeTranslate, #modeSetlists, #modeGuitar,
#modeToneX, #modeSettings, #modeChocolate, #modeLibrarian,
#modePlayAlong { display: none !important; }
/* Tuner (#644) */
.tuner-panel { position: fixed; top: 56px; right: 16px; z-index: 30; width: min(360px, calc(100vw - 32px)); background: var(--panel-bg, var(--toolbar)); border: 1px solid var(--border); border-radius: 10px; padding: 14px; display: flex; flex-direction: column; gap: 10px; box-shadow: 0 8px 28px rgba(0,0,0,.45); }
.tuner-head { display: flex; align-items: center; justify-content: space-between; }
.tuner-title { font-family: var(--font-mono); font-size: .75rem; letter-spacing: .12em; color: var(--text-muted); }
.tuner-close { background: transparent; border: 1px solid var(--border); border-radius: 6px; color: var(--text-secondary); font-size: 1rem; line-height: 1; padding: 2px 8px; cursor: pointer; }
.tuner-close:hover { color: var(--text-primary, #fff); border-color: var(--accent, #19c5d8); }
.tuner-route { font-size: .72rem; color: var(--text-muted); }
/* The instrument leads: the note is the biggest thing on the panel, the scale sits under it, and the
   input picker and button live at the bottom where they are set once and then ignored. */
.tuner-note { font-size: 3.4rem; font-weight: 700; text-align: center; line-height: 1; margin-top: 2px; }
.tuner-note.in-tune { color: #37d67a; }
.tuner-note-sub { text-align: center; font-family: var(--font-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); margin-top: -6px; }
.tuner-scale { position: relative; height: 34px; margin: 4px 0 2px; }
.tuner-ticks { position: absolute; inset: 0; }
.tuner-tick { position: absolute; top: 14px; transform: translateX(-50%); font-family: var(--font-mono); font-size: .62rem; color: var(--text-muted); }
.tuner-tick::before { content: ""; position: absolute; left: 50%; top: -8px; width: 1px; height: 6px; background: var(--border-subtle); }
.tuner-tick.centre { color: var(--text-secondary); }
.tuner-tick.centre::before { background: var(--text-muted); height: 8px; top: -10px; }
/* One track with a centre line, and one needle that turns green when the note is close enough. */
.tuner-scale::before { content: ""; position: absolute; left: 0; right: 0; top: 8px; height: 2px; background: var(--border-subtle); border-radius: 1px; }
.tuner-scale::after { content: ""; position: absolute; left: 50%; top: 0; width: 2px; height: 18px; background: var(--text-muted); transform: translateX(-50%); }
.tuner-needle { position: absolute; top: 0; left: 50%; width: 4px; height: 18px; margin-left: -2px; background: var(--accent, #19c5d8); border-radius: 2px; transition: left .06s linear; }
.tuner-needle.in-tune { background: #37d67a; }
.tuner-cents, .tuner-hz { text-align: center; font-family: var(--font-mono); font-size: .78rem; color: var(--text-secondary); }
.tuner-game { align-self: center; font-family: var(--font-mono); font-size: .7rem; letter-spacing: .06em; color: var(--text-muted); text-decoration: none; }
.tuner-game:hover { color: var(--accent-blue); }
.tuner-hz { font-size: .68rem; color: var(--text-muted); }
.tuner-controls { display: flex; gap: 8px; align-items: center; margin-top: 2px; }
.tuner-select { flex: 1 1 auto; min-width: 0; }
.tuner-go { flex: 0 0 auto; }
.lang-toggle:hover { color: var(--text-secondary); border-color: var(--border); }
/* One compact identity strip, the live-params panel's own header
   (2026-09-02, Alex: "the first row could entirely fit into the first
   row that has the Patch, including the disconnect button"). The
   identity used to be a whole separate panel with its own heading. */
.id-strip {
  display: flex; align-items: center; gap: 16px;
  /* nowrap + min-width:0 on the row (2026-09-02): with wrap, a device that
     reports a long model string -- Pocket Master's is the whole preset and
     chain order, "Preset: Clone mode, vol 2 (NR > FX1 > DRV > ...)" --
     pushed DESCONECTAR onto a line of its own. The row shrinks and wraps
     internally instead, so the button stays put. */
  flex-wrap: nowrap;
  padding-bottom: 12px; margin-bottom: 12px;
  border-bottom: 1px solid var(--border-subtle);
}
.id-strip .id-row { flex: 1 1 auto; min-width: 0; }
.id-strip .id-row .v { overflow-wrap: anywhere; }
.id-strip .btn { flex: 0 0 auto; }
.id-strip .stepper { display: flex; align-items: center; gap: 6px; }
.id-strip .spacer { flex: 1 1 auto; }

/* 2026-09-22, Alex's live bench, design pass: raw protocol text (Model ID)
   and the bench-test sweep, collapsed by default -- see editor.html's own
   comment on #diagnosticsPanel. A muted, slightly recessed strip (not
   another .amp-face card) so it visually reads as "the same panel's own
   footnote," not a second, equally-important section. */
.diagnostics-panel {
  background: var(--well-edge); border: 1px solid var(--border-subtle);
  border-radius: 8px; padding: 12px 16px; margin-bottom: 12px;
}
.diagnostics-panel .id-row { font-size: 0.78rem; }
.id-row { display: flex; flex-wrap: wrap; gap: 20px; font-family: var(--font-mono); font-size: 0.85rem; }
.id-row .k { color: var(--text-muted); text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.08em; display: block; margin-bottom: 2px; }
.id-row .v { color: var(--text-primary); }
.id-row .v.pending { color: var(--text-muted); font-style: italic; }
/* Density pass 2026-09-03 (Alex: "a little design love and you could make
   it fit in a regular computer screen without having to scroll"). Gaps and
   paddings tightened, and the tile itself shrunk (see .knob/.vbar) -- the PM
   board is 10 cards, so a few px per tile is a row of scrolling. */
.param-grid { display: flex; flex-wrap: wrap; gap: 10px; min-height: 90px; }
.param-grid .empty-hint { color: var(--text-muted); font-size: 0.85rem; font-family: var(--font-body); align-self: center; }

/* ════════════════════════════════════════════════════════════
   PARAM GROUP CARD  (ChromaChainPedal faithful port -- issue #76,
   Katana visual-parity pass, 2026-08-23)
   Accent-tinted corner-to-corner gradient: accentColor 30% -> cardTop ->
   cardBottom, exactly ChromaChainPedal.swift's own recipe. `.neutral`
   (the amp/front-panel section) skips the tint -- ChromaKatanaAmpBlockView
   itself has no accent wash, just individual per-knob colors on a plain
   panelTop/panelBottom background.
   ════════════════════════════════════════════════════════════ */
.param-group {
  --group-accent: var(--accent-blue);
  border-radius: 10px;
  border: 1px solid color-mix(in oklch, var(--group-accent) 35%, transparent);
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--group-accent) 30%, transparent) 0%,
    var(--card-top) 40%, var(--card-bottom) 100%);
  padding: 9px 10px;
  /* 2026-09-02 (Alex: "we could have 4 pedals per row, at least").
     `flex: 1 1 190px` lets four cards share a ~900px panel and grow to
     fill it evenly; a fifth won't fit its own 190px basis, so the row
     wraps at four and widens to five/six on a wider screen. max-width
     keeps a lone card on the last row from stretching across the whole
     panel -- the original reason this used a fixed basis. */
  flex: 1 1 186px;
  min-width: 174px;
  max-width: 280px;
}
/* Unconfirmed/candidate-tier groups (FX LOOP, GAFC -- katana.js's own
   `group: "... (unconfirmed)"` naming) used to sit visually identical to
   the real, confirmed pedal cards next to them (bench 2026-09-22, Alex:
   "other knobs that are not correctly placed... where should it properly
   live?"). They're already structurally separate cards -- the missing
   piece was making that separation visible: dashed border, muted fill,
   order pushed after every confirmed card so the real signal chain reads
   first. Selector matches ANY group whose name ends in "(unconfirmed)",
   not just these two, so a future candidate-tier addition on any device
   gets the same treatment automatically. */
.param-group[data-group-name$="(unconfirmed)"] {
  border-style: dashed;
  border-color: color-mix(in oklch, var(--group-accent) 45%, var(--border));
  background: var(--card-top);
  opacity: 0.8;
  order: 1; /* after every confirmed card (default order: 0) in the same flex row */
}
.param-group[data-group-name$="(unconfirmed)"] .param-group-title::after {
  content: " \26A0"; /* small warning glyph -- readable at the existing title size, no extra markup */
  opacity: 0.7;
}
/* MS-200D+'s "SCREEN" group (ms200dplus.js's own header comment: "Diagnostic-
   only" -- raw undecoded hex, no real structural value) collapses with the
   same Diagnóstico disclosure every device already shares for identity/
   bench-test (#284, bench 2026-09-22). `#paramGrid` gets `.diagnostics-open`
   toggled by main.js's setDiagnosticsOpen() -- default closed, matching
   every other device's own diagnostics content. */
#paramGrid:not(.diagnostics-open) .param-group[data-group-name="SCREEN"] {
  display: none;
}
/* Tighter tile spacing inside a pedal card (Alex, 2026-09-01: "all the
   pedals, taking a LOT LESS space") -- 218px now fits 3 of knob.js's 56px
   knobs per row (3*56 + 2*10 gap = 188px, inside 218 minus padding)
   instead of 2, so a 6-knob amp card is 2 rows tall instead of 3. */
/* A pedal card's top row is its footswitch and its effect name, side by
   side (2026-09-02, Alex: "the effect name could live in the same row as
   the on off light button") -- the LED tells you if it's engaged, the
   selector tells you what it is, and the knobs start on the next row.
   The selector's own "EFFECT" caption is dropped inside a pedal card:
   the card's title already names the block and the selected row already
   names the effect. Scoped away from `.neutral`, whose front-panel row
   lays its controls out side by side instead. */
.param-group:not(.neutral) .footswitch { flex: 0 0 auto; }
.param-group:not(.neutral) .dropdown-tile { flex: 1 1 110px; min-width: 0; align-self: center; }
.param-group:not(.neutral) .dropdown-tile .k-label { display: none; }
.param-group:not(.neutral) .dropdown-select { width: 100%; }

/* ════════════════════════════════════════════════════════════
   SEMAPHORE  (real 3-way colour selector -- Katana BOOST/MOD/FX/DELAY/
   REVERB, semaphore.js. Bench session 2026-09-22, Alex: "The SEMAPHORE is
   still not usable... the colors for the effects types are not color
   coded... I should be able to switch between any 3 mods, one for each
   color." Replaces the 2026-09-08 attempt above (a *Select dropdown +
   3 more colorType dropdowns, none in the amp's own LED colours, none
   showing which was actually live) with one widget: 3 real green/red/
   yellow lights, the live one lit, each with its own assigned-effect
   dropdown right under it. Issues #273/#276/#272. )
   ════════════════════════════════════════════════════════════ */
.semaphore-tile { display: flex; flex-direction: column; align-items: stretch; gap: 6px; flex: 1 1 100%; }

.semaphore-note {
  margin-top: 4px;
  font-size: 11px;
  line-height: 1.25;
  opacity: 0.72;
  text-align: center;
  /* Alex, 2026-10-10: "the boost card's effects names are longer than the frame of the fx". A fixed 24ch
     is WIDER than a narrow card's inner box, and with `text-align: center` the note then overflows both
     sides and the card clips it -- you see the middle of each wrapped line and not its start. Measured:
     "Clean Boost - louder and fuller, no grit; the transparent always-on lift" is ~74 characters, more
     than the 24ch box could ever hold, so it is capped by the card now and wraps inside it. */
  max-width: 100%;
  overflow-wrap: anywhere;
}
/* One row per colour (light + full-width dropdown): three side-by-side selects were ~40px each and showed two characters (Alex, 2026-10-07). */
.semaphore-lights { display: flex; flex-direction: column; gap: 6px; }
.semaphore-cell { display: flex; flex-direction: row; align-items: center; gap: 8px; min-width: 0; }
.semaphore-cell .semaphore-assign { flex: 1 1 0; min-width: 0; text-align: left; }
.semaphore-light {
  width: 22px; height: 22px; border-radius: 50%; padding: 0;
  border: 2px solid color-mix(in oklch, var(--semaphore-color) 45%, var(--border));
  background: radial-gradient(circle at 35% 30%, var(--well-center), var(--well-edge));
  cursor: pointer; transition: all 0.15s ease;
}
.semaphore-light:disabled { cursor: default; }
.semaphore-light.active {
  background: radial-gradient(circle at 35% 30%, var(--semaphore-color), var(--semaphore-color));
  border-color: var(--semaphore-color);
  box-shadow: 0 0 10px var(--semaphore-color);
}
.semaphore-assign {
  font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-primary);
  background: var(--card-top); border: 1px solid color-mix(in oklch, var(--dropdown-accent) 45%, var(--border));
  border-radius: 5px; padding: 3px 4px; width: 100%; cursor: pointer; text-align: center;
}
.semaphore-assign:disabled { cursor: default; opacity: 0.62; }
/* The front panel (Alex, 2026-09-01: "this patch part should be on the
   TOP, horizontal... Katana has a panel with several major knobs on the
   top of the pedals"). A neutral group is, by definition in this app, the
   device's own front panel rather than one stompbox in the chain -- so it
   spans the full grid width and lays its controls out in ONE horizontal
   row, above the pedalboard, instead of sitting in the card flow as if it
   were just another pedal. Applies to every neutral group, so Katana's own
   AMP panel gets the same real front-panel treatment. */
.param-group.neutral {
  border-color: var(--border);
  background: linear-gradient(135deg, var(--panel-top), var(--panel-bottom));
  flex: 1 1 100%;
  order: -1; /* always ahead of the pedal cards, whatever order tiles arrive in */
}
/* Katana: the AMP front panel is a neutral group that is supposed to span the
   full width (rule above), but the generic .param-group max-width: 280px (added
   the next day, for lone pedal cards) also capped it, and its body is nowrap +
   overflow-x: auto -- so on the real screen the AMP card was ONE narrow card with
   a horizontal scrollbar and Treble / Presence / Volume / Cab Res / Contour /
   Solo / Solo Level / Chain Routing were scrolled out of sight (Alex bench
   2026-09-21: "we're not showing" solo, cab resonance, contour). Scoped to
   Katana because other families' neutral cards (PM SAVE, Valeton PATCH) have
   only been seen at pedal-card width. */
#paramGrid[data-family="katana"] .param-group.neutral { max-width: none; }
.param-group.neutral .param-group-body {
  flex-wrap: nowrap;
  overflow-x: auto;
  align-items: flex-end;
  gap: 22px;
}
/* Katana AMP panel: real 3-row split (#277, bench 2026-09-23, design
   canvas sign-off) -- discrete pills (AMP TYPE/CAB RES/CONTOUR/CHAIN
   ROUTING), then the real continuous knobs (GAIN..VOLUME) on their own
   row, then SOLO. Replaces the single nowrap+horizontal-scroll row above
   (Alex, 2026-09-21: "BLEND wraps into BLEN/D, knobs unreadable" -- the
   symptom this specifically fixes) with real wrapping at 3 deliberate
   break points, not wherever the browser happens to run out of room.
   `order` + a flex-basis:100% pseudo-element per break (the standard
   flexbox force-wrap technique: `::before`/`::after` on a flex container
   are themselves real flex items, so exactly 2 of them make exactly 2
   break points = 3 rows) -- keyed off render.js's own real
   `data-param-key` attribute (already set on every tile), so this needs
   no render.js change and can't silently drift from which key is which
   real PARAMS entry. Scoped to Katana only, same as the rule above. */
#paramGrid[data-family="katana"] .param-group.neutral .param-group-body {
  flex-wrap: wrap;
  overflow-x: visible;
}
#paramGrid[data-family="katana"] .param-group.neutral .param-group-body::before {
  content: ""; flex-basis: 100%; order: 5;
}
#paramGrid[data-family="katana"] .param-group.neutral .param-group-body::after {
  content: ""; flex-basis: 100%; order: 12;
}
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:ampType"] { order: 1; }
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:cabResonance"] { order: 2; }
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:contour"] { order: 3; }
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:chainRouting"] { order: 4; }
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:gain"] { order: 6; }
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:bass"] { order: 7; }
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:mid"] { order: 8; }
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:treble"] { order: 9; }
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:presence"] { order: 10; }
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:volume"] { order: 11; }
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:soloActive"] { order: 13; }
#paramGrid[data-family="katana"] .param-group.neutral [data-param-key="katana:soloLevel"] { order: 14; }
/* Any FUTURE neutral-group tile with no explicit order above (a new real
   confirmed param added later) keeps its natural DOM order and lands in
   the last row rather than silently vanishing or colliding with a
   numbered slot -- `order` defaults to 0, which would sort BEFORE every
   explicit value here, so this gives unlisted tiles a high default
   instead. The `[data-param-key=...]` rules above always win regardless
   of stylesheet order: an attribute selector is more specific than this
   plain child combinator, so a listed tile is never caught by this. */
#paramGrid[data-family="katana"] .param-group.neutral .param-group-body > * {
  order: 20;
}

/* Katana-only: native's own ChromaKatanaBroadView lays its 6 real
   chain-position blocks (Booster/Mod/FX/Delay/Reverb/Delay2) out as ONE
   full-width row that shrinks each card to fit rather than wrapping to a
   second row (2026-09-08, Alex: "the pedals in one single row... we can
   use the entire width of the screen"). The generic `.param-group`
   recipe above (flex-basis 186px, capped at 280px) was tuned for other
   devices' "4ish cards, wrap freely" layout -- scoped here via
   `data-family` (set by main.js's setFamily()) rather than by group name,
   since katanaMk1/gt1000/gt1b reuse the exact same group names
   (AMP/DELAY/REVERB/etc.) for unrelated card sets. `flex: 1 1 0` with no
   basis lets all 6 share the row exactly evenly; the 140px floor still
   wraps to a second row rather than crushing a card unreadably on a
   truly narrow window -- native's own equivalent floor before it falls
   back to horizontal scroll. */
#paramGrid[data-family="katana"] .param-group:not(.neutral) {
  /* Alex, 2026-10-10: "make the row fit the window." The row wraps (`.param-grid` is flex-wrap: wrap),
     but a card could still exceed it: `min-width: 140px` is a FLOOR in the wrong place -- it stops a card
     shrinking, so a card whose own content needs more (a knob row is 3x56px + gaps, plus padding) pushes
     past the container and the end of the row runs off the window. A preferred WIDTH that may shrink is
     what is wanted: cards keep 140px whenever there is room, and give way when there is not. */
  flex: 1 1 140px;
  min-width: 0;
  max-width: 100%;
}

/* A bypassed pedal reads as unlit next to a lit one -- render.js's
   setGroupBypassed(), driven by the device's own real decoded enable
   bits. Same visual grammar as .knob:not(.writable): dim + desaturate,
   never hide. */
.param-group.bypassed {
  opacity: 0.5;
  filter: saturate(0.25);
}
.param-group-title {
  font-family: var(--font-display); font-weight: 600; font-size: 0.72rem;
  letter-spacing: 0.12em; color: var(--group-accent); text-transform: uppercase;
  margin-bottom: 10px;
}
.param-group.neutral .param-group-title { color: var(--text-muted-dim); }
/* 3 knobs per row (Alex, 2026-09-02: "each pedal could have 3 knobs per
   row"). A card at its 190px basis grows to ~215px in a 4-up row; minus
   24px of padding that leaves ~191px of body, and 3 of knob.js's 56px
   knobs plus two 9px gaps is 186px. So a 6-knob amp card is 2 rows
   tall, not 3. */
.param-group-body { display: flex; flex-wrap: wrap; gap: 9px; align-items: flex-start; }

/* Drag-and-drop pedal reorder (issue #76 1056a, PM only) -- the title bar
   is the drag handle (not the whole card), so knob dragging inside the
   card's body stays untouched. */
.param-group.reorderable .param-group-title { cursor: grab; }
.param-group.reorderable.dragging { opacity: 0.4; }
.toolbar-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
.toolbar-row .spacer { flex: 1; }
.toolbar-row .count { font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); }

/* Patch library / channel browser (issue #76 comment 1360) -- read-only
   card grid, same family as .param-group's own card recipe. */
/* 2026-09-22, Alex's live bench: "i need the patch selector to be a tad
   smaller, and fit in one single row" -- Katana's 8 slots wrapped to two rows
   at the old 150px/10px sizing. Shrunk to fit 8 on one row on a normal desktop
   width now that .container above is no longer capped; still wraps on a
   narrow window rather than overflowing, same as before. */
.patch-library-grid { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.patch-tile {
  flex: 0 1 110px; min-width: 96px;
  background: linear-gradient(135deg, var(--card-top), var(--card-bottom));
  border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px;
}
.patch-tile-code {
  font-family: var(--font-mono); font-size: 0.65rem; letter-spacing: 0.08em;
  color: var(--text-muted-dim); text-transform: uppercase; margin-bottom: 3px;
}
.patch-tile-name { font-family: var(--font-body); font-size: 0.8rem; color: var(--text-primary); }
.patch-tile-type { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-muted); margin-top: 2px; }
/* Dev-only surfaces (#430, Alex 2026-10-10: the diagnosis tools, bench test and the amp-writing
   buttons should not be in a normal visitor's way -- "if someone intentionally goes to the debug,
   intentionally clicks on write to my pedal, their problem"). Hidden by DEFAULT in CSS, not by JS,
   so there is no frame where a visitor sees them before a script runs, and `!important` so a device
   module that sets `style.display` inline (main.js does, per device) cannot re-show them behind the
   gate's back. body.dev-mode (set from ?debug=1 -- see js/lib/devGate.js) lets them through. */
body:not(.dev-mode) .dev-only { display: none !important; }

/* issue #368: Pocket Master's 100-tile grid is the only one whose subtitle
   field ever carried a full sentence (a device-wide status wrongly written
   into every tile, fixed in pm.js to use the hint line instead). Scoped to
   just this grid (`.patch-library-grid--pm`, set by patchLibrary.js from
   main.js's own device family) as a backstop so a long subtitle wraps onto
   its own lines instead of overflowing the 96px tile -- no other device's
   grid carries this modifier, so nothing else is restyled. */
.patch-library-grid--pm .patch-tile-name,
.patch-library-grid--pm .patch-tile-type { overflow-wrap: anywhere; word-break: break-word; }
.patch-tile .btn { padding: 7px 8px; font-size: 0.7rem; margin-top: 6px; width: 100%; }
.patch-library-hint {
  font-family: var(--font-mono); font-size: 0.8rem; color: var(--accent-amber);
  background: var(--accent-amber-fill); border: 1px solid var(--accent-amber); border-radius: 8px;
  margin-top: 10px; padding: 8px 12px;
}
.patch-library-test-log {
  font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-secondary);
  background: var(--card-bottom); border: 1px solid var(--border); border-radius: 8px;
  margin-top: 10px; padding: 10px 12px; max-height: 220px; overflow-y: auto;
  white-space: pre-wrap; word-break: break-word;
}
.warn {
  background: oklch(0.24 0.06 60); border: 1px solid oklch(0.45 0.10 60); color: oklch(0.85 0.12 80);
  border-radius: 8px; padding: 10px 12px; font-size: 0.85rem; margin: 10px auto 0; max-width: 900px; display: none; font-family: var(--font-body);
}
.toast {
  position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
  background: oklch(0.22 0.08 25); border: 1px solid oklch(0.45 0.14 25); color: oklch(0.88 0.12 40);
  border-radius: 8px; padding: 10px 16px; font-size: 0.85rem; font-family: var(--font-body);
  z-index: 100; box-shadow: 0 4px 16px rgba(0,0,0,0.4);
  opacity: 0; transition: opacity 0.2s ease;
}
.toast.show { opacity: 1; }
.foot-note { color: var(--text-muted); font-size: 0.78rem; font-family: var(--font-body); text-align: center; padding: 10px; }

/* Orientation banner on the connect screen (2026-09-22): says what this
   page is and that using it is logged, before anything else. */
.intro-badge {
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.04em;
  color: var(--accent-amber); background: var(--accent-amber-fill);
  border: 1px solid var(--accent-amber); border-radius: 999px;
  display: inline-block; padding: 4px 12px; margin: 0 auto 10px;
}
.intro-telemetry { padding: 0; margin-bottom: 14px; font-size: 0.74rem; line-height: 1.4; }
.login-row {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  margin-bottom: 16px; flex-wrap: wrap;
}
.login-row .foot-note { padding: 0; }

/* ════════════════════════════════════════════════════════════
   MODE NAV + generic mode-scoped panels (Translate/Setlists/
   Guitar/Settings)
   ════════════════════════════════════════════════════════════ */
/* top: was a hardcoded "41px" guess at .titlebar's own rendered height --
   real bug (Alex, live testing): whenever the titlebar actually rendered
   taller than that guess (slow web-font swap, a different OS font
   baseline, etc.), this bar would sit UNDER the titlebar once both went
   sticky on a scrolled, connected device screen (z-index 9 vs 10), which
   looked exactly like the nav disappearing -- see main.js's
   syncTitlebarHeight() for the real, measured value this var now holds. */
#modeNav { position: sticky; top: var(--titlebar-h, 41px); z-index: 9; }
/* Livesets screen (2026-10-13): the action row used to be a non-wrapping flex that ran under the second panel. */
.translate-panel { min-width: 0; }
.liveset-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; align-items: stretch; }
.liveset-actions .btn { flex: 0 1 auto; min-width: 0; white-space: normal; }
.liveset-hint { flex: 1 1 100%; margin: 0; }
.liveset-install { flex: 1 1 100%; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.translate-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--gap); max-width: 1200px; margin: 20px auto; padding: 0 20px; align-items: start; }
@media (max-width: 900px) { .translate-grid { grid-template-columns: 1fr; } }
.translate-panel { display: flex; flex-direction: column; gap: 10px; min-height: 200px; }
/* Real bug, live testing today (Alex, Translate wizard Step 1 -- "What
   are you playing today?"): device buttons rendered as broken segments,
   some showing icon-like glyph fragments instead of full device names
   ("Device | [icon] | Katana | [icon] | Zoom | ..."). Root cause: this
   element carries BOTH .segmented (a container-level border-radius:pill
   + overflow:hidden -- the classic "one fused pill, straight inner
   edges" look, which only reads correctly for a SINGLE ROW of buttons)
   AND .device-picker's own flex-wrap:wrap. The moment this specific
   picker wraps onto more than one row (4 devices, a narrower panel/
   window than whatever this was designed against), the single giant
   corner radius gets carved into a now-MULTI-ROW block and clips real
   chunks off whichever button lands nearest a corner -- a clipped
   label can read as a stray icon-like fragment sitting beside the rest
   of a name rather than one complete, readable button. Cancels
   .segmented's container-level pill/clipping here and gives each
   button its OWN independent rounded pill instead (same idea as
   .wizard-target-btn's own per-button card styling in Step 2, not a
   new pattern) -- every device stays one whole, fully visible button
   no matter how many rows it wraps to. */
.device-picker { display: flex; flex-wrap: wrap; width: 100%; gap: 6px; overflow: visible; border: none; border-radius: 0; }
.device-picker button { flex: 1; white-space: nowrap; border-radius: var(--radius-pill); border: 1px solid var(--border); }
input[type="text"], input[type="search"] {
  width: 100%; padding: 8px 10px; font-size: 0.85rem; font-family: var(--font-body);
  background: var(--cell-bg); color: var(--text-primary);
  border: 1px solid var(--border); border-radius: 8px;
}
.preset-list { display: flex; flex-direction: column; gap: 6px; max-height: 320px; overflow-y: auto; }
.preset-item {
  padding: 8px 10px; border-radius: 8px; border: 1px solid var(--border-subtle);
  background: var(--tile-top); cursor: pointer; transition: all 0.1s ease;
}
.preset-item:hover { border-color: var(--border); }
.preset-item.selected { border-color: var(--accent-blue); background: var(--accent-blue-fill); }
.preset-item .p-name { font-family: var(--font-display); font-weight: 600; font-size: 0.85rem; color: var(--text-primary); }
.preset-item .p-tags { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-muted); margin-top: 2px; }
.effect-row {
  display: flex; justify-content: space-between; padding: 6px 10px; margin-bottom: 4px;
  background: var(--cell-bg); border-radius: 6px; font-size: 0.8rem;
}
.effect-row .e-count { color: var(--text-muted); font-family: var(--font-mono); }
.param-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.param-row .p-label { width: 100px; font-family: var(--font-display); font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted); flex-shrink: 0; }
.param-row .p-bar { flex: 1; height: 6px; background: var(--knob-track); border-radius: 3px; overflow: hidden; }
.param-row .p-fill { height: 100%; background: var(--accent-blue); }
.param-row .p-pct { width: 36px; text-align: right; font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-secondary); }
.compat-badge { display: flex; align-items: center; gap: 6px; font-size: 0.8rem; padding: 6px 0; }
.compat-badge.good { color: var(--led-connected); }
.compat-badge.bad { color: var(--accent-amber); }

/* ── Translate Phase 3 wizard (2026-09-07, issue #76 comment 1475
   Question 2) -- new classes only, existing .translate-grid/.panel/
   .segmented/.btn/.compat-badge above stay untouched (Setlists/Library/
   Guitar/Chocolate share them). Verdict tier colors reuse the existing
   --accent-* custom properties rather than inventing a new palette:
   full=green, partial=amber, none=red. ── */
.tone-wizard { max-width: 1200px; margin: 20px auto; padding: 0 20px; display: flex; flex-direction: column; gap: 14px; }
.wizard-steps { display: flex; gap: 6px; }
.wizard-step-tab {
  flex: 1; padding: 10px 14px; font-family: var(--font-display); font-weight: 600;
  font-size: 0.78rem; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--text-muted); background: var(--card-top); border: 1px solid var(--border-subtle);
  border-radius: var(--radius-card); cursor: pointer; transition: all 0.1s ease;
}
.wizard-step-tab:hover { color: var(--text-secondary); }
.wizard-step-tab.active { color: var(--accent-blue); border-color: var(--accent-blue); background: var(--accent-blue-fill); }
.wizard-step { display: flex; flex-direction: column; gap: 14px; }
.wizard-back {
  font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-muted);
  background: none; border: none; cursor: pointer; padding: 0 0 8px 0; text-align: left;
}
.wizard-back:hover { color: var(--text-secondary); }

.preset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; margin-top: 10px; }
.preset-card {
  position: relative; padding: 10px 12px; border-radius: var(--radius-card); border: 1px solid var(--border-subtle);
  background: var(--tile-top); cursor: pointer; transition: all 0.1s ease; display: flex; flex-direction: column; gap: 8px;
}
.preset-card:hover { border-color: var(--accent-blue); }
.preset-card .p-name { font-family: var(--font-display); font-weight: 600; font-size: 0.9rem; color: var(--text-primary); padding-right: 24px; }
.preset-card .p-tags { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-muted); }
.wizard-add-lib {
  position: absolute; top: 8px; right: 8px; width: 22px; height: 22px; padding: 0;
  border-radius: 999px; font-size: 0.85rem; line-height: 1;
}

.pedalboard-strip { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.pedalboard-strip.mini .stompbox-chip { font-size: 0.65rem; padding: 3px 8px; }
.pedalboard-connector { text-align: center; color: var(--text-muted); font-size: 1rem; }
.stompbox-chip {
  display: inline-block; padding: 5px 10px; border-radius: var(--radius-pill);
  font-family: var(--font-display); font-size: 0.72rem; font-weight: 600;
  border: 1px solid var(--border); color: var(--text-secondary); background: var(--card-top);
}
.stompbox-chip.tier-full { color: var(--accent-green); border-color: var(--accent-green); background: var(--accent-green-fill); }
.stompbox-chip.tier-partial { color: var(--accent-amber); border-color: var(--accent-amber); background: var(--accent-amber-fill); }
.stompbox-chip.tier-none { color: var(--text-muted); border-color: var(--border-subtle); background: var(--card-bottom); text-decoration: line-through; }

.wizard-target-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; }
.wizard-target-btn {
  display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 10px;
  border-radius: var(--radius-card); border: 1px solid var(--border); background: var(--card-top);
  color: var(--text-primary); cursor: pointer; font-family: var(--font-display); font-size: 0.85rem; font-weight: 600;
}
.wizard-target-btn.active { border-color: var(--accent-blue); background: var(--accent-blue-fill); color: var(--accent-blue); }
.wizard-target-btn .device-badge {
  font-family: var(--font-mono); font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted);
}
.wizard-target-btn .device-badge.real-driver { color: var(--accent-blue); }
.wizard-target-btn .device-badge.connected { color: var(--led-connected); }

.wizard-headline { font-family: var(--font-display); font-weight: 600; font-size: 0.95rem; color: var(--text-primary); text-align: center; margin: 4px 0; }
.wizard-effect-detail { padding: 10px 12px; border-radius: 8px; border-left: 3px solid var(--border); background: var(--cell-bg); margin-bottom: 8px; }
.wizard-effect-detail.tier-full { border-left-color: var(--accent-green); }
.wizard-effect-detail.tier-partial { border-left-color: var(--accent-amber); }
.wizard-effect-detail.tier-none { border-left-color: var(--text-muted); opacity: 0.85; }
.wizard-effect-detail .wed-title { font-family: var(--font-display); font-weight: 600; font-size: 0.82rem; color: var(--text-primary); margin-bottom: 6px; }
.wizard-effect-detail .wed-tag { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-muted); text-transform: uppercase; margin-left: 6px; }
.wizard-slider-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.wizard-slider-row .ws-label { width: 100px; font-family: var(--font-display); font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted); flex-shrink: 0; }
.wizard-slider-row input[type="range"] { flex: 1; accent-color: var(--accent-blue); }
.wizard-slider-row .ws-pct { width: 36px; text-align: right; font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-secondary); }
.wizard-substitute-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 0.78rem; }
.wizard-substitute-row select { font-family: var(--font-body); font-size: 0.78rem; padding: 4px 6px; background: var(--card-bottom); color: var(--text-primary); border: 1px solid var(--border); border-radius: 6px; }
.wizard-substitute-row label { display: flex; align-items: center; gap: 4px; color: var(--text-muted); }

.wizard-live-warning { color: var(--accent-amber); font-size: 0.82rem; padding: 8px 10px; border: 1px solid var(--accent-amber); border-radius: 8px; background: var(--accent-amber-fill); margin-bottom: 10px; }
.wizard-checklist { display: flex; flex-direction: column; gap: 4px; max-height: 220px; overflow-y: auto; margin-top: 10px; }
.wizard-checklist-row { font-family: var(--font-mono); font-size: 0.75rem; padding: 3px 0; }
.wizard-checklist-row.pushed { color: var(--text-secondary); }
.wizard-checklist-row.skipped { color: var(--text-muted); }
.wizard-save-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0; }
.dial-in-sheet { margin-top: 10px; padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--card-top); }
.dial-in-sheet h3 { font-family: var(--font-display); font-size: 0.9rem; margin: 0 0 10px 0; color: var(--text-primary); }
.dial-sheet-effect { margin-bottom: 8px; }
.dial-sheet-effect strong { font-family: var(--font-display); font-size: 0.82rem; color: var(--text-primary); }
.dial-sheet-param { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-secondary); padding-left: 10px; }
@media print {
  body > *:not(.tone-wizard), .tone-wizard > *:not(#wizStep3), .wizard-steps, .wizard-back,
  #wizStep3Body > *:not(#wizDialSheet) { display: none !important; }
}
.list-row { display: flex; align-items: center; gap: 8px; padding: 8px; border-radius: 8px; background: var(--tile-top); margin-bottom: 6px; }
.list-row .l-name { flex: 1; font-family: var(--font-display); font-weight: 600; font-size: 0.85rem; }
.list-row .l-meta { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-muted); }
.form-row { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
.form-row label { font-family: var(--font-display); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
.toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--border-subtle); }
.toggle-row:last-child { border-bottom: none; }

/* GT-1B hardware test panel (2026-08-26, Bruno's session) */
.hwtest-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid var(--border-subtle); border-radius: 6px; }
.hwtest-row .hw-status { min-width: 90px; font-family: var(--font-mono); font-size: 0.75rem; }
.hwtest-row button { padding: 2px 10px; font-size: 0.75rem; }


/* Setlist write progress (2026-09-02, Alex: "add some kind of loader when
   you're writing, specially if it's a long write"). An 18-slot write is
   about a minute of BLE traffic with nothing else to look at, and silence
   for a minute is indistinguishable from a hang. */
.setlist-progress-track {
  height: 6px; border-radius: 3px; overflow: hidden;
  background: var(--card-bottom); border: 1px solid var(--border-subtle);
}
.setlist-progress-fill {
  height: 100%; width: 0%;
  background: var(--accent-green);
  box-shadow: 0 0 8px var(--accent-green);
  transition: width 0.25s ease;
}
/* Anything mid-write is not safe to press again. */
.btn:disabled { opacity: 0.45; cursor: not-allowed; }


/* Song -> device location rows (2026-09-02). A song has one identity and
   many homes; this is the table that says where. */
.panel-subtitle {
  font-family: var(--font-display); font-weight: 600; font-size: 0.68rem;
  letter-spacing: 0.12em; color: var(--text-muted-dim); text-transform: uppercase;
  margin-bottom: 8px;
}
.binding-row {
  display: flex; align-items: center; gap: 12px;
  padding: 6px 0; border-bottom: 1px solid var(--border-subtle);
  font-family: var(--font-mono); font-size: 0.82rem;
}
.binding-row:last-child { border-bottom: none; }
.binding-row .b-device { flex: 0 0 130px; color: var(--text-secondary); }
.binding-row .b-where { flex: 0 0 110px; color: var(--text-primary); }
.binding-row .b-src { flex: 1 1 auto; color: var(--text-muted); font-size: 0.72rem; }
/* A device the app genuinely cannot move -- the Katana's MIDI channel
   select is confirmed dead on hardware. Shown, never actioned. */
.binding-row.manual .b-where { color: var(--accent-amber); }
.binding-row.unbound .b-where { color: var(--text-muted); font-style: italic; }


/* ════════════════════════════════════════════════════════════
   HEADER-STRIP CONTROLS  (setHeaderControl, main.js)
   Preset-level facts that are not pedals -- the Pocket Master's AMP MODE
   and preset volume. They used to be a full-width card of their own, which
   on a 10-card board cost an entire row.
   ════════════════════════════════════════════════════════════ */
.id-controls { display: flex; gap: 18px; align-items: flex-start; }
.id-controls > div { display: flex; flex-direction: column; }
.hc-select {
  font-family: var(--font-mono); font-size: 0.85rem;
  color: var(--text-primary); background: var(--card-bottom);
  border: 1px solid var(--border); border-radius: 6px;
  padding: 1px 6px; cursor: pointer;
}
.hc-select:hover { border-color: var(--accent-blue); }

/* ════════════════════════════════════════════════════════════
   KATANA STAGE LAYOUT (Alex, 2026-10-07: "make it playable in one screen")
   Everything below is scoped to body[data-family="katana"] (set by main.js applyFamilyChrome), so no other
   device changes. Goal: channel buttons, amp row and every effect block visible together at 1280x800
   with no scrolling. The not-yet-confirmed groups (FX loop, noise gate, GAFC) are hidden until "?" is pressed.
   ════════════════════════════════════════════════════════════ */
body[data-family="katana"] .titlebar { padding: 6px 16px; }
body[data-family="katana"] .titlebar .brand { font-size: 1.05rem; }
body[data-family="katana"] .tabs { padding: 0 16px; }
body[data-family="katana"] .tab { padding: 6px 14px; font-size: 0.78rem; }
body[data-family="katana"] .container { padding-top: 8px; padding-bottom: 8px; }
body[data-family="katana"] .panel { padding: 8px 12px; margin-bottom: 8px; }
body[data-family="katana"] #btnUnconfirmed { display: inline-flex; }
body:not([data-family="katana"]) #btnUnconfirmed { display: none; }

/* channel buttons: one slim row, the whole tile is the target */
body[data-family="katana"] #patchLibraryPanel .panel-title,
body[data-family="katana"] #patchLibraryPanel .toolbar-row .count,
body[data-family="katana"] #patchLibraryPanel .patch-library-hint { display: none; }
body[data-family="katana"] #patchLibraryPanel .toolbar-row { margin: 0; justify-content: flex-end; gap: 6px; }
body[data-family="katana"] #patchLibraryPanel .toolbar-row .btn { padding: 2px 8px; font-size: 0.68rem; }
body[data-family="katana"] .patch-library-grid { flex-wrap: nowrap; margin-top: 4px; gap: 6px; }
body[data-family="katana"] .patch-tile { flex: 1 1 0; min-width: 0; padding: 5px 7px; position: relative; }
/* The amp label is the tile's payload, not decoration, so it must be readable whole.
   It carried `white-space: nowrap` + ellipsis, which cut 7 of the 8 tiles on Alex's bench (2026-10-10):
   "Crunch · COMBO CR...", "Lead · HIGAIN STA...". Nine `flex: 1 1 0` columns cannot fit a 21-character
   label on one line at any readable size -- measured: "Crunch · COMBO CRUNCH" needs ~128px at 0.62rem
   mono, the tile offers ~96px -- so it wraps onto a second line (the same treatment issue #368 gave the
   Pocket Master grid) and the tile grows with it. The patch NAME used to clip at 2.3em for the same
   reason; PANEL's own hint line is three lines long, so it is allowed to be. */
body[data-family="katana"] .patch-tile-name { font-size: 0.78rem; line-height: 1.15; overflow-wrap: anywhere; word-break: break-word; }
body[data-family="katana"] .patch-tile-type { font-size: 0.62rem; margin-top: 1px; overflow-wrap: anywhere; word-break: break-word; }
body[data-family="katana"] .patch-tile .btn { width: 100%; margin-top: 4px; padding: 3px 4px; font-size: 0.62rem; }

/* amp panel + toolbar */
body[data-family="katana"] .amp-face { padding: 8px 12px; }
body[data-family="katana"] .id-strip { padding: 0; margin: 0 0 6px; gap: 6px; justify-content: flex-end; }
body[data-family="katana"] .id-strip .btn { padding: 3px 10px; font-size: 0.68rem; }
body[data-family="katana"] #paramGrid { gap: 8px; min-height: 0; }
body[data-family="katana"] #paramGrid .param-group.neutral { padding: 6px 10px; }
body[data-family="katana"] #paramGrid .param-group.neutral .param-group-body { gap: 14px; align-items: flex-end; }

/* effect blocks: 6 side by side, knobs two to a row */
body[data-family="katana"] #paramGrid .param-group:not(.neutral) {
  flex: 1 1 150px; min-width: 150px; max-width: none; padding: 6px 8px;
}
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .param-group-body {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 6px; align-items: start; justify-items: center;
}
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .knob svg { width: 36px; height: 36px; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .knob .k-label { font-size: 0.56rem; letter-spacing: 0.04em; text-align: center; line-height: 1.1; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .knob .k-value { font-size: 0.66rem; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .semaphore-tile,
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .dropdown-tile { grid-column: 1 / -1; }
body[data-family="katana"] .semaphore-assign { font-size: 0.62rem; padding: 2px 3px; }
body[data-family="katana"] .semaphore-lights { gap: 3px; }
body[data-family="katana"] .param-group-title { font-size: 0.66rem; margin-bottom: 4px; }

/* -- Zoom G5n: make the patch identity readable -----------------------------
   Alex, 2026-10-09: "fix this layout so we can read the patch info correctly".
   The PATCH card carried a tile labelled PATCH NAME whose text clipped to
   "PATCH I", a patch name clipped to "ETERna", and a tall empty area under both
   -- the card was a narrow grid cell stretched to the row's height by the flex
   default (align-items: stretch on .param-grid, where the tallest card is an FX
   card holding a full knob set).

   The patch's identity is worth the full width: it is prose (a name, a bank and
   a patch position), not a peer of a knob. */
body[data-family="g5n"] #paramGrid .param-group[data-group-name="PATCH"] {
  flex: 1 1 100%; max-width: none;
}
body[data-family="g5n"] #paramGrid .param-group[data-group-name="PATCH"] .param-group-body {
  display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: center;
  min-height: 0;
}

/* Cards size to their own content instead of the tallest card in the row. */
#paramGrid { align-items: flex-start; }

/* The raw byte-diff card ("OFFSET 4 / 13 / 29...", one per non-zero byte) is a
   debugging surface, not something to read the pedal by. Alex, 2026-10-09: "a thousand
   debug fx there" -- they were the anonymous DEBUG group rendering unconditionally while
   every other diagnostic surface was already behind DIAGNOSTICO. Same mechanism: off
   until asked for. */
#paramGrid:not(.diagnostics-open) .param-group[data-group-name="DEBUG"] { display: none; }

/* ── MS-200D+: the native app's own organisation ────────────────────────────
   Alex, 2026-10-09: "can we have a layout org similar to the native app? here it
   looks a lot more confusing". The native editor shows the patch bar and the
   effect pedals, and puts everything device-level behind one DEVICE CONTROLS
   button. The web showed all nine cards at once, each capped at 280px by the
   generic .param-group rule, so knobs and the patch description were squeezed to
   one word per line (also issue #553).

   Same mechanism as the SCREEN group above and as the Katana's
   .show-unconfirmed rule: hide by default, reveal from the existing
   diagnostics toggle -- device-level controls ARE diagnostics, so this needs no
   new button and no JS. */
body[data-family="ms200dplus"]:not(.device-controls-open) #paramGrid .param-group[data-group-name="FILE LIST"],
body[data-family="ms200dplus"]:not(.device-controls-open) #paramGrid .param-group[data-group-name="CHAIN EDIT"] { display: none; }

/* #619 (Alex, 2026-10-12: "this controles do dispositivo could live at the top bar. smaller font and a little bit
   of design love"): DEVICE / TUNER / DEVICE CONFIG are the rig's state -- how it is set, not a place you visit -- so
   they are always visible as one compact strip ahead of the patch and the pedals, in smaller type. FILE LIST and
   CHAIN EDIT are actions, and stay behind the button. */
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE"],
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="TUNER"],
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE CONFIG"] {
  order: -2; flex: 1 1 auto; min-width: 0; max-width: none; padding: 6px 10px 8px;
}
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE"] .param-group-title,
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="TUNER"] .param-group-title,
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE CONFIG"] .param-group-title {
  font-size: 0.58rem; letter-spacing: 0.1em; margin: 0 0 4px;
}
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE"] .param-group-body,
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="TUNER"] .param-group-body,
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE CONFIG"] .param-group-body {
  flex-wrap: wrap; overflow: visible; gap: 4px 12px; align-items: flex-end;
}
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE"] .knob svg,
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="TUNER"] .knob svg,
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE CONFIG"] .knob svg { width: 32px; height: 32px; }
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE"] .k-value,
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="TUNER"] .k-value,
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE CONFIG"] .k-value { font-size: 0.64rem; }
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE"] .dropdown-select,
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="TUNER"] .dropdown-select,
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="DEVICE CONFIG"] .dropdown-select { font-size: 0.68rem; padding: 2px 5px; }

/* Nothing on this screen renders truncated: a label keeps its words whole and its tile grows to fit the longest
   one, instead of wrapping mid-word ("INSERT E / AT SLOT" on the SET EFFECT card, same class as the Pocket Master's
   SAVE AS, #596). */
body[data-family="ms200dplus"] #paramGrid .param-group .k-label {
  white-space: normal; overflow-wrap: normal; word-break: normal; hyphens: none; min-width: min-content; line-height: 1.15;
}
body[data-family="ms200dplus"] #paramGrid .param-group .knob,
body[data-family="ms200dplus"] #paramGrid .param-group .dropdown-tile,
body[data-family="ms200dplus"] #paramGrid .param-group .text-tile { min-width: min-content; }

/* The patch and its effect slots are the work: give them the width the generic
   280px cap denies them, so knobs sit in a readable row and the patch
   description reads as a sentence rather than one word per line. */
body[data-family="ms200dplus"] #paramGrid .param-group:not(.neutral) { flex: 1 1 320px; min-width: 300px; max-width: none; }
body[data-family="ms200dplus"] #paramGrid .param-group:not(.neutral) .param-group-body {
  display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: flex-start;
}
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="PATCH"],
body[data-family="ms200dplus"] #paramGrid .param-group[data-group-name="PATCH LIBRARY"] { flex: 1 1 100%; max-width: none; }

/* hidden until "?" (see main.js): groups whose addresses are not confirmed on hardware */
body[data-family="katana"]:not(.show-unconfirmed) .param-group[data-group-name$="(unconfirmed)"] { display: none; }
body[data-family="katana"] #paramGrid .param-group.neutral .param-group-title { margin: 0 0 -14px; position: relative; z-index: 1; }
body[data-family="katana"] #paramGrid .param-group.neutral .dropdown-tile .dropdown-select { font-size: 0.78rem; padding: 3px 6px; }
body[data-family="katana"] .patch-tile .btn { margin-top: 2px; padding: 2px 4px; }
body[data-family="katana"] .patch-tile-code { margin-bottom: 0; }
/* whole tile is the button; the live channel glows */
body[data-family="katana"] .patch-tile .btn { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
body[data-family="katana"] .patch-tile { cursor: pointer; transition: border-color 0.12s, box-shadow 0.12s; }
body[data-family="katana"] .patch-tile:hover { border-color: var(--accent-blue); }
body[data-family="katana"] .patch-tile.live { border-color: var(--accent-green); box-shadow: 0 0 10px color-mix(in oklch, var(--accent-green) 55%, transparent); background: color-mix(in oklch, var(--accent-green) 14%, var(--card-top)); }
body[data-family="katana"] .patch-tile.live .patch-tile-code::after { content: " \25CF LIVE"; color: var(--accent-green); }
/* effect blocks: three knobs to a row */
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .param-group-body { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* toolbar floats over the empty right half of the AMP title row instead of costing its own row */
body[data-family="katana"] .amp-face { position: relative; }
body[data-family="katana"] .amp-face > .id-strip { position: absolute; top: 10px; right: 14px; margin: 0; z-index: 2; }
body[data-family="katana"] .panel { margin-bottom: 6px; }
/* stage: block switch moves up into the title row; semaphore keeps only its three rows; bank lists live in the channel tiles */
body[data-family="katana"] #paramGrid .param-group:not(.neutral) { position: relative; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .param-group-body > .footswitch:first-child { position: absolute; top: 3px; right: 6px; margin: 0; z-index: 2; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .param-group-body > .footswitch:not(:first-child) { grid-column: auto; justify-self: center; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .param-group-body > .footswitch:not(:first-child) .fs-label { display: block; font-size: 0.56rem; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .param-group-body > .footswitch:first-child .fs-label { display: none; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .param-group-title { margin-bottom: 4px; min-height: 22px; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .semaphore-tile > :first-child:not(.semaphore-lights) { display: none; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .semaphore-tile { min-width: 0; overflow: hidden; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .semaphore-cell { min-width: 0; }
body[data-family="katana"] #paramGrid .param-group[data-group-name^="PATCHES"] { display: none; }
body[data-family="katana"] .panel { padding: 6px 10px; }
body[data-family="katana"] .container { padding-top: 6px; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .param-group-body { gap: 2px 6px; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .knob { gap: 1px; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .knob .k-value { line-height: 1.1; white-space: nowrap; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .semaphore-assign { padding: 1px 3px; font-size: 0.6rem; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .dropdown-tile { min-width: 0; max-width: 100%; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .dropdown-select { min-width: 0; max-width: 100%; font-size: 0.7rem; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .text-tile { grid-column: 1 / -1; min-width: 0; max-width: 100%; }
body[data-family="katana"] #paramGrid .param-group:not(.neutral) .text-tile * { font-size: 0.72rem; }
body[data-family="katana"] .id-strip .id-row, body[data-family="katana"] .id-strip .stepper { display: none; }

/* Raw tiles for addresses the app does not map yet arrive as loose children of the grid (a real amp sends many).
   They counted as flex items and squeezed every block to a sliver (Alex's 20:39 screenshots): stage hides them,
   the "?" button shows them again. */
body[data-family="katana"]:not(.show-unconfirmed) #paramGrid > :not(.param-group) { display: none; }

/* ════════════════════════════════════════════════════════════
   MAIN PAGE (connect screen) -- one landscape screen, no scrolling (Alex, 2026-10-07).
   Left: the pedal, grouped by how well it is tested. Right: MIDI ports + Connect, then the optional login and the
   telemetry note. Same ids as before; only the layout changed. Below 860px it stacks.
   ════════════════════════════════════════════════════════════ */
.conn-screen { min-height: 0; align-items: flex-start; padding: 18px 20px; }
.conn-screen .conn-card { max-width: 1120px; width: 100%; text-align: left; padding: 16px 20px; }
.conn-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; }
.conn-head h2 { margin: 0; font-size: 1.5rem; }
.conn-head .intro-badge { margin: 0; }
.conn-body { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(280px, 1fr); gap: 22px; align-items: start; }
@media (max-width: 860px) { .conn-body { grid-template-columns: 1fr; } }

/* left: pedal picker -- tier blocks in a 2-column grid */
.conn-pick { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }
.conn-pick .fam-tier { min-width: 0; }
.conn-pick .fam-tier-tested, .conn-pick .fam-tier-zoom, .conn-pick .fam-tier-community { grid-column: 1 / -1; }
.conn-pick .fam-group-label { text-align: left; margin: 0 0 6px; }
.conn-pick .segmented {
  display: flex !important; flex-wrap: wrap; gap: 8px; justify-content: flex-start !important; margin: 0 !important;
  border: none; border-radius: 0; overflow: visible; background: none;
}
.conn-pick .segmented button {
  border: 1px solid var(--border); border-radius: 10px; padding: 10px 16px; font-size: 0.82rem;
  background: linear-gradient(180deg, var(--card-top), var(--card-bottom)); color: var(--text-primary);
  transition: border-color 0.12s, background 0.12s;
}
.conn-pick .segmented button:hover { border-color: var(--accent-blue); }
.conn-pick .segmented button.active { border-color: var(--accent-blue); background: var(--accent-blue-fill); color: var(--accent-blue); box-shadow: 0 0 0 1px var(--accent-blue) inset; }
.conn-pick .fam-tier-tested .segmented button { font-size: 0.85rem; padding: 14px 8px; flex: 1 1 90px; text-align: center; white-space: nowrap; }
.conn-pick .fam-tier-community .segmented button, .conn-pick .fam-tier-generic .segmented button { font-size: 0.72rem; padding: 7px 12px; color: var(--text-muted); }
.conn-pick .fam-tier-zoom .fam-group-label { color: var(--accent-amber); }
.conn-pick .fam-tier-zoom .segmented button { font-size: 0.8rem; padding: 8px 14px; border-style: dashed; }

/* right: connect */
.conn-side { display: flex; flex-direction: column; gap: 10px; }
.conn-side > p:first-child { margin: 0; font-size: 1rem; color: var(--text-primary); }
.conn-side #midiConnect .id-row { display: flex; flex-direction: column; gap: 8px; align-items: stretch; margin-bottom: 10px; justify-content: flex-start !important; }
.conn-side #midiConnect .id-row > div { display: flex; flex-direction: column; gap: 4px; }
.conn-side select { margin-bottom: 0; }
.conn-side #btnConnectMidi, .conn-side #btnConnectBle { width: 100%; padding: 14px 20px; font-size: 1rem; }
.conn-side #pmConnect .foot-note { margin: 0 0 8px; }
.conn-side .login-row { justify-content: flex-start; margin: 6px 0 0; padding-top: 12px; border-top: 1px solid var(--border-subtle); }
.conn-side .intro-telemetry { margin: 0; text-align: left; }
.conn-side #browserHint { margin: 0 !important; text-align: left; font-size: 0.74rem; }
.conn-card .conn-side .intro-telemetry { font-size: 0.74rem; line-height: 1.45; color: var(--text-muted); }

/* Play-Along screen (#110 B3) */
.pa-panel { max-width: 1100px; margin: 20px auto; }
.pa-drop { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 14px; border: 1px dashed var(--border); border-radius: 10px; margin-bottom: 10px; }
.pa-drop.drag { border-color: var(--accent-blue); background: var(--accent-blue-fill); }
.pa-controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 10px 0; }
.pa-stem { display: grid; grid-template-columns: 70px 34px 34px 120px 1fr; align-items: center; gap: 8px; padding: 4px 0; border-top: 1px solid var(--border-subtle); }
.pa-stem .btn { padding: 4px 0; text-align: center; font-size: 0.75rem; }
.pa-stem .btn.active { background: var(--accent-blue-fill); color: var(--accent-blue); border-color: var(--accent-blue); }
.pa-stem.pa-silent .pa-wave { opacity: 0.25; }
.pa-wave { width: 100%; height: 36px; }

/* ToneX tab */
.tx-panel { max-width: 1100px; margin: 20px auto; }
.tx-now { display: flex; align-items: center; gap: 12px; margin: 10px 0; }
.tx-cols { display: grid; grid-template-columns: 260px 1fr; gap: 16px; }
.tx-cols #txPresets { max-height: 460px; overflow-y: auto; }
.tx-current { border-color: var(--accent-green) !important; }
#txParams { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 4px 18px; max-height: 460px; overflow-y: auto; align-content: start; }
.tx-param { display: grid; grid-template-columns: 84px minmax(0, 1fr) 48px; align-items: center; gap: 8px; font-size: 0.74rem; min-width: 0; }
.tx-param input[type=range] { width: 100%; min-width: 0; }
.tx-name { color: var(--text-muted); font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tx-val { text-align: right; font-family: var(--font-mono); }

/* Alex, 2026-10-10 (screenshot 13:47): "make the row fit the window" was still true. Two rules above
   set `min-width: 150px`, then `min-width: 300px`, on every non-neutral card -- a FLOOR that stops a
   card shrinking. With 8-9 cards in a 1280px window the row cannot fit, so it overflows and the
   leftmost card is clipped by the window (BOOST's rows read "…Clean Boost", "…an Boost - louder and
   no grit"). A floor is only right while the cards fit; below that, shrinking is what fits. Placed
   last so it wins on order, and the basis scales with the viewport instead of a fixed card width. */
#paramGrid .param-group:not(.neutral) {
  flex: 1 1 clamp(118px, 11.5vw, 220px);
  min-width: 0;
  max-width: none;
}

/* Alex, 2026-10-10: the note under a card showed the MIDDLE of its line ("...an Boost - louder and no
   grit" instead of "KATANA Boost - louder..."), which only happens to a centred, non-wrapping block
   that is wider than its container -- the container clips it from both sides. An earlier attempt capped
   the width but a later rule re-centred the text, so this one goes last and sets alignment and wrapping
   together. A read-only note is never worth clipping: left-align it and let it wrap. */
#paramGrid .semaphore-note,
#paramGrid .param-group-body .semaphore-note,
#paramGrid .param-group .dropdown-note,
#paramGrid .param-group .effect-note {
  max-width: 100%;
  text-align: left;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
  hyphens: none;
}

/* Alex, 2026-10-10, third attempt and the first one that cannot be overridden: the card row must fit
   the window. Two earlier rules set `min-width: 150px` and then `min-width: 300px` on every non-neutral
   card at desktop widths; a floor stops a card shrinking, so with 8-9 cards the row is wider than the
   window and the leftmost card -- with its dropdown -- ends up over the edge. The effect dropdown then
   opens clipped at the window's bottom-left, which is what Alex kept photographing. Specificity and
   order both already lost this fight twice, so these three declarations carry !important: a floor is
   the bug, and a basis that scales with the grid means the cards fit by construction at any width. */
#paramGrid .param-group:not(.neutral) {
  flex: 1 1 clamp(112px, 10.5vw, 210px) !important;
  min-width: 0 !important;
  max-width: 100% !important;
}
#paramGrid { max-width: 100%; overflow-x: hidden; }

/* Alex, 2026-10-10: "This assign feature should be on a amp settings menu, not as a pedal. The exp pedal and
   foot control, too." The AMP SETTINGS group is hidden from the effects row -- it is not a pedal and does not
   belong beside BOOST -- and appears only while its toolbar button is open. Same shape as BTS, where ASSIGN is
   a tab. The classes are toggled by main.js's amp-settings button. */
#paramGrid .param-group[data-group-name="AMP SETTINGS"] { display: none; }
/* Alex, 2026-10-10: "Amps settings shouldn't be a card, it makes everything too small. It should be another
   screen. Make a pop up thing for both native and web." In the row it shrank every card beside it, so it is not
   in the row at all now: it opens as its own panel over the editor. Same DOM, presented as an overlay, which
   keeps the layout code untouched -- the group simply stops being a grid child while it is open. */
body.amp-settings-open #paramGrid .param-group[data-group-name="AMP SETTINGS"] {
  display: block;
  position: fixed;
  top: 9vh; left: 50%; transform: translateX(-50%);
  width: min(760px, 92vw); max-height: 78vh; overflow: auto;
  background: var(--panel, #14161c);
  border: 1px solid var(--line, #2b3040);
  border-radius: 6px;
  box-shadow: 0 18px 60px rgba(0, 0, 0, .6);
  padding: 10px 14px 16px;
  z-index: 60;
}
body.amp-settings-open #paramGrid .param-group[data-group-name="AMP SETTINGS"] .amp-settings-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  border-bottom: 1px solid var(--line, #2b3040); padding-bottom: 6px; margin-bottom: 8px;
}
body.amp-settings-open #paramGrid .param-group[data-group-name="AMP SETTINGS"] .amp-settings-bar h3 {
  font-size: 12px; letter-spacing: .08em; margin: 0;
}
body.amp-settings-open #paramGrid .param-group[data-group-name="AMP SETTINGS"] .amp-settings-bar button {
  cursor: pointer;
}
/* While the panel is open the row behind it should not compete with it. */
body.amp-settings-open #paramGrid { opacity: .35; pointer-events: none; }
body.amp-settings-open #paramGrid .param-group[data-group-name="AMP SETTINGS"] { opacity: 1; pointer-events: auto; }

/* #537 public gate, as CSS so it can be lifted. The body class is set by main.js's applyPublicGate() and
   removed for logged-in testers -- see that function's comment for why the first version (deleting DEVICES
   entries) was wrong: it locked Alex out of his own GP-5. */
/* :not([id^="familySwitch"]) matters: `familySwitch...` also starts with "fam", so the first version of this
   line hid the wrapper that CONTAINS the Katana button and left the picker completely empty -- Katana gone
   too. Caught by loading the page, not by reading the rule. */
body.public-katana [id^="fam"]:not(#famKatana):not(#famKatanaMk1):not(#famKatanaGen3):not([id^="familySwitch"]) { display: none !important; }
body.public-katana [id^="familySwitch"]:not(#familySwitchTested) { display: none !important; }
body.public-katana #modeTranslate,
body.public-katana #modeSetlists,
body.public-katana #modeLibrary,
body.public-katana #modeGuitar,
body.public-katana #modePlayAlong,
body.public-katana #modeToneX,
body.public-katana #modeChocolate { display: none !important; }

/* #540 gate rule: anything red is fixed or pulled from the public list. The FX LOOP and GAFC controls are still
   addresses the amp has not confirmed (#148 / #275 wait on the bench), so in the public build they are not just
   hidden by default -- the "?" button that reveals them is gone, and the groups stay off even if the class were
   set. A shorter editor that works beats a longer one that half-works. */
body.public-katana #btnUnconfirmed { display: none !important; }
body.public-katana .param-group[data-group-name$="(unconfirmed)"] { display: none !important; }


/* ════════════════════════════════════════════════════════════
   ONE LAYOUT FOR EVERY DEVICE (#598)  --  patch display, chain, patch list below the chain, status bar.
   Native is the reference; the Katana screen is the benchmark.
   ════════════════════════════════════════════════════════════ */

/* Nothing on a device screen is wider than the viewport. */
body { overflow-x: hidden; }
.container, .panel, #editorScreen { min-width: 0; max-width: 100%; }

/* The build stamp is for us, not for the player: behind the DEV toggle on every device (it stays in the DOM,
   telemetry's resolveBuild() reads its text). */
body:not(.dev-mode) .titlebar .buildstamp { display: none; }

/* A long patch list is a LIST under the chain: collapsible panel, filter, one line per patch. */
.patch-list-panel .panel-title { cursor: pointer; user-select: none; }
.patch-list-panel .panel-title::after { content: "  \25BE"; opacity: 0.6; }
.patch-list-panel.collapsed .panel-title::after { content: "  \25B8"; }
.patch-list-panel.collapsed #patchLibraryBody { display: none; }
.patch-list-filter {
  width: 100%; max-width: 360px; margin: 8px 0; padding: 7px 10px; box-sizing: border-box;
  font-family: var(--font-body); font-size: 0.85rem; color: var(--text-primary);
  background: var(--cell-bg); border: 1px solid var(--border); border-radius: 6px;
}
.patch-list { display: flex; flex-direction: column; gap: 2px; max-height: 360px; overflow-y: auto; }
.patch-row {
  display: flex; align-items: baseline; gap: 10px; padding: 5px 10px; border-radius: 6px; cursor: pointer;
  font-family: var(--font-body); font-size: 0.85rem; color: var(--text-primary);
  border: 1px solid transparent; min-width: 0;
}
.patch-row:hover { background: color-mix(in oklch, var(--accent-blue) 10%, transparent); }
.patch-row.live { background: color-mix(in oklch, var(--accent-blue) 16%, transparent); border-color: var(--accent-blue); }
.patch-row-code { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-muted); min-width: 34px; text-align: right; flex: none; }
.patch-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.patch-row-type { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-muted); flex: none; max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.patch-row-live { font-family: var(--font-display); font-weight: 700; font-size: 0.62rem; letter-spacing: 0.1em; color: var(--accent-blue); flex: none; }
.patch-list-empty { padding: 10px; color: var(--text-muted); font-size: 0.85rem; font-family: var(--font-body); }

/* Any remaining tile grid: a button label never breaks mid-word ("SELECT ON DE VICE"). */
.patch-tile .btn { white-space: normal; overflow-wrap: normal; word-break: normal; hyphens: none; line-height: 1.15; }

/* The bottom status bar. */
.status-bar {
  position: sticky; bottom: 0; z-index: 8; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 18px;
  margin-top: 14px; padding: 7px 14px; background: var(--toolbar); border-top: 1px solid var(--border);
  font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-secondary);
}
.status-item { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); flex: none; }
.status-ok .status-dot { background: oklch(0.72 0.17 150); }
.status-warn .status-dot { background: oklch(0.78 0.15 75); }
.status-error .status-dot { background: oklch(0.65 0.2 25); }
.status-progress { display: inline-block; width: 110px; height: 4px; border-radius: 2px; background: var(--border); overflow: hidden; }
.status-progress > span { display: block; height: 100%; background: var(--accent-blue); }

/* Katana polish (benchmark screenshots, 2026-10-12): values never collide, selects never lose their text,
   the unconfirmed groups read as a quiet candidate tier instead of a loud dashed slab. */
.knob { min-width: 0; padding: 0 2px; }
.knob .k-value { white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.param-group { min-width: 0; }
.param-group select, .dropdown-tile select { min-width: 0; max-width: 100%; width: 100%; text-overflow: ellipsis; font-size: 0.78rem; }
.param-group:not(.neutral) .dropdown-tile { min-width: 0; }
.param-group[data-group-name$="(unconfirmed)"] .knob .k-label { overflow-wrap: anywhere; line-height: 1.15; max-width: 100%; text-align: center; font-size: 0.56rem; }
.param-group[data-group-name$="(unconfirmed)"] { max-width: 100%; flex: 0 1 auto; opacity: 0.9; background: var(--card-bottom); }
body[data-family="katana"] #paramGrid .param-group[data-group-name$="(unconfirmed)"] { flex: 0 1 380px; }


/* ── The pedal's patch list as a rail (#616 / #621 / #598) ───────────────────────────────────────────
   One list for every device that can read its own memory (MS-200D+, G5n): one row per slot, the live one marked,
   a filter above it. It sits at the LEFT of the chain on a wide window -- the native editor's shape -- and falls
   below the chain on a narrow one, which is where #598 puts the list. Same markup, two placements. */
#deviceScanGrid.scan-grid { display: flex; flex-direction: column; gap: 1px; }
.scan-rail .patch-list-filter { width: 100%; box-sizing: border-box; margin: 0 0 6px; }
.scan-cell {
  padding: 4px 8px; border-radius: 4px; font-size: 0.74rem; border: 1px solid transparent;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.scan-cell[data-clickable="1"] { cursor: pointer; }
.scan-cell[data-clickable="1"]:hover { background: color-mix(in oklch, var(--input-accent, #8ab) 14%, transparent); }
.scan-cell.live {
  border-color: var(--input-accent, #8ab); color: var(--text-primary);
  background: color-mix(in oklch, var(--input-accent, #8ab) 20%, transparent);
}
.scan-cell.live::after { content: "  \25CF"; color: var(--accent-green, #7fd); }
.scan-star { display: inline-block; width: 1.3em; cursor: pointer; color: var(--text-secondary, #9ab); }
.scan-star.on { color: var(--accent-amber, #fc6); }
.scan-live-line { font-size: 0.74rem; margin: 0 0 6px; color: var(--accent-green, #7fd); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.editor-split { display: flex; gap: 14px; align-items: flex-start; }
.editor-split > .panel { margin-bottom: 14px; }
.editor-split > .scan-rail { flex: 0 0 270px; order: -1; position: sticky; top: 8px; max-height: calc(100vh - 24px); overflow-y: auto; }
.editor-split > .panel:not(.scan-rail) { flex: 1 1 0; min-width: 0; }
@media (max-width: 999px) {
  .editor-split { flex-direction: column; }
  .editor-split > .scan-rail { order: 2; flex: none; width: 100%; position: static; max-height: 420px; }
  .editor-split > .panel:not(.scan-rail) { width: 100%; }
}

/* G5n: nine FX blocks as a 3 x 3 board, not eight across and one stranded on its own row (#621). */
body[data-family="g5n"] #paramGrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; align-items: start; }
body[data-family="g5n"] #paramGrid .param-group { max-width: none; min-width: 0; }
body[data-family="g5n"] #paramGrid .param-group.neutral,
body[data-family="g5n"] #paramGrid .empty-hint { grid-column: 1 / -1; }
@media (max-width: 760px) { body[data-family="g5n"] #paramGrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* GP-5 (#630, child of #598): the ungrouped PATCH select is the patch display, so it is always the first thing in the
   grid -- ahead of the effect cards whatever order the device's replies arrive in. */
body[data-family="valeton"] #paramGrid > .dropdown-tile { order: -3; flex: 1 1 100%; max-width: 420px; }

/* Pocket Master (#596 child): SAVE AS and its hint no longer clip, and nothing is wider than the viewport.
   The SAVE group is neutral, so its row was nowrap + overflow-x:auto -- the hint sentence and the writable field
   fought for one fixed row and the label ended mid-character at the panel edge. Here the row wraps, the field and
   the hint each get a real width, and words wrap whole. */
body[data-family="pm"] #paramGrid { max-width: 100%; min-width: 0; }
body[data-family="pm"] #paramGrid .param-group[data-group-name="SAVE"] { flex: 1 1 100%; max-width: none; min-width: 0; }
body[data-family="pm"] #paramGrid .param-group[data-group-name="SAVE"] .param-group-body {
  flex-wrap: wrap; overflow: visible; gap: 8px 16px; align-items: flex-start;
}
body[data-family="pm"] #paramGrid .param-group[data-group-name="SAVE"] .text-tile { flex: 1 1 260px; min-width: min(260px, 100%); }
body[data-family="pm"] #paramGrid .param-group[data-group-name="SAVE"] .k-label,
body[data-family="pm"] #paramGrid .param-group[data-group-name="SAVE"] .t-value {
  white-space: normal; overflow-wrap: break-word; word-break: normal; hyphens: none;
}
body[data-family="pm"] .patch-tile .btn,
body[data-family="pm"] .patch-tile-code { white-space: normal; overflow-wrap: normal; word-break: normal; }

/* Pocket Master (#68): the chain is the page. Native puts save in the patch bar, not above the pedals, so the SAVE
   card sits AFTER the chain instead of pushing NR/FX1/DRV/AMP below the fold on a phone. And on a narrow screen the
   header strip wraps: CLONE/MODEL/VOL keep their own line and DIAGNOSTICS / DEVICES / DISCONNECT drop to the next
   one, instead of overlapping it and pushing DISCONNECT past the right edge (the sideways scroll). */
body[data-family="pm"] #paramGrid .param-group[data-group-name="SAVE"] { order: 1; }
/* The effect name inside a pedal card (TWD Deluxe, User Profile 1) had the global 11rem text-tile floor, wider than a
   card on a 360 px phone, so it ran past the card edge. It takes the card's width and wraps between words. */
body[data-family="pm"] #paramGrid .param-group:not(.neutral) .text-tile { min-width: 0; max-width: 100%; }
@media (max-width: 700px) {
  body[data-family="pm"] .id-strip { flex-wrap: wrap; gap: 8px 10px; }
  body[data-family="pm"] .id-strip .id-row { flex: 1 1 100%; }
  body[data-family="pm"] .id-strip .spacer { display: none; }
  /* The page header too: with TUTORIALS in the nav it no longer fit 360 px and pushed the PT toggle off-screen. */
  body[data-family="pm"] .titlebar { flex-wrap: wrap; padding: 8px 16px; row-gap: 6px; }
}

/* #616: the visible half of "this knob is editable" -- a - / + pair under the value on the MS-200D+ (native shows a
   value box with arrows). Small, and only on a writable knob. */
.k-steppers { display: flex; gap: 4px; margin-top: 2px; }
.k-step {
  font: inherit; font-size: 0.72rem; line-height: 1; min-width: 20px; padding: 2px 5px; cursor: pointer;
  color: var(--text-secondary); background: none; border: 1px solid var(--border); border-radius: 5px;
}
.k-step:hover { color: var(--text-primary); border-color: var(--input-accent, currentColor); }
.knob.writable:focus-visible { outline: 1px solid var(--input-accent, currentColor); outline-offset: 2px; border-radius: 6px; }

/* MIDI DEVICES list (#639) */
.midi-device-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--border); }
.midi-device-text { flex: 1 1 auto; min-width: 0; }
.midi-device-name { font-size: 0.9rem; overflow-wrap: anywhere; }
.midi-device-meta { font-size: 0.72rem; color: var(--text-muted-dim, var(--text-secondary)); }
.midi-device-missing .midi-device-meta { color: var(--accent-amber, #fc6); }
.midi-device-connected .midi-device-meta { color: var(--accent-green, #7fd); }

/* GA-FC foot surface (#640): thumb-sized buttons, full screen on a tablet, nothing smaller than 72px to hit. */
.gafc-overlay { position: fixed; inset: 0; z-index: 50; flex-direction: column; background: var(--bg, #0e0f12); padding: 14px; gap: 12px; overflow: auto; }
.gafc-head { display: flex; align-items: center; justify-content: space-between; font-size: 1.1rem; letter-spacing: 0.12em; }
.gafc-surface { display: flex; flex-direction: column; gap: 14px; flex: 1 1 auto; }
.gafc-row { display: grid; gap: 12px; align-items: stretch; }
.gafc-channels { grid-template-columns: 2.2rem repeat(4, minmax(0, 1fr)); }
.gafc-effects { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.gafc-row-label { display: flex; align-items: center; justify-content: center; color: var(--text-secondary); font-size: 1.1rem; }
.gafc-btn {
  min-height: 88px; font: inherit; font-size: 1.25rem; letter-spacing: 0.06em; cursor: pointer; touch-action: manipulation;
  color: var(--text-secondary); background: var(--panel-top, #1a1c21); border: 2px solid var(--border); border-radius: 14px;
}
.gafc-btn:active { transform: scale(0.98); }
.gafc-unknown { opacity: 0.45; }
.gafc-live, .gafc-on { color: #fff; border-color: var(--accent-green, #7fd); background: color-mix(in oklch, var(--accent-green, #7fd) 28%, transparent); opacity: 1; }
.gafc-note { font-size: 0.8rem; color: var(--text-muted-dim, var(--text-secondary)); text-align: center; min-height: 1.2em; }
@media (max-width: 520px) { .gafc-effects { grid-template-columns: repeat(2, minmax(0, 1fr)); } }


/* Help-us-find-a-missing-effect strip (issue #216). Visitor-facing, read-only, dismissible. */
.missing-fx { position: sticky; top: 0; z-index: 40; display: flex; gap: 14px; align-items: flex-start;
  justify-content: space-between; padding: 10px 14px; margin: 0 0 8px; border-radius: 8px;
  background: rgba(18,22,30,.96); color: #e8eaf0; border: 1px solid rgba(255,255,255,.14);
  font-size: 13px; line-height: 1.35; }
.missing-fx[hidden] { display: none !important; }
.missing-fx p { margin: 3px 0 0; }
.missing-fx-why { opacity: .7; }
.missing-fx-state { opacity: .92; }
.missing-fx-actions { display: flex; gap: 8px; flex: 0 0 auto; align-items: flex-start; }
.missing-fx-actions button { font: inherit; padding: 6px 11px; border-radius: 6px; cursor: pointer;
  border: 1px solid rgba(255,255,255,.22); background: rgba(255,255,255,.08); color: inherit; }
.missing-fx-actions .mfx-primary { background: #e8a33d; color: #14161c; border-color: transparent;
  font-weight: 600; }
.missing-fx-actions button[disabled] { opacity: .6; cursor: default; }
.missing-fx-actions .mfx-close { padding: 6px 10px; }
@media (max-width: 640px) { .missing-fx { flex-direction: column; } }


/* The how-to panel, for a visitor who has just arrived (issue #216's telemetry finding). */
.howto ol { margin: 6px 0 0; padding-left: 20px; }
.howto li { margin: 0 0 3px; }
.howto li > span:first-child { font-weight: 600; }
.howto-note { opacity: .78; }
.howto code { font-size: 12px; }

/* Library front door (js/screens/libraryImport.js) */
.library-sources { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.library-sources .btn.active { border-color: var(--accent-blue); }
.library-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.library-drop { margin-top: 8px; padding: 12px; border: 1px dashed var(--border, #3a4250); border-radius: 8px; color: var(--text-muted); font-size: 0.78rem; text-align: center; }
.library-drop.over { border-color: var(--accent-blue); color: var(--text); }
.library-result { text-align: left; padding: 6px 0 0; }
.library-result[data-tone="error"] { color: var(--accent-red); }
.library-result[data-tone="ok"] { color: var(--accent-green); }
.library-result[data-tone="warn"] { color: var(--accent-amber); }
.library-formats { margin-top: 12px; text-align: left; }
.library-formats ul { margin: 6px 0 0; padding-left: 18px; color: var(--text-muted); font-size: 0.78rem; }
.library-sources .btn.active, .library-sources .btn.active:hover { border-color: var(--accent-blue); }

/* ════════════════════════════════════════════════════════════
   VALETON GP-5 stage layout (js/screens/valetonLayout.js): patch bar on top, GLOBAL as the hero card on the left,
   one block at a time as a full pedal screen on the right, chosen from a signal-chain strip. Classic board: ?layout=classic
   ════════════════════════════════════════════════════════════ */
#paramGrid[data-layout="gp5"] {
  display: grid;
  grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  grid-template-areas: "patch patch" "global nav" "global stage" "other other";
  gap: 12px;
  align-items: start;
}
#paramGrid[data-layout="gp5"] .param-group { max-width: none; min-width: 0; flex: none; }
#paramGrid[data-layout="gp5"] .param-group[data-gp5-role="patch"] { grid-area: patch; order: 0; }
#paramGrid[data-layout="gp5"] .param-group[data-gp5-role="global"] { grid-area: global; align-self: stretch; }
#paramGrid[data-layout="gp5"] .param-group[data-gp5-role="block"][data-gp5-selected="true"] { grid-area: stage; }
#paramGrid[data-layout="gp5"] .param-group[data-gp5-role="block"][data-gp5-selected="false"] { display: none; }
#paramGrid[data-layout="gp5"] .param-group[data-gp5-role="other"] { grid-column: 1 / -1; }
#paramGrid[data-layout="gp5"] .gp5-tabs { grid-area: nav; display: flex; gap: 6px; flex-wrap: wrap; }
.gp5-tab {
  display: inline-flex; align-items: center; gap: 7px; padding: 7px 12px; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--border, #2b3340); background: var(--panel-top, #161b22); color: var(--text-muted); font-family: var(--font-heading, inherit);
  letter-spacing: 0.08em; font-size: 0.8rem;
}
.gp5-tab.selected { color: var(--text); border-color: var(--accent-blue); box-shadow: 0 0 0 1px var(--accent-blue) inset; }
.gp5-led { width: 9px; height: 9px; border-radius: 50%; background: var(--accent-green); box-shadow: 0 0 6px var(--accent-green); }
.gp5-tab.off .gp5-led { background: #3a414d; box-shadow: none; }
.gp5-tab.off .gp5-tab-label { opacity: 0.6; }
/* GLOBAL: the hero. A tall card with its settings in a clean two-column grid. */
#paramGrid[data-layout="gp5"] .param-group[data-gp5-role="global"] .param-group-title { font-size: 1rem; }
#paramGrid[data-layout="gp5"] .param-group[data-gp5-role="global"] .param-group-body { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 10px; justify-items: center; }
/* A block's pedal screen: wide card, controls on a roomy grid. */
#paramGrid[data-layout="gp5"] .param-group[data-gp5-role="block"] .param-group-body { display: flex; flex-wrap: wrap; gap: 18px 22px; align-items: flex-end; justify-content: flex-start; padding-top: 8px; }
#paramGrid[data-layout="gp5"] .param-group[data-gp5-role="global"] [data-param-key="valeton:readGlobals"] { grid-column: 1 / -1; }
#paramGrid[data-layout="gp5"] .param-group[data-gp5-role="block"] .footswitch { order: -2; align-self: center; }
#paramGrid[data-layout="gp5"] .param-group[data-gp5-role="block"] .dropdown-tile { order: -1; flex: 1 1 220px; max-width: 360px; align-self: center; }
/* EQ: a real fader bank: bands in ONE row at equal width on a shared baseline, switch and type on their own row above. */
#paramGrid[data-layout="gp5"] .param-group[data-group-name="EQ"] .param-group-body { flex-wrap: nowrap; justify-content: center; gap: 0; align-items: flex-end; flex-flow: row wrap; }
#paramGrid[data-layout="gp5"] .param-group[data-group-name="EQ"] .footswitch,
#paramGrid[data-layout="gp5"] .param-group[data-group-name="EQ"] .dropdown-tile { flex: 0 0 auto; margin-bottom: 14px; }
#paramGrid[data-layout="gp5"] .param-group[data-group-name="EQ"] .dropdown-tile { flex: 1 1 240px; max-width: 360px; margin-left: 12px; }
#paramGrid[data-layout="gp5"] .param-group[data-group-name="EQ"] .vbar { flex: 0 0 76px; width: 76px; }
#paramGrid[data-layout="gp5"] .param-group[data-group-name="EQ"] .vbar-track { height: 150px !important; width: 18px !important; }
#paramGrid[data-layout="gp5"] .param-group[data-group-name="EQ"] .vbar .k-label { font-size: 0.78rem; white-space: nowrap; }
#paramGrid[data-layout="gp5"] .param-group[data-group-name="EQ"] .vbar .k-value { font-size: 0.8rem; }
@media (max-width: 760px) {
  #paramGrid[data-layout="gp5"] { grid-template-columns: minmax(0, 1fr); grid-template-areas: "patch" "nav" "stage" "global" "other"; }
}
#paramGrid[data-layout="gp5"] .param-group[data-group-name="EQ"] .vbar:last-child { margin-left: 18px; }
