/* DEVELOPER NOTES
 * Dark tuner-app look. Heat-map cell colors are set inline by app.js
 * (hsl hue 220->0 over the table's own min..max); this file only handles
 * chrome/layout. Modified cells get .mod (amber outline) and the sidebar row of
 * an edited map gets .edited (bold + italic); selection is a
 * rectangular marquee — .sel tint plus sel-t/b/l/r perimeter edges (see the
 * --sel-* box-shadow composition at the .grid td.sel rule), with the crosshair
 * band (.cross) and lit axis headers (.hdr-on) around it.
 * Flash wizard styles are the fw-* block at the bottom (modal DOM is built
 * by js/flashui.js); --ok/--err vars exist for its success/failure states.
 * RESPONSIVE: the last block (search "RESPONSIVE / MOBILE LAYER") turns the
 * 300px sidebar into an off-canvas drawer below 820px — it depends on the page
 * providing #sidebar-toggle, #nav-scrim and toggling body.nav-open (index.html
 * wires this; any page with a 300px editor sidebar can opt in the same way).
 * Keep those ids in sync with that block.
 */
:root {
  --bg: #14161a; --panel: #1c1f25; --edge: #2c313a; --fg: #d6d9de;
  --dim: #8a919c; --accent: #e8552e; --amber: #ffb020;
  --ok: #58c07a; --err: #e05b5b;
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
/* Phone browsers "font-boost" text inside wide scrollable content (the tool
   strip, the grid) — labels inflate past their buttons and neighbouring
   controls look overlapped. The layout is already responsive; never boost.
   (misfire/index.html learned this first — keep both in step.) */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
  background: var(--bg); color: var(--fg);
  font: 14px/1.4 "Segoe UI", system-ui, sans-serif;
  display: flex; flex-direction: column;
}
/* brand | (optional status text) | actions. BOTH the bar and the button row
   wrap, because #actions has no fixed width — the bike's full name is one
   button and the catalog adds one more per chip, so it can outgrow any window.
   Before wrapping, the squeeze landed on the middle item (#bin-meta, flex:1 =
   basis 0), which collapsed to min-content and rendered its text as a vertical
   one-word-wide column. index.html no longer has that middle item at all (the
   sidebar card owns those facts); dash.html still does, for its connection
   line, so the rule stays and now ellipsizes rather than collapses. */
/* TWO ROWS (2026-08-14): #topbar is a column of two .topbar-row flex rows —
   Row 1 (#topbar-top) is brand + the account/nav cluster; Row 2 (#actions) is the
   tune verbs. Each row still wraps on its own; splitting them stops the account
   links and the verbs from fighting over one right edge. */
#topbar {
  display: flex; flex-direction: column; gap: 8px;
  padding: 8px 14px; background: var(--panel); border-bottom: 1px solid var(--edge);
}
.topbar-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Row 1: brand hugs the left; the identity strip is pushed to the right corner
   (#identbar margin-left:auto below). #top-nav removed 2026-08-29 — its tools/role
   buttons all migrated to the sidebar row and the Row-2 verb strip. */
/* Fullscreen FAB — floats bottom-left (clear of #view-fab bottom-right). Shown only
   on the phone, where hiding the browser URL bar actually reclaims screen; the inline
   script also keeps it hidden where the Fullscreen API is missing. */
#fs-fab {
  display: none; position: fixed; z-index: 45; left: 12px; bottom: 12px;
  width: 44px; height: 44px; padding: 0; border-radius: 999px;
  align-items: center; justify-content: center; font-size: 20px; line-height: 1;
  color: var(--text, #e8e8e8); background: var(--panel, #1b2130);
  border: 1px solid var(--border, #2c3444); box-shadow: 0 2px 8px rgba(0,0,0,.45);
  opacity: .82; cursor: pointer; -webkit-tap-highlight-color: transparent;
}
#fs-fab.is-fs { opacity: 1; border-color: var(--accent, #e8730c); color: var(--accent, #e8730c); }
/* Identity strip (identbar.js): email + a compact door sign-out button. margin-left
   auto pins it to the top-right corner now that #top-nav (which used to hold that
   auto margin) is gone; the phone query below resets the margin and re-orders it. */
#identbar { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
#identbar .identbar-email { font-size: 13px; color: #cfcfcf; }
#identbar .identbar-signout { padding: 5px 9px; line-height: 0;
  display: inline-flex; align-items: center; justify-content: center; }
#brand { flex: 0 0 auto; font-weight: 800; letter-spacing: 1px; font-size: 23px; line-height: 1.05; text-align: center; }
#brand span { color: var(--accent); }
#brand small { display: block; font-weight: 600; font-size: 11.5px; letter-spacing: 2px; text-transform: uppercase; color: var(--accent); margin-top: 2px; }
/* min-width:0 is what actually permits the shrink below min-content (flex items
   default to min-width:auto); without it the ellipsis never engages. */
#bin-meta {
  flex: 1 1 220px; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--dim); font-size: 12.5px;
}
/* Row 2 (verbs): kept to the right edge, same as it read before the split. */
#actions { justify-content: flex-end; }
/* Role navigation group (Tuner/Developer/Admin/Messages). Its own flex box so the
   buttons kept the 8px gap, fenced by a divider on EACH side. Emptied 2026-08-29:
   both role buttons (Admin + Messages) moved to the sidebar #tune-source-row, so
   #role-nav and its rules are gone — re-add them if a header-only role button
   returns. */
/* TOOLS is a dropdown (app.js wireToolsMenu / showToolItem): a .btn trigger + an
   absolutely positioned list of surfaces, so opening it floats over the editor
   instead of reflowing the header. Anchored right so it never runs off the window
   edge. The menu is always present (Engine Simulator is a link for everyone);
   role-gated items are revealed by level. Same .role-menu markup Admin/Messages use. */
.role-menu { position: relative; display: inline-block; }
.role-menu[hidden] { display: none; }
.role-menu-list {
  position: absolute; top: calc(100% + 4px); right: 0; z-index: 40;
  min-width: 190px; padding: 4px; display: flex; flex-direction: column; gap: 2px;
  background: var(--panel); border: 1px solid var(--edge); border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .45);
}
.role-menu-list[hidden] { display: none; }
.role-menu-item {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  /* display:flex on the class beats the UA's [hidden]{display:none} (author rule vs
     UA rule), so without the [hidden] override below every role-gated menu item
     ships VISIBLE-but-unwired — a "Download Offline Kit" that does nothing
     (2026-09-06). Same reason .role-menu / .role-menu-list carry theirs above. */
  padding: 6px 10px; border: 0; border-radius: 4px; background: transparent;
  color: var(--fg); font: inherit; font-size: 13px; text-align: left;
  white-space: nowrap; cursor: pointer; text-decoration: none;   /* item may be a link (Engine Sim) */
}
.role-menu-item[hidden] { display: none; }
a.role-menu-item:hover { color: var(--fg); }
.role-menu-item:hover { background: #343a44; }
/* Offline Flash sits in this menu (2026-09-05) and is disabled until a
   recognized flashable tune is open — dim it like a disabled .btn. */
.role-menu-item:disabled { opacity: .4; cursor: default; }
.role-menu-item:disabled:hover { background: transparent; }
.role-menu-item .devonly {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .4px;
  color: var(--amber); border: 1px solid #6e3a1e; border-radius: 3px; padding: 0 5px;
}
.btn {
  display: inline-block; padding: 5px 12px; border-radius: 4px; cursor: pointer;
  background: #2a2f37; border: 1px solid var(--edge); color: var(--fg); font-size: 13px;
  user-select: none; text-decoration: none; /* text-decoration for <a class="btn"> */
  white-space: nowrap; /* a label wrapped mid-phrase reads as two broken buttons */
}
.btn:hover { background: #343a44; }
.btn:disabled { opacity: .4; cursor: default; }
.btn.sm { padding: 2px 9px; font-size: 12.5px; }
.btn[hidden] { display: none; } /* display:inline-block above would defeat the hidden attribute */
.btn.warn { border-color: #6e3a1e; color: var(--amber); }
.btn.on { background: #33281f; border-color: var(--amber); color: #ffd9a8; }
/* Auto-save: a .btn-styled label wrapping a real checkbox (tunelib.js toggles
   .on to match, so it lights up amber like an active .on button) */
#autosave-lbl { display: inline-flex; align-items: center; gap: 6px; }
#autosave-lbl input { accent-color: var(--amber); margin: 0; cursor: pointer; }
#layout { flex: 1; display: flex; min-height: 0; }
#sidebar {
  width: 300px; min-width: 300px; overflow-y: auto;
  background: var(--panel); border-right: 1px solid var(--edge); padding: 10px;
}
/* TUNE SOURCE block — Open BIN + Defs + one base-image button per chip, moved
   out of the header button row on 2026-07-28 to sit directly above the card
   that names the loaded file (index.html DEVELOPER NOTES has the why).
   Open BIN is full width and on its own line: it is the common path, and a
   300px sidebar cannot fit it beside two more buttons without wrapping mid
   phrase. The row below wraps instead of scrolling, so a two-chip bike
   (Defs + Chip 1 + Chip 2) lays out on two lines rather than clipping.
   flex:1 1 auto + a min-width lets each button take its share of the row while
   staying readable; the labels themselves are already shortened for a narrow
   button by app.js chipButtonLabel(). */
#tune-source { margin: 0 0 10px; }
#tune-source-row, #chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
/* display:flex above would otherwise defeat the hidden attribute */
#tune-source-row[hidden], #chip-row[hidden] { display: none; }
/* Defs + Export BIN hug their labels (flex 0 0 auto) and sit left-aligned side by
   side, rather than each stretching to fill the row — Defs alone no longer spans
   the full width (Tim, 2026-08-29). Chip buttons keep their own even-split rule below. */
#tune-source-row .btn { flex: 0 0 auto; text-align: center; }
/* flex-basis 0 (not auto) so the chip buttons divide the row EVENLY instead of
   sizing to their labels — "Chip 1" and "Chip 2" are a pair and reading as one
   is what makes them scan as a pair. min-width keeps a third chip from
   squeezing the labels rather than wrapping. */
#chip-row .btn { flex: 1 1 0; min-width: 76px; text-align: center; }
/* TUNE-SET CONTROLLER SWITCHER (app.js renderSetSwitcher). A staged multi-controller
   tune set gets one button per controller here, so the customer jumps between them
   without reopening My Tunes. The active controller carries .on (amber), same as the
   mode toggle. A boxed panel so it reads as "this set" above the loaded-file card. */
#tuneset-switch {
  margin: 0 0 10px; padding: 8px; border: 1px solid var(--edge);
  border-radius: 6px; background: var(--panel);
}
#tuneset-switch[hidden] { display: none; }
#tuneset-switch .ts-head { font-size: 12.5px; font-weight: 600; color: var(--fg); }
#tuneset-switch .ts-hint { font-size: 11px; color: var(--dim); margin: 2px 0 7px; }
#tuneset-switch .ts-btns { display: flex; flex-wrap: wrap; gap: 6px; }
#tuneset-switch .ts-btns .btn { flex: 1 1 0; min-width: 76px; text-align: center; }
/* Per-controller state pill inside each button: ● = edited (not yet flashed),
   ✓ = flashed this session. Untouched controllers carry no pill. */
#tuneset-switch .ts-pill { margin-left: 5px; font-size: 10px; vertical-align: middle; }
#tuneset-switch .ts-edited { color: var(--amber); }
#tuneset-switch .ts-flashed { color: #4caf78; }
/* "Flash N changed controllers" — full-width, sits below the controller row. Not
   .accent: the orange primary is Flash to ECU's alone. */
#tuneset-switch .ts-flash {
  display: block; width: 100%; margin-top: 8px; text-align: center;
  border-color: var(--amber); color: #ffd9a8;
}
#tuneset-switch .ts-flash:hover { background: #33281f; }

/* TUNE-SET FLASH picker + report (app.js openSetFlashPicker / finishSetFlash). Same
   overlay/modal shell as the flash wizard (#flash-overlay/#flash-modal) so the two
   read as one system, reusing its .fw-head/.fw-body/.fw-foot/.fw-box classes. */
.ggset-overlay {
  position: fixed; inset: 0; z-index: 55; background: rgba(6, 8, 10, .72);
  display: flex; align-items: center; justify-content: center;
}
.ggset-modal {
  width: 560px; max-width: calc(100vw - 32px); max-height: calc(100vh - 32px);
  background: var(--panel); border: 1px solid var(--edge); border-radius: 8px;
  box-shadow: 0 14px 60px rgba(0, 0, 0, .7); display: flex; flex-direction: column;
}
.ggset-intro { color: var(--dim); font-size: 12.5px; line-height: 1.5; }
.ggset-row {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px; margin: 6px 0;
  border: 1px solid var(--edge); border-radius: 6px; cursor: pointer;
}
.ggset-row:hover { border-color: #3a414c; }
.ggset-row input { flex: none; width: 16px; height: 16px; accent-color: var(--accent); }
.ggset-name { flex: 1 1 auto; font-weight: 600; color: var(--fg); }
.ggset-state { flex: none; font-size: 12px; }
.ggset-state.chg { color: var(--amber); }
.ggset-state.done { color: #4caf78; }
.ggset-state.none { color: var(--dim); }
.ggset-report-row {
  display: flex; align-items: baseline; gap: 10px; padding: 6px 2px;
  border-bottom: 1px solid var(--edge);
}
.ggset-report-row:last-child { border-bottom: 0; }
.ggset-ricon { flex: none; font-weight: 800; }
.ggset-ricon.ok { color: #4caf78; }
.ggset-ricon.bad { color: #ff8080; }
.ggset-rdetail { flex: 1 1 auto; text-align: right; color: var(--dim); font-size: 12.5px; }
/* CONTROLLER TILES (flash hub). One square per controller, name on top, colour =
   state: orange pending, green flashed, red failed, grey not selected. */
.ggset-tiles { display: flex; gap: 12px; margin: 4px 0 16px; }
.ggset-tile {
  position: relative; flex: 1 1 0; min-width: 0; text-align: center;
  padding: 30px 14px; min-height: 96px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 6px;
  border-radius: 10px; border: 2px solid var(--edge); background: var(--panel);
}
.ggset-tile.sel { cursor: pointer; }
.ggset-tile.sel:hover { filter: brightness(1.12); }
.ggset-tname { font-weight: 700; font-size: 17px; overflow-wrap: anywhere; line-height: 1.2; }
.ggset-tstate { font-size: 12px; opacity: .9; }
/* Checkbox INSIDE the tile, top-left. Amber filled when ticked/flashed, hollow when not. */
.ggset-check {
  position: absolute; top: 10px; left: 10px; width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid var(--dim); border-radius: 5px;
  font-size: 14px; font-weight: 800; line-height: 1; color: transparent;
}
.ggset-check.on { border-color: currentColor; color: inherit; background: rgba(255, 255, 255, .06); }
.ggset-tile.t-pending { border-color: var(--amber); background: #2a2118; color: #ffd9a8; }
.ggset-tile.t-done { border-color: #2d5c3d; background: #16301f; color: #a9e8c0; }
.ggset-tile.t-fail { border-color: #7a2a2a; background: #2a1717; color: #ffb8b8; }
.ggset-tile.t-idle { color: var(--dim); }
/* Auto-flash run: a controller currently being written (amber-ish, "flashing…"). */
.ggset-tile.t-active { border-color: var(--amber); background: #2a2118; color: #ffd9a8; }
/* Per-tile progress bar for the GG-Flasher-232 auto-flash run. */
.ggset-bar { height: 6px; border-radius: 3px; background: rgba(255,255,255,.12); margin-top: 8px; overflow: hidden; }
.ggset-bar-fill { height: 100%; width: 0; background: var(--amber); transition: width .2s ease; }
.ggset-log { max-height: 130px; overflow: auto; margin-top: 10px; }
.ggset-banner { border-radius: 6px; padding: 10px 12px; margin: 2px 0 6px; font-weight: 700; }
.ggset-banner.ok { border: 1px solid #2d5c3d; background: #16301f; color: #a9e8c0; }
.ggset-banner.err { border: 1px solid #7a2a2a; background: #2a1717; color: #ffb8b8; }
/* Full-width variant of .btn. .btn is inline-block, so width:100% alone would
   still leave it baseline-aligned in a text flow — block + centred text is what
   makes it read as a bar. Not .accent: the orange is Flash to ECU's alone, and
   two competing primaries in one column is worse than none. */
.btn.wide {
  display: block; width: 100%; text-align: center;
  padding: 7px 12px; font-size: 13.5px; font-weight: 600;
  border-color: #3d4450; background: #30363f;
}
.btn.wide:hover { background: #3a414c; }
/* My Tunes + My Account share one row. My Account is the SINGLE copy at every
   width (Tim 2026-08-30): the two flex items split the row evenly, giving a My
   Account button right beside My Tunes. The old top-right #identbar twin is
   retired below — it vanished in act-as mode (identbar renders only "Stop
   acting" then), leaving a desktop act-as session with no My Account at all. */
#tune-hub { display: flex; gap: 6px; }
#tune-hub .btn.wide { flex: 1 1 0; min-width: 0; }
/* Retire the top-right My Account twin everywhere so there is exactly one. */
#identbar-account { display: none; }
#rom-info { font-size: 12.5px; color: var(--dim); padding-bottom: 10px; }
#rom-info b { color: var(--fg); }
/* The specifics the def's <info> block declares (rominfo.js): a label/value
   grid under the ROM ID line. auto column so a long label ("Immobiliser code")
   sets the gutter and the values still line up; values wrap rather than push
   the 300px sidebar wider (a VIN is 17 chars and must stay readable). */
.rom-facts { display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; margin-top: 7px; }
.rom-facts .rf-k { color: var(--dim); white-space: nowrap; }
.rom-facts .rf-v { color: var(--fg); overflow-wrap: anywhere; }
.rom-facts .rf-k.has-hint { border-bottom: 1px dotted #4a5260; cursor: help; align-self: start; }
.rom-facts .rf-v .bad { color: #e5834a; }
/* Group Tune / Individual Maps toggle (shown once a recognized bin is loaded) —
   two separate gapped buttons, not a flush segmented control: the shared-edge
   look read as the buttons overlapping. Active mode carries .on (amber).
   The labels are long, so the buttons wrap their text rather than truncate: a
   clipped "Individual Ma…" in a 300px sidebar is worse than two lines. */
#mode-toggle { display: flex; gap: 6px; margin: 0 0 10px; }
#mode-toggle[hidden] { display: none; } /* display:flex would defeat the hidden attribute */
#mode-toggle .btn { flex: 1; white-space: normal; line-height: 1.25; }
.group-hint { color: var(--dim); font-size: 11.5px; padding: 10px 6px 0; line-height: 1.5; }

/* ---- Group Tune nav tree (js/app.js renderGroupNav / renderGroupMembers) ----
   One .grp box per group: a .grp-row (which is also a .tbl, so it selects and
   highlights exactly like a map row) with the maps that group drives INDENTED
   beneath it, one checkbox each. This replaced the wrapping #group-members chip
   row that used to sit under the table title — see index.html.
   Collapse is per group and stores the CLOSED ids, so the default is expanded. */
.grp { margin-top: 6px; }
.grp-row { display: flex; align-items: center; gap: 6px; }
.grp-row .grp-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* The fold arrow. Its own hit target — clicking the row opens the group, only
   the caret folds it (app.js stops the click there). */
.grp-caret {
  flex: none; width: 12px; text-align: center; color: var(--dim);
  font-size: 10px; transition: transform .12s ease;
}
.grp-caret:hover { color: var(--fg); }
.grp.closed .grp-caret { transform: rotate(-90deg); }
/* n/m — how many of the group's maps this edit reaches. Updated in place by
   renderGroupMembers so a tick doesn't rebuild the nav under the pointer. */
.grp-count {
  flex: none; font-size: 11px; color: var(--dim);
  background: #2a2f37; border-radius: 8px; padding: 0 6px;
}
.grp-row.active .grp-count { background: #47382a; color: #ffd9a8; }
/* Open the group builder on THIS group (js/groupbuilder.js): re-point main, add
   maps the def never listed, rename or delete a custom one. Kept quiet until the
   row is hovered or selected — it is a per-group action, not a per-group label,
   and fifteen pencils down the sidebar reads as clutter. */
.grp-edit {
  flex: none; background: none; border: 0; color: var(--dim); cursor: pointer;
  font: inherit; font-size: 12px; line-height: 1; padding: 2px 4px; border-radius: 3px;
  opacity: 0; transition: opacity .12s ease;
}
.grp-row:hover .grp-edit, .grp-row.active .grp-edit { opacity: 1; }
.grp-edit:hover { color: var(--fg); background: #3a414c; }
.grp-edit:focus-visible { opacity: 1; outline: 1px solid var(--accent); }
/* "+ New group". Full width under the list, and the ONLY control in the empty
   state — a bike whose def declares no groups lands there, and building one is
   the only useful thing to do. */
.grp-new { width: 100%; margin: 10px 0 0; }
.grp.closed .grp-members { display: none; }
/* The indent IS the relationship — these maps belong to the group above them.
   The rule down the left says where the group's reach starts and stops. */
.grp-members { margin: 2px 0 2px 13px; padding-left: 9px; border-left: 1px solid var(--edge); }
.grp-members .em {
  display: flex; align-items: center; gap: 6px;
  padding: 2px 6px; border-radius: 3px; font-size: 12px;
  color: var(--fg); cursor: pointer;
}
.grp-members .em:hover { background: #262b33; }
.grp-members .em.off { color: var(--dim); text-decoration: line-through; text-decoration-color: #5a6270; }
.grp-members .em.main { color: #ffd9a8; cursor: default; }
.grp-members .em input { accent-color: var(--amber); margin: 0; flex: none; cursor: pointer; }
.grp-members .em.main input { cursor: default; }
.grp-members .em-main-tag {
  font-size: 9.5px; text-transform: uppercase; letter-spacing: .5px;
  color: var(--amber); border: 1px solid var(--amber); border-radius: 3px;
  padding: 0 3px; flex: none;
}
/* Detail-level picker: how much of the def's map list to show (js/app.js
   USER_LEVELS / renderLevelPicker). Sits directly above the search box because
   both narrow the same list — this one by the def's userlevel, that one by
   text. The <select> takes the leftover width so the map count in each option
   ("Advanced — 248 maps") is readable in a 300px sidebar. */
#level-row { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; }
#level-row[hidden] { display: none; } /* display:flex would defeat the hidden attribute */
#level-row label {
  flex: 0 0 auto; font-size: 11.5px; text-transform: uppercase;
  letter-spacing: .6px; color: var(--dim);
}
#user-level {
  flex: 1 1 auto; min-width: 0; padding: 4px 6px;
  font: inherit; font-size: 12.5px;
  background: #101216; border: 1px solid var(--edge); border-radius: 4px; color: var(--fg);
}
#user-level:focus { outline: none; border-color: var(--accent); }
/* Quick-find box: filters the map list live (js/app.js filterNav). Shown only
   once a ROM is loaded. */
#nav-search {
  width: 100%; box-sizing: border-box; margin: 0 0 4px;
  padding: 5px 9px; font: inherit; font-size: 13px;
  background: #101216; border: 1px solid var(--edge); border-radius: 4px; color: var(--fg);
}
#nav-search::placeholder { color: var(--dim); }
#nav-search:focus { outline: none; border-color: var(--accent); }
#nav-search[hidden] { display: none; }
#nav-empty { color: var(--dim); font-size: 12.5px; padding: 8px 4px; }
/* Search visibility: match-driven (.filter-hide), overriding category collapse.
   The #table-nav.searching prefix outweighs `.cat.closed .tbl` so matched maps
   inside a collapsed category still show while a query is active. */
.filter-hide { display: none !important; }
#table-nav.searching .cat .tbl:not(.filter-hide) { display: block; }
/* Same idea for a folded group: while a query is live its member list shows, so
   a hit on a member NAME ("cyl 3") isn't answered with a group you then have to
   unfold by hand. Clearing the box puts the fold back exactly as it was. */
#table-nav.searching .grp.closed .grp-members { display: block; }
.cat { margin-top: 10px; }
/* MAP CATEGORY HEADINGS — bigger than the map rows they head (2026-07-29, Tim:
   "needs to be a little bigger to stand out"). At the old 11.5px --dim they were
   SMALLER and fainter than the 13px rows beneath them, so a heading read as a
   caption hanging off the list above it rather than as the start of a section.
   13.5px/700 in a lighter grey outweighs a row without shouting; the extra top
   margin is what actually separates one section from the previous one's rows.
   NOT nested under .cat: Group Tune's "Group tune" head is a bare .cat-name with
   no .cat parent (app.js renderGroupNav) and was inheriting plain body text. */
.cat-name {
  font-size: 13.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .7px;
  color: #aab2be; padding: 4px 4px 3px;
}
.cat > .cat-name { cursor: pointer; }
.cat > .cat-name:hover { color: var(--fg); }
.cat.closed .tbl { display: none; }
.tbl {
  padding: 3px 8px; font-size: 13px; cursor: pointer; border-radius: 3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tbl:hover { background: #262b33; }
.tbl.active { background: #33281f; color: #ffd9a8; }
/* Stable table number (numberTables, app.js) — dim, monospace, so it reads as a
   reference tag and the user can say "compare 11 to 12" to the assistant. */
.tbl .tbl-no {
  display: inline-block; min-width: 2.4em; margin-right: 6px;
  color: #6f7681; font: 11px/1 ui-monospace, Consolas, monospace; text-align: right;
}
.tbl.active .tbl-no { color: #c69a6a; }
/* AN EDITED MAP (2026-07-29, Tim-directed): bold + italic, in the category tree,
   on a group row and on a group's member rows. Set by app.js markNavEdits() off
   the same byte-level test the grid's amber .mod outline uses, so a row cannot
   disagree with the cells inside it. Colour is deliberately untouched — .active
   owns the amber, and a row that is edited AND open has to read as both. The
   caret/count/pencil are reset because they are controls, not the map's name. */
.tbl.edited, .grp-members .em.edited { font-weight: 700; font-style: italic; }
.grp-row.edited .grp-caret, .grp-row.edited .grp-count, .grp-row.edited .grp-edit {
  font-weight: 400; font-style: normal;
}
#content { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative; /* anchors #view-fab */ }
#table-head { padding: 10px 16px 6px; border-bottom: 1px solid var(--edge); }
#table-head h2 { margin: 0 0 2px; font-size: 16px; }
#table-desc { color: var(--dim); font-size: 12.5px; max-width: 900px; }
/* ---- Auto-folding table head (2026-08-27): a freshly opened map shows its
   full description for a beat, then folds to title + first line so the grid
   keeps the vertical room (the V-Max blurbs run five lines on a phone).
   app.js thFold()/thAutoFold() drive the inline max-heights; these rules add
   the clip, the motion and the ▸/▾ affordance. Applies at every width. ---- */
#table-desc, #table-addr { overflow: hidden; transition: max-height .35s ease, opacity .35s ease; }
#table-head.th-can-toggle { cursor: pointer; -webkit-tap-highlight-color: transparent; }
#table-head.th-can-toggle #table-title::after {
  content: "▾"; color: var(--dim); font-size: 11px; margin-left: 8px; vertical-align: 2px;
}
#table-head.th-folded #table-title::after { content: "▸"; }
#table-head.th-folded #table-desc { opacity: .75; }
#table-head.th-folded #table-addr { opacity: 0; }
/* Dev-only (?dev=1) storage-address line under the description — monospace so
   addresses line up when cross-referencing the disassembly. */
#table-addr { margin-top: 3px; color: var(--dim); opacity: .8;
  font: 11.5px/1.5 ui-monospace, "Cascadia Mono", Consolas, monospace; }
#table-addr b { color: var(--fg); font-weight: 600; }
/* Wraps to as many rows as the width needs (flex-wrap) — the button row had
   grown past the viewport, pushing Δ Change / Trace / the Reverts off the right
   edge where they were unreachable (Tim 2026-08-14). gap already covers both
   axes, so wrapped rows get their 6px vertical spacing for free. */
#table-tools { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
#table-tools[hidden] { display: none; } /* display:flex above would defeat the hidden attribute */
.tool-sep { width: 1px; height: 18px; background: var(--edge); margin: 0 4px; flex: none; }
#op-val {
  width: 72px; padding: 2px 8px; font: inherit; font-size: 12.5px;
  background: #101216; border: 1px solid var(--edge); border-radius: 4px; color: var(--fg);
}
#op-val:focus { outline: none; border-color: var(--accent); }
/* sel-info absorbs leftover toolbar width and ellipsizes — long selection
   text must never push Revert into overlap. Buttons/input never shrink. */
#sel-info {
  color: var(--dim); font-size: 12.5px; margin: 0 10px; flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#table-tools .btn, #table-tools #op-val { flex: none; }
/* Dead-zone caption: its own line under the toolbar. The pickers live in the
   dialog (js/deadzoneui.js), not here. */
#mask-info { margin-top: 6px; color: var(--dim); font-size: 12px; line-height: 1.5; }
#mask-info[hidden] { display: none; }
/* Table/Graph|3D view switch — a flush segmented pair (see mode-toggle note:
   there the shared edge read as overlap, but this small 2-button pill reads
   fine flush). Lives in the floating #view-fab now. Active side = .on. */
#view-toggle { display: inline-flex; flex: none; }
#view-toggle[hidden] { display: none; } /* inline-flex would defeat the hidden attribute */
#view-toggle .btn { border-radius: 0; }
#view-toggle .btn:first-child { border-radius: 4px 0 0 4px; }
#view-toggle .btn:last-child { border-radius: 0 4px 4px 0; border-left: none; }
/* Group Tune % Change | Values switch — a flush segmented pill, same shape as
   #cmp-view, riding in the floating #view-fab. Active side = .on. Only a Group Tune
   PERCENT group shows it (app.js renderGroupView). */
#group-view { display: inline-flex; flex: none; }
#group-view[hidden] { display: none; } /* inline-flex would defeat the hidden attribute */
#group-view .btn { border-radius: 0; font-weight: 700; }
#group-view .btn:first-child { border-radius: 4px 0 0 4px; }
#group-view .btn:last-child { border-radius: 0 4px 4px 0; border-left: none; }
#view-fab #group-view { box-shadow: 0 2px 10px rgba(0, 0, 0, .55); }
/* Caption for the Values view. Same slot and reasoning as #mask-info. */
#group-view-note { margin: 6px 0 2px; color: var(--dim); font-size: 12px; line-height: 1.5; }
#group-view-note[hidden] { display: none; }
/* There is NO .read-only rule here any more (2026-07-28). The group Values view
   used to carry it and the whole editing half of the toolbar disappeared; that
   view is editable now (grouptune.js converts the units), so the tools are the
   same in every view. compare's .cmp-ol (css/compare.css) is the one remaining
   case, and it is a different thing entirely: those numbers belong to another
   file, so there is nothing here that an edit could legitimately mean. */
/* The 2D-graph chart view has no marquee, so the grid-only editing controls
   (nudge, value ops, interpolate, copy/paste, Neighbours) are meaningless there
   — hide them, leaving only the view switch, hover readout and Revert. The 3D
   surface DOES support a block selection (Ctrl/⌘-drag or shift+arrows), so
   renderTable adds .map3d and this rule then keeps the tools visible. */
#table-tools.chart-view:not(.map3d) .grid-only { display: none; }
/* TICK-BOX TABLES (def <toggle>, app.js toggleView). The two sets swap: a table
   whose cells are on/off loses the arithmetic (.num-only — nudge, = += × %,
   Interpolate, Smooth, Neighbours) and gains Enable/Disable. Hiding rather than
   disabling is deliberate: on a fault-code table "×1.05" is not a weak
   operation, it is a way to quietly write a DIFFERENT fault code. Select table,
   Copy/Paste, Δ Change and both Reverts stay — they are state-agnostic. */
#table-tools .toggle-only { display: none; }
#table-tools.toggle-view .toggle-only { display: inline-flex; }
#table-tools.toggle-view span.toggle-only { display: inline-block; }
#table-tools.toggle-view .num-only { display: none; }
/* Neighbours spread toggle — a flush segmented pill like #view-toggle. Active
   segment carries .on (amber). Shared with grid + 3D view (it's .grid-only, so
   it hides only in the 2D graph). */
#neighbor-mode { display: inline-flex; flex: none; }
#neighbor-mode .btn { border-radius: 0; }
#neighbor-mode .btn:first-child { border-radius: 4px 0 0 4px; }
#neighbor-mode .btn:last-child { border-radius: 0 4px 4px 0; border-left: none; }
/* Smooth strength (Light|Med|Full) — same segmented pill, sitting against the
   Smooth button it belongs to. Active segment carries .on. */
#smooth-str { display: inline-flex; flex: none; }
#smooth-str .btn { border-radius: 0; }
#smooth-str .btn:first-child { border-radius: 4px 0 0 4px; }
#smooth-str .btn:last-child { border-radius: 0 4px 4px 0; border-left: none; }
.tool-lbl { color: var(--dim); font-size: 12px; flex: none; }
/* Tool switch (Select|Brush) and the pen's Raise|Lower — same segmented pill as
   #neighbor-mode; active segment carries .on. app.js setTool / setBrushDir. */
#tool-mode, #brush-dir { display: inline-flex; flex: none; }
#tool-mode .btn, #brush-dir .btn { border-radius: 0; }
#tool-mode .btn:first-child, #brush-dir .btn:first-child { border-radius: 4px 0 0 4px; }
#tool-mode .btn:last-child, #brush-dir .btn:last-child { border-radius: 0 4px 4px 0; border-left: none; }
/* Brush mode swaps the arithmetic for the pen in the same slot: the nudge/value
   ops carry .select-only and drop out while Brush is active; the pen knobs carry
   .brush-only and appear only then (app.js setTool toggles .brush-tool). */
#table-tools:not(.brush-tool) .brush-only { display: none; }
#table-tools.brush-tool .select-only { display: none; }
.brush-num { width: 54px; }
.brush-range { width: 90px; vertical-align: middle; }
/* BRUSH MODE = A SLIM TWO-ROW TOOLBAR (2026-08-27). The pen has its own shaping
   knobs and none of the marquee tools apply, so Brush mode:
     • drops Size / Strength / Falloff (and Light/Med/Full while Smoothing) onto a
       SECOND ROW under the tool+mode row — .tool-break is a full-width flex spacer
       that forces the wrap, and .pen-knob items are ordered after it;
     • hides every Selector-only map tool (.pen-hide): Interpolate, Smooth, the
       Neighbours pill, Copy and Paste — a brush can't do any of those.
   Desktop only (the flex row wraps here); the phone strip resets both below. */
#table-tools.brush-tool .pen-hide { display: none; }
.tool-break { display: none; flex-basis: 100%; width: 100%; height: 0; margin: 0; }
#table-tools.brush-tool .tool-break { display: block; order: 1; }
#table-tools.brush-tool .pen-knob { order: 2; }
/* Light/Med/Full is the SMOOTHING pen's strength, so in Brush mode it shows only
   when the pen is in Smooth (app.js setBrushMode adds .bmode-smooth); Raise/Lower
   don't use it. Selector mode is untouched — it still pairs with the Smooth button. */
#table-tools.brush-tool #smooth-str { display: none; }
#table-tools.brush-tool.bmode-smooth #smooth-str { display: inline-flex; order: 2; }
/* ONE-FINGER Rotate|Paint pill for the 3D surface. No right button on a phone,
   so with the pen armed this decides what one finger does (two fingers always
   pinch-zoom + pan — mapview.js). Shown ONLY on touch devices, and only in the
   3D surface view with the pen armed (.map3d + .brush-tool). Segmented like the
   other pills. Hidden by default so mouse/desktop never sees it. */
#touch-pen-mode {
  display: none; position: absolute; top: 10px; right: 26px; z-index: 5;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .55);
}
#touch-pen-mode .btn { border-radius: 0; }
#touch-pen-mode .btn:first-child { border-radius: 4px 0 0 4px; }
#touch-pen-mode .btn:last-child { border-radius: 0 4px 4px 0; border-left: none; }
/* Lives in the floating #view-fab, directly above the Table/3D switch (Tim
   2026-09-01 — off the crowded tool strip); #view-fab mirrors #table-tools'
   .map3d / .brush-tool classes (app.js) so the same gate applies. */
@media (hover: none) and (pointer: coarse) {
  #touch-pen-mode.map3d.brush-tool { display: inline-flex; }
  /* …and the gesture toast steps down out from under it. */
  #touch-pen-mode.map3d.brush-tool ~ #chart-hint { top: 52px; }
}
/* Δ Change view keeps the grid-editing tools visible — unlike chart-view, the
   delta overlay is editable (nudge/ops/interpolate/copy/paste operate on the
   real value; the cell just displays the delta). So there is NO diff-view
   hide rule to match chart-view's. */
/* #map-area is the positioning box for the map's own overlays (#touch-pen-mode,
   #chart-hint) — see index.html. min-height:0 keeps #grid-wrap's own scrolling
   intact inside the column flex. */
#map-area { flex: 1; display: flex; flex-direction: column; min-height: 0; position: relative; }
#grid-wrap { flex: 1; overflow: auto; padding: 12px 16px; -webkit-overflow-scrolling: touch; }
/* While the pen is active the grid owns one-finger touch drags (app.js touch
   painting) — without this the browser scrolls instead of letting it paint.
   Two-finger drags still pan (handled in the same JS). Selector mode keeps
   native touch scrolling: no touch-action override there. */
.grid.brush { touch-action: none; }
/* Canvas visualisations (js/mapview.js) fill the grid area; it owns its own
   zoom (wheel) so the CSS-zoom #zoom-ctrls stays hidden in chart view. */
.map-canvas { width: 100%; height: 100%; }
/* Floating bottom-right cluster — the compare/group controls + Table/3D switch +
   grid zoom in/out, moved out of the crowded top toolbar. Absolute against
   #content (not the scrolling #grid-wrap) so it stays put on scroll; right offset
   clears the vertical scrollbar when one is present.
   HORIZONTAL ROW along the bottom PERIMETER (Tim 2026-09-02): a single line hugging
   the bottom edge, so the wide compare strips (#cmp-scale Sensitivity, #cmp-lock)
   run LEFT of the Table/3D · Width/Page · zoom cluster instead of stacking UPWARD
   and spilling over the middle of the table. DOM order gives the left-to-right run;
   justify-content:flex-end pins the whole row to the right edge. wrap keeps it from
   overflowing off-screen on a narrow map — the overflow wraps to a line above,
   still right-aligned, and the zoom/Table cluster (last in DOM) stays on the
   bottom line at the corner. */
#view-fab { position: absolute; right: 26px; bottom: 16px; z-index: 5; display: flex; flex-direction: row; flex-wrap: wrap; justify-content: flex-end; align-items: flex-end; gap: 8px; }
#view-fab[hidden] { display: none; } /* display:flex would defeat the hidden attribute */
#view-fab #view-toggle { box-shadow: 0 2px 10px rgba(0, 0, 0, .55); }
/* The 3D "how to drive it" toast (app.js showChartHint). Absolute inside
   #map-area, so it sits at the map's top edge and never covers the table header.
   Fades on its own (the .show class is dropped after CHART_HINT_MS);
   pointer-events:none so a rotate gesture that starts under it still reaches
   the canvas. */
#chart-hint {
  position: absolute; top: 10px; left: 10px; right: 10px; transform: translateY(-6px); z-index: 6;
  padding: 7px 14px; border-radius: 8px;
  background: rgba(16, 26, 36, .94); border: 1px solid var(--edge); color: #cfe4f5;
  font-size: 12px; line-height: 1.35; text-align: center; white-space: normal;
  box-shadow: 0 2px 12px rgba(0, 0, 0, .55); pointer-events: none;
  opacity: 0; transition: opacity .45s ease, transform .45s ease;
}
#chart-hint.show { opacity: 1; transform: translateY(0); }
/* Width | Page — what the grid auto-fits to (app.js state.fitMode). Same flush
   segmented pill as #view-toggle / #neighbor-mode; active segment carries .on.
   BOTH segments are unlit while a manual +/- zoom is in force, which is the
   honest reading (fit is not in charge) and makes either one a "re-fit" button.
   Hides in chart view alongside #zoom-ctrls. */
#fit-mode { display: inline-flex; flex: none; box-shadow: 0 2px 10px rgba(0, 0, 0, .55); }
#fit-mode[hidden] { display: none; } /* inline-flex would defeat the hidden attribute */
#fit-mode .btn { border-radius: 0; }
#fit-mode .btn:first-child { border-radius: 4px 0 0 4px; }
#fit-mode .btn:last-child { border-radius: 0 4px 4px 0; border-left: none; }
#zoom-ctrls { display: flex; gap: 8px; }
#zoom-ctrls[hidden] { display: none; } /* hidden in chart view (canvas owns zoom) */
#zoom-in, #zoom-out {
  width: 38px; height: 38px; padding: 0; border-radius: 50%;
  font-size: 21px; line-height: 36px; text-align: center;
  opacity: .85; box-shadow: 0 2px 10px rgba(0, 0, 0, .55);
}
#zoom-in:hover, #zoom-out:hover { opacity: 1; }
/* width:max-content so the grid is a FIXED-SIZE object that applyScale() scales,
   never a fluid one the container squeezes. Without it a grid wider than
   #grid-wrap gets compressed, its header text wraps, and its natural HEIGHT
   grows — so the height applyScale measured (zoom cleared, scrollbar showing)
   was not the height it got after scaling, and "Page" fit overshot the card by
   ~4%. Measured 2026-07-28: natH read 1300, rendered 1354. */
table.grid { border-collapse: collapse; font-size: 12.5px; user-select: none; width: max-content; }
.grid th {
  background: #232830; color: var(--dim); font-weight: 600;
  padding: 3px 8px; border: 1px solid var(--edge); text-align: center;
  position: sticky; z-index: 2;
}
.grid thead th { top: 0; }
.grid tbody th.rowhdr { left: 0; }
/* TEXT row headers (app.js renderTable, .rowhdr.text — a static Y axis whose
   labels are sentences, e.g. the Hayabusa DTC pages naming each fault code).
   Centred, they read as a ragged block; wrapped, the row heights go uneven and
   the tick box drifts away from its label. Left-aligned and unwrapped instead,
   and because table.grid is width:max-content the column simply grows to the
   longest label — which is the point: the whole description has to be legible.
   Numeric axes keep the centred .grid th default.
   white-space:PRE, not nowrap: the labels use runs of spaces as marker columns
   (def-dtc-toggles.mjs labelFor), and `nowrap` still COLLAPSES runs of spaces —
   only `pre` keeps them, and it does not wrap either. */
.grid tbody th.rowhdr.text {
  text-align: left; white-space: pre; font-weight: 500;
  font-variant-numeric: tabular-nums;
}
/* Axis headers are click targets: a column/row header selects that whole
   column/row (app.js headerDown), the corner selects the table. .hdr-on lights
   the headers the marquee spans — the header half of the crosshair. */
.grid th.colhdr, .grid th.rowhdr, .grid th.corner-hdr { cursor: pointer; }
.grid th.colhdr:hover, .grid th.rowhdr:hover, .grid th.corner-hdr:hover { background: #2c323c; color: var(--fg); }
.grid th.hdr-on { background: #33281f; color: #ffd9a8; }
/* RomRaider-style axis/data titles printed on the grid (app.js renderTable):
   X name across the top, Y name rotated down the left, cell-data units along
   the bottom. All three are NON-sticky (position:static overrides the sticky
   .grid th above) so they scroll away instead of colliding with the sticky
   value-header row and row-header column. */
.grid .axis-x-title, .grid .axis-y-title, .grid .axis-data-title {
  position: static; background: var(--panel); color: var(--accent);
  font-weight: 700; letter-spacing: .4px; border-color: var(--edge);
}
.grid thead tr.axis-x-row th { position: static; background: var(--panel); border-color: var(--edge); }
.grid .axis-x-title { padding: 3px 8px; }
.grid tbody th.axis-y-title { position: static; padding: 2px; }
.grid .axis-y-title span {
  display: inline-block; white-space: nowrap;
  writing-mode: vertical-rl; transform: rotate(180deg); /* reads bottom-to-top */
}
.grid tfoot td { border: 1px solid var(--edge); background: var(--panel); cursor: default; }
.grid .axis-data-title { padding: 3px 8px; text-align: center; }
.grid td {
  min-width: 46px; padding: 3px 6px; border: 1px solid #00000055;
  text-align: center; cursor: cell; color: #101216; font-weight: 600;
  box-shadow: var(--dead, inset 0 0 #0000);
}
/* Dead-zone overlay (app.js syncMaskTools / js/reachmask.js): cells the ECU can
   never read. The heat colour is an INLINE background, so the dim has to be an
   inset box-shadow to sit over it — same trick as the marquee below, and both
   compose through --dead / --sel-* vars because separate rules can't add
   box-shadows together. Purely cosmetic: these cells stay editable. */
.grid td.dead { --dead: inset 0 0 0 999px #12151acc; color: #767c86; font-weight: 500; }
/* Marquee: every cell in the drag rect gets .sel (white tint); cells on the
   rect's edges also get sel-t/b/l/r, each contributing one side of the white
   perimeter via a --sel-* var (classes can't compose box-shadow directly).
   Inset shadows draw under cell content, so text and edit inputs stay clear. */
.grid td.sel {
  box-shadow:
    var(--sel-t, inset 0 0 #0000), var(--sel-b, inset 0 0 #0000),
    var(--sel-l, inset 0 0 #0000), var(--sel-r, inset 0 0 #0000),
    inset 0 0 0 999px #ffffff40,
    var(--dead, inset 0 0 #0000); /* last = painted under the tint, so a selected
                                     dead cell still reads as dimmed */
}
/* Crosshair band: cells sharing a row or column with the marquee but not in it
   (app.js paintSelection). Same inset-tint trick as .sel, at a quarter of the
   strength so the marquee still reads as the selection; --dead stays last so a
   masked cell inside the band is still visibly dimmed. Never both .sel and
   .cross — the two rules can't fight. */
.grid td.cross {
  box-shadow: inset 0 0 0 999px #ffffff20, var(--dead, inset 0 0 #0000);
}
.grid td.sel-t { --sel-t: inset 0 2px 0 0 #fff; }
.grid td.sel-b { --sel-b: inset 0 -2px 0 0 #fff; }
.grid td.sel-l { --sel-l: inset 2px 0 0 0 #fff; }
.grid td.sel-r { --sel-r: inset -2px 0 0 0 #fff; }
/* Brush (pen) tool — crosshair cursor over the grid, and a cyan hover ring that
   previews the pen's reach (app.js showBrushHover). Cyan, not white, so it never
   reads as the selection tint; placed AFTER .sel so the ring shows even over a
   stale marquee. .brush-core = the strong middle of the pen, .brush-hi = the rim. */
.grid.brush, .grid.brush td { cursor: crosshair; }
.grid td.brush-hi { box-shadow: inset 0 0 0 999px #38bdf826; }
.grid td.brush-core { box-shadow: inset 0 0 0 999px #38bdf84d; }
.grid td.mod { outline: 2px dashed var(--amber); outline-offset: -2px; }
/* TICK-BOX CELLS (app.js renderToggleCell). No heat colour — the value is a
   state, not a magnitude, and a gradient across fault codes would read as if
   some were "more on" than others. Flat panel colours instead: ticked cells sit
   forward, cleared ones recede, and a hand-typed value that is neither keeps
   its number beside an indeterminate box. Backgrounds are set HERE and not
   inline (unlike every other cell) so .sel / .cross / .dead still compose over
   them through the same inset-shadow trick. */
.grid td.tog { padding: 2px 6px; line-height: 1; }
.grid td.tog-on { background: #22303a; }
.grid td.tog-off { background: #191c22; }
.grid td.tog-custom { background: #33281f; }
.grid td.tog input[type="checkbox"] {
  width: 15px; height: 15px; margin: 0; vertical-align: middle;
  accent-color: var(--accent); cursor: pointer;
}
.grid td.tog input[type="checkbox"]:disabled { cursor: not-allowed; opacity: .35; }
/* The number only appears on a custom value, so it never competes with a box. */
.grid td.tog .tog-val {
  margin-left: 6px; color: #ffd9a8; font-size: 11.5px; vertical-align: middle;
}
/* Δ Change grid shows signed deltas + diverging colors (set inline by diffColor)
   but is editable like the normal grid, so its cells keep the default cell
   cursor — no read-only override here. */
.grid td input {
  width: 100%; border: none; background: #fff; font: inherit; text-align: center;
}
.switch-list { padding: 4px 0; }
.switch-list label {
  display: flex; gap: 10px; align-items: baseline; padding: 6px 8px;
  border: 1px solid var(--edge); border-radius: 4px; margin-bottom: 6px;
  cursor: pointer; max-width: 720px;
}
.switch-list label:hover { background: #22262e; }
.switch-list .unknown { color: var(--amber); }
#statusbar {
  padding: 4px 14px; background: var(--panel); border-top: 1px solid var(--edge);
  color: var(--dim); font-size: 12px;
}
#statusbar.dirty { color: var(--amber); }

/* ---- flash wizard (js/flashui.js builds the DOM) ---- */
.btn.accent { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.btn.accent:hover { background: #f0663f; }

/* ---- Get a Flashing Tool store (js/buytool.js builds the DOM) ---- */
/* A quiet link at the flash step, not a competing accent button. */
.btn.linkish {
  background: none; border: none; color: var(--accent); font-weight: 600;
  padding: 4px 6px; text-decoration: underline; text-underline-offset: 2px;
}
.btn.linkish:hover { color: #f0663f; background: none; }
.ts-overlay {
  position: fixed; inset: 0; z-index: 60; background: rgba(6, 8, 10, .72);
  display: flex; align-items: center; justify-content: center; padding: 16px;
}
.ts-panel {
  width: 620px; max-width: 100%; max-height: calc(100vh - 32px);
  background: var(--panel); border: 1px solid var(--edge); border-radius: 8px;
  box-shadow: 0 14px 60px rgba(0, 0, 0, .7); display: flex; flex-direction: column;
}
.ts-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--edge); }
.ts-title { margin: 0; font-size: 17px; font-weight: 800; letter-spacing: .4px; }
.ts-x {
  margin-left: auto; background: none; border: none; color: var(--dim);
  font-size: 18px; line-height: 1; cursor: pointer; padding: 2px 7px; border-radius: 4px;
}
.ts-x:hover { color: var(--fg); background: #2a2f37; }
.ts-sub { margin: 12px 16px 4px; color: var(--dim); font-size: 13px; }
.ts-list { padding: 8px 16px 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
.ts-empty { color: var(--dim); font-size: 13px; margin: 6px 0 10px; }
.ts-card {
  display: flex; gap: 14px; align-items: stretch;
  border: 1px solid var(--edge); border-radius: 8px; padding: 12px; background: #101216;
}
.ts-card.rec { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.ts-img { width: 108px; height: 108px; object-fit: contain; border-radius: 6px; background: #0b0d10; flex: 0 0 auto; }
.ts-body { display: flex; flex-direction: column; gap: 4px; flex: 1 1 auto; min-width: 0; }
.ts-badge {
  align-self: flex-start; font-size: 11px; letter-spacing: .4px; text-transform: uppercase;
  color: #fff; background: #3a2016; border: 1px solid var(--accent); border-radius: 999px; padding: 2px 9px;
}
.ts-name { margin: 2px 0 0; font-size: 15px; font-weight: 700; }
.ts-blurb { margin: 0; color: var(--dim); font-size: 13px; line-height: 1.4; }
/* Wire-protocol chips the hardware speaks (K-Line, ISO 9141, JTAG, …) — the
   buy-card counterpart of the GT-V1 ad's chip row. Display only. */
.ts-caps { display: flex; flex-wrap: wrap; gap: 4px 6px; margin-top: 3px; }
.ts-cap {
  font-family: ui-monospace, Consolas, monospace; font-size: 10.5px; letter-spacing: .3px;
  color: var(--dim); background: #171b21; border: 1px solid var(--edge); border-radius: 5px;
  padding: 1px 7px; white-space: nowrap;
}
.ts-foot { margin-top: auto; display: flex; align-items: center; gap: 12px; padding-top: 6px; }
.ts-price { font-size: 16px; font-weight: 800; }
.ts-buy { text-decoration: none; }
.ts-buy.disabled { opacity: .55; cursor: default; pointer-events: none; }
@media (max-width: 560px) {
  .ts-card { flex-direction: column; }
  .ts-img { width: 100%; height: 150px; }
}
#flash-overlay {
  position: fixed; inset: 0; z-index: 50; background: rgba(6, 8, 10, .72);
  display: flex; align-items: center; justify-content: center;
}
#flash-modal, #bx-modal {
  width: 660px; max-width: calc(100vw - 32px); max-height: calc(100vh - 32px);
  background: var(--panel); border: 1px solid var(--edge); border-radius: 8px;
  box-shadow: 0 14px 60px rgba(0, 0, 0, .7); display: flex; flex-direction: column;
}
/* Busa "Download flasher" handoff modal (js/busaflash.js) — same shell as the
   flash wizard overlay so the two read as one system. */
#bx-overlay {
  position: fixed; inset: 0; z-index: 50; background: rgba(6, 8, 10, .72);
  display: flex; align-items: center; justify-content: center;
}
.fw-box code, .fw-list code, .fw-body p code {
  background: #101216; border: 1px solid var(--edge); border-radius: 4px;
  padding: 0 5px; font-size: 12px;
}
.fw-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--edge); }
.fw-title { font-weight: 800; letter-spacing: .6px; }
.fw-sim {
  font-size: 11px; letter-spacing: .5px; padding: 2px 9px; border-radius: 999px;
  background: #16301f; color: var(--ok); border: 1px solid #2d5c3d;
}
.fw-close {
  margin-left: auto; background: none; border: none; color: var(--dim);
  font-size: 20px; line-height: 1; cursor: pointer; padding: 2px 7px; border-radius: 4px;
}
.fw-close:hover { color: var(--fg); background: #2a2f37; }
/* Flash-tool store link: wizard header or beside Add USB flasher in the hub. */
.fw-gettool {
  margin-left: auto; background: none; border: none; color: var(--accent);
  font-weight: 600; font-size: 12.5px; cursor: pointer; padding: 2px 6px;
  border-radius: 4px; text-decoration: underline; text-underline-offset: 2px;
}
.fw-gettool:hover { color: #f0663f; }
.ggset-modal .fw-foot { flex-wrap: wrap; }
.ggset-modal .ggset-actions { justify-content: space-between; }
.fw-steps { display: flex; gap: 6px; padding: 12px 16px 0; flex-wrap: wrap; }
.fw-step {
  font-size: 11px; text-transform: uppercase; letter-spacing: .6px; color: var(--dim);
  padding: 3px 10px; border-radius: 999px; border: 1px solid var(--edge);
}
.fw-step.on { color: #fff; border-color: var(--accent); background: #3a2016; }
.fw-step.done { color: var(--ok); border-color: #2d5c3d; }
.fw-body { padding: 14px 18px 16px; overflow-y: auto; font-size: 13px; }
.fw-body p { margin: 0 0 10px; }
.fw-foot { display: flex; gap: 8px; justify-content: flex-end; padding: 12px 16px; border-top: 1px solid var(--edge); }
.fw-kv { display: grid; grid-template-columns: 120px 1fr; gap: 5px 14px; margin: 0 0 12px; }
.fw-kv dt { color: var(--dim); }
.fw-kv dd { margin: 0; }
.fw-note { color: var(--dim); font-size: 12.5px; }
/* Connect-step port status — a primary readout, not a footnote */
.fw-port-status { font-size: 14px; font-weight: 600; }
.fw-box { border: 1px solid var(--edge); border-radius: 6px; padding: 10px 12px; margin: 0 0 10px; }
.fw-box.warn { border-color: #6e3a1e; background: #2a2118; color: #ffd9a8; }
.fw-box.err { border-color: #7a2a2a; background: #2a1717; color: #ffb8b8; }
.fw-box.ok { border-color: #2d5c3d; background: #16301f; color: #a9e8c0; }
.fw-chip { display: inline-block; padding: 1px 10px; border-radius: 999px; font-weight: 700; font-size: 12px; }
.fw-chip.c1 { background: #16283e; color: #7db6f0; border: 1px solid #2a4a6e; }
.fw-chip.c2 { background: #3a2016; color: #ffab8a; border: 1px solid #6e3a1e; }
/* Any controller past the first two (a 3rd module on a multi-ECU vehicle). */
.fw-chip.cx { background: #1f2b22; color: #8fd6a6; border: 1px solid #35553f; }
.fw-switch {
  display: flex; align-items: center; gap: 18px; padding: 16px 18px; margin: 0 0 12px;
  background: #101216; border: 1px solid var(--amber); border-radius: 8px;
}
.fw-switch-num {
  flex: none; width: 86px; height: 86px; display: flex; align-items: center; justify-content: center;
  font-size: 56px; font-weight: 800; color: #101216; background: var(--amber); border-radius: 12px;
}
.fw-switch-txt b { display: block; font-size: 17px; margin-bottom: 4px; }
ol.fw-list { margin: 0 0 4px; padding-left: 22px; }
ol.fw-list li { margin: 4px 0; }
.fw-check {
  display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; margin-top: 12px;
  border: 1px solid var(--edge); border-radius: 6px; cursor: pointer;
}
.fw-check:hover { background: #22262e; }
.fw-check input { margin-top: 2px; }
.flash-tool-picker { margin-bottom: 12px; }
.flash-tool-picker details { margin-top: 10px; }
.flash-tool-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 10px; }
.flash-tool-actions .fw-gettool { margin-left: 0; }
.flash-tool-picker input[type="text"] {
  flex: 1; min-width: 0; padding: 6px 8px; color: var(--fg);
  background: var(--bg); border: 1px solid var(--edge); border-radius: 4px;
}
.flash-tool-picker .btn + .btn, .flash-usb-connection .btn + .btn { margin-left: 6px; }
.fw-stages {
  list-style: none; margin: 0 0 12px; padding: 0;
  display: grid; grid-template-columns: 1fr 1fr; gap: 3px 18px;
}
.fw-stages li { display: flex; gap: 8px; align-items: baseline; color: var(--dim); font-size: 12.5px; padding: 2px 0; }
.fw-stages li .ico { width: 16px; text-align: center; flex: none; }
.fw-stages li.active { color: var(--fg); }
.fw-stages li.active .ico { color: var(--accent); animation: fw-pulse 1s infinite; }
.fw-stages li.done { color: var(--ok); }
.fw-stages li.fail { color: var(--err); }
@keyframes fw-pulse { 50% { opacity: .3; } }
.fw-bar-row { display: flex; align-items: center; gap: 10px; margin: 0 0 8px; }
.fw-bar { flex: 1; height: 14px; background: #101216; border: 1px solid var(--edge); border-radius: 999px; overflow: hidden; }
.fw-bar > div { height: 100%; width: 0%; background: linear-gradient(90deg, #c8431f, var(--accent)); transition: width .12s linear; }
.fw-pct { width: 44px; text-align: right; font: 12.5px Consolas, monospace; color: var(--fg); }
.fw-log {
  height: 120px; overflow-y: auto; background: #101216; border: 1px solid var(--edge);
  border-radius: 4px; padding: 6px 9px; font: 11.5px Consolas, monospace; color: var(--dim);
}
.fw-log div { white-space: pre-wrap; }
.fw-result-title { font-size: 16px; font-weight: 800; margin-bottom: 4px; }
ul.fw-guide { margin: 6px 0 0; padding-left: 20px; }
ul.fw-guide li { margin: 4px 0; }

/* ---- My Tunes library (js/tunelib.js builds the DOM; reuses fw-* modal
   chrome so both modals match). z-index below the flash overlay. ---- */
#tl-overlay {
  position: fixed; inset: 0; z-index: 40; background: rgba(6, 8, 10, .72);
  display: flex; align-items: center; justify-content: center;
}
#tl-overlay[hidden] { display: none; } /* display:flex would defeat the hidden attribute */
#tl-modal {
  width: 760px; max-width: calc(100vw - 32px); max-height: calc(100vh - 32px);
  background: var(--panel); border: 1px solid var(--edge); border-radius: 8px;
  box-shadow: 0 14px 60px rgba(0, 0, 0, .7); display: flex; flex-direction: column;
}
/* The modal body scrolls as one column: Open BIN, base tunes, then saved tunes.
   (Was .tl-list scrolling on its own; the base-tune sections moved that up a level
   so all three sections share one scroll.) */
.tl-body {
  overflow-y: auto; padding: 12px 14px; min-height: 140px;
  display: flex; flex-direction: column; gap: 10px;
}
#tl-openbin { margin: 0; }
#tl-bases { display: flex; flex-direction: column; gap: 8px; }
.tl-section-h {
  font-weight: 700; font-size: 11.5px; letter-spacing: .5px; text-transform: uppercase;
  color: var(--dim);
}
/* Garage filter bar above the Gurued-tunes list (2026-09-01). */
.tl-bikefilter {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin: 2px 0 6px; font-size: 12.5px;
}
.tl-bikefilter .tl-bf-lbl { color: var(--dim); font-weight: 600; }
.tl-bikefilter select {
  background: var(--panel, #1c1f25); color: var(--fg); border: 1px solid var(--edge);
  border-radius: 7px; padding: 5px 8px; font: 12.5px system-ui; max-width: 100%;
}
.tl-bikefilter .tl-bf-mods { color: var(--accent, #e8552e); font-weight: 600; }
/* Admin-only "show whole catalog" toggle above the Gurued-tunes list. */
.tl-showall {
  display: flex; align-items: center; gap: 6px;
  margin: 2px 0 8px; font-size: 12px; color: var(--dim); cursor: pointer;
}
.tl-showall input { accent-color: var(--accent); cursor: pointer; }
.tl-make { font-weight: 600; color: var(--fg); margin-top: 2px; }
/* The tunes DRILL-DOWN (2026-08-12): MAKE group (.tl-group.tl-make) -> MODEL group
   (.tl-folder) -> tune-family subheading (.tl-folder-h, only when >1) -> variant
   rows, as nested <details>. All start collapsed — "collapse down to the Make and
   expand from there". Native disclosure = no JS state to lose. */
.tl-group {
  border: 1px solid var(--edge); border-radius: 8px; padding: 0 10px;
}
.tl-group[open] { padding-bottom: 8px; }
/* A tune-family label inside a model that holds more than one family. */
.tl-folder-h {
  font-size: 13px; font-weight: 700; letter-spacing: .4px; text-transform: uppercase;
  color: var(--fg); margin: 12px 0 4px 14px; padding-bottom: 3px;
  border-bottom: 1px solid var(--edge);
}
/* "unreleased" badge — a draft only a full-access account sees in the tunes list. */
.tl-draft {
  flex: none; font-size: 10px; font-weight: 700; letter-spacing: .4px;
  color: #e8c877; background: rgba(232, 200, 119, .12);
  border: 1px solid #5a4a24; border-radius: 9px; padding: 1px 7px;
}
.tl-gsum {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  padding: 9px 2px; font-size: 13px; list-style-position: inside;
}
.tl-gsum:hover .tl-gname { color: #ffd9a8; }
.tl-gsum .tl-gname { font-weight: 600; }
.tl-gsum .tl-gcount {
  margin-left: auto; font-size: 11px; color: var(--dim);
  background: rgba(255, 255, 255, .05); border-radius: 9px; padding: 1px 8px;
}
.tl-folder { margin: 4px 0 4px 14px; }
.tl-folder > .tl-gsum { padding: 6px 2px; }
.tl-folder .tl-baserow { margin: 4px 0 4px 14px; }
.tl-baserow {
  display: flex; align-items: center; gap: 12px; padding: 7px 12px;
  border: 1px solid var(--edge); border-radius: 6px;
}
.tl-baserow .tl-name { font-weight: 600; white-space: normal; }
/* Whole-row Load (2026-09-03, Tim): rows that delegate their click to the Load
   button say so with the cursor — the hover highlight is no longer a lie. */
.tl-rowload { cursor: pointer; }
.tl-list { display: flex; flex-direction: column; }
.tl-row {
  display: flex; align-items: center; gap: 12px; padding: 8px 12px;
  border: 1px solid var(--edge); border-radius: 6px; margin-bottom: 8px;
}
.tl-main { flex: 1; min-width: 0; cursor: pointer; }
.tl-main:hover .tl-name { color: #ffd9a8; }
.tl-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tl-meta { color: var(--dim); font-size: 11.5px; }
/* Storage-location badge (device only vs cloud-synced) inside the meta line. */
.tl-loc { display: inline-block; padding: 0 6px; border-radius: 8px; font-weight: 600;
          font-size: 10.5px; line-height: 15px; white-space: nowrap; vertical-align: baseline; }
.tl-loc.loc-cloud   { color: #8fe3a2; background: rgba(120, 220, 150, 0.14); }
.tl-loc.loc-pending { color: #ffc98a; background: rgba(255, 190, 120, 0.14); }
.tl-loc.loc-local   { color: var(--dim); background: rgba(160, 170, 180, 0.12); }
/* The "device only" pill is a clickable "Sync now" button — reset the native
   button chrome and add affordances (pointer, border, hover). */
button.tl-loc { font-family: inherit; border: 1px solid rgba(255, 190, 120, 0.45);
                cursor: pointer; transition: background 0.12s, color 0.12s; }
button.tl-loc.loc-pending:hover { background: rgba(255, 190, 120, 0.28); color: #ffdcae; }
/* Clicking a tune name opens this details popup (over the My Tunes list). */
#tl-detail-overlay {
  position: fixed; inset: 0; z-index: 70; background: rgba(6, 8, 10, .72);
  display: flex; align-items: center; justify-content: center;
}
#tl-detail-modal {
  width: 480px; max-width: calc(100vw - 32px); max-height: calc(100vh - 32px);
  background: var(--panel); border: 1px solid var(--edge); border-radius: 8px;
  box-shadow: 0 14px 60px rgba(0, 0, 0, .7); display: flex; flex-direction: column;
}
.tl-det-name { font-weight: 700; font-size: 15px; margin-bottom: 12px; word-break: break-word; }
.tl-det-sec { margin-bottom: 14px; }
.tl-det-h { font-size: 10.5px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase;
            color: var(--dim); border-bottom: 1px solid var(--edge); padding-bottom: 4px; margin-bottom: 6px; }
.tl-det-grid { display: grid; grid-template-columns: minmax(88px, auto) 1fr; gap: 4px 12px; align-items: baseline; }
.tl-det-k { color: var(--dim); font-size: 12px; }
.tl-det-v { font-size: 12.5px; word-break: break-word; }
.tl-det-hash { font-size: 11px; word-break: break-all; color: var(--dim); }
.tl-det-sync { margin-top: 10px; }
.tl-det-sync:disabled { opacity: .6; cursor: default; }
.tl-actions { display: flex; gap: 6px; flex: none; }
#tl-advanced, .tl-single-parts { margin: 12px 0; color: var(--dim); font-size: 12px; }
#tl-advanced > summary, .tl-single-parts > summary { cursor: pointer; padding: 8px 0; }
.tl-single-parts .btn { margin: 4px 6px 4px 0; }

/* 013 — the "List for sale" card (tunelib.js sellTune): one overlay, title/
   price/notes + a saved-tune slot per controller. Same dark palette. */
.tl-sell-overlay {
  position: fixed; inset: 0; background: rgba(4,8,12,.72); z-index: 1200;
  display: flex; align-items: center; justify-content: center;
}
.tl-sell-card {
  width: min(480px, calc(100% - 40px)); background: #101823;
  border: 1px solid #2a3a4d; border-radius: 12px; padding: 16px 18px;
  color: #dfe8f0; box-shadow: 0 12px 40px rgba(0,0,0,.55);
}
.tl-sell-h { font-size: 15px; font-weight: 700; margin-bottom: 6px; }
.tl-sell-hint { font-size: 12px; color: #e0b36a; margin-bottom: 10px; }
.tl-sell-row { display: flex; align-items: center; gap: 10px; margin: 7px 0; }
.tl-sell-lbl { flex: 0 0 84px; font-size: 12px; color: #9fb4c6; }
.tl-sell-row input, .tl-sell-row select {
  flex: 1; min-width: 0; background: #0c141d; border: 1px solid #2a3a4d;
  color: #dfe8f0; border-radius: 7px; padding: 6px 9px; font-size: 13px;
}
.tl-sell-act { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
.tl-sell-hint2 { font-size: 12px; color: #e0b36a; }
.tl-empty { color: var(--dim); text-align: center; padding: 30px 10px; }

/* ---- dead-zone dialog (js/deadzoneui.js builds the DOM) ----
   Reuses the fw-* modal chrome (head/body/foot/box/note) so it matches the flash
   wizard and My Tunes. dz-row is a label + control line; the controls are wide
   because saved-tune and YCC-T map names run long. */
#dz-overlay {
  position: fixed; inset: 0; z-index: 55; background: rgba(6, 8, 10, .72);
  display: flex; align-items: center; justify-content: center;
}
#dz-overlay[hidden] { display: none; } /* display:flex would defeat the hidden attribute */
#dz-modal {
  width: 620px; max-width: calc(100vw - 32px); max-height: calc(100vh - 32px);
  background: var(--panel); border: 1px solid var(--edge); border-radius: 8px;
  box-shadow: 0 14px 60px rgba(0, 0, 0, .7); display: flex; flex-direction: column;
}
.dz-row {
  display: flex; align-items: center; gap: 12px; margin: 0 0 10px; cursor: pointer;
}
.dz-row > span { flex: none; width: 150px; color: var(--dim); }
.dz-row select {
  flex: 1; min-width: 0; padding: 4px 8px; font: inherit; font-size: 12.5px;
  background: #101216; border: 1px solid var(--edge); border-radius: 4px; color: var(--fg);
}
.dz-row select:focus { outline: none; border-color: var(--accent); }
.dz-row select:disabled { opacity: .5; }
.dz-row input[type=checkbox] { width: 16px; height: 16px; accent-color: var(--accent); }
#dz-summary { margin-top: 14px; line-height: 1.55; }
#dz-flatten:disabled { opacity: .45; cursor: default; }

/* ---- Busa in-browser flash modal (busaflashdirect.js) ---- */
.bf-overlay { position: fixed; inset: 0; z-index: 56; background: rgba(6, 8, 10, .72);
  display: flex; align-items: center; justify-content: center; }
.bf-modal { width: 640px; max-width: calc(100vw - 32px); max-height: calc(100vh - 32px);
  background: var(--panel); border: 1px solid var(--edge); border-radius: 8px;
  box-shadow: 0 14px 60px rgba(0, 0, 0, .7); display: flex; flex-direction: column; }
.bf-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--edge); }
.bf-title { font-weight: 800; letter-spacing: .4px; }
.bf-danger { color: #e05b5b; font-weight: 600; letter-spacing: 0; }
.bf-close { margin-left: auto; background: none; border: none; color: var(--dim);
  font-size: 20px; line-height: 1; cursor: pointer; padding: 2px 7px; border-radius: 4px; }
.bf-close:hover { color: var(--fg); background: #2a2f37; }
.bf-close:disabled { opacity: .35; cursor: default; }
.bf-body { padding: 14px 18px 16px; overflow-y: auto; font-size: 13px; }
.bf-step { margin: 0 0 12px; }
.bf-step-title { margin: 0 0 8px; font-size: 18px; line-height: 1.3; font-weight: 800; }
.bf-step .btn { margin-right: 8px; }
.bf-license-warning { margin-bottom: 18px; }
.bf-license-warning > summary { cursor: pointer; font-weight: 700; padding: 4px 0; }
.bf-license-warning[open] > summary { margin-bottom: 8px; }
.bf-verification { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--edge); }
.bf-verification [data-vin-state] { margin-bottom: 8px; }
.bf-verification .btn { margin-right: 8px; }
.bf-verification a { color: #7db6f0; }
#bf-warn { border: 1px solid #6e3a1e; background: #2a2118; color: #ffd9a8;
  border-radius: 6px; padding: 9px 11px; font-size: 12.5px; margin: 0 0 12px; }
.bf-barwrap { height: 14px; background: #0f1114; border: 1px solid var(--edge); border-radius: 4px; overflow: hidden; margin: 8px 0 4px; }
.bf-barfill { height: 100%; width: 0%; background: var(--ok); transition: width .2s; }
.bf-log { margin: 8px 0 0; padding: 9px 11px; border: 1px solid var(--edge); border-radius: 6px;
  background: #0c0f13; font: 12px/1.5 Consolas, monospace; white-space: pre-wrap;
  max-height: 30vh; overflow-y: auto; }
.bf-foot { display: flex; gap: 8px; justify-content: flex-end; padding: 12px 16px; border-top: 1px solid var(--edge); }
.bf-dim { color: var(--dim); } .bf-ok { color: var(--ok); } .bf-err { color: #e05b5b; }
.bf-warn { color: #e8c15b; } .bf-rx { color: #7db6f0; }

/* ---- CAN-X2 board flash modal (busaflashcanx2.js) ----
   Reuses the bf-* modal chrome; only the board-address input and the image-mode
   controls are its own. */
.cx-addr { background: #0f1114; color: var(--fg); border: 1px solid var(--edge);
  border-radius: 4px; padding: 4px 8px; font: inherit; font-size: 12.5px; width: 190px;
  font-family: Consolas, monospace; margin-right: 8px; }
.cx-addr:focus { outline: none; border-color: var(--accent); }
.cx-addr::placeholder { color: var(--dim); }
.cx-modes { display: flex; flex-direction: column; gap: 5px; margin: 6px 0 2px; }
.cx-mode { display: flex; align-items: flex-start; gap: 7px; font-size: 12.5px; cursor: pointer; }
.cx-mode input { flex: none; margin-top: 2px; accent-color: var(--accent); }
.cx-bench-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 6px 0 2px; }
.cx-bench-row input[type="file"] { font-size: 12px; color: var(--dim); max-width: 100%; }
/* The image's integrity checkwords — recomputed per edit so the operator can see
   them move. Monospace values, wrapping chips. */
.cx-imgcard { margin: 6px 0 2px; border: 1px solid var(--edge); border-radius: 6px;
  background: rgba(255,255,255,.02); }
.cx-imgcard > summary { list-style: none; cursor: pointer; padding: 7px 10px; font-size: 12.5px;
  color: var(--fg); display: flex; align-items: center; gap: 7px; user-select: none; }
.cx-imgcard > summary::-webkit-details-marker { display: none; }
.cx-imgcard > summary::before { content: "▸"; color: var(--dim); font-size: 11px; transition: transform .12s; }
.cx-imgcard[open] > summary::before { transform: rotate(90deg); }
.cx-imgcard > summary::after { content: "details"; margin-left: auto; color: var(--dim); font-size: 11px; }
.cx-imgcard[open] > summary::after { content: "hide"; }
.cx-imgcard[open] > summary { border-bottom: 1px solid var(--edge); }
.cx-imgcard > #cx-image, .cx-imgcard > #cx-note { margin-left: 10px; margin-right: 10px; }
.cx-imgcard > #cx-note { margin-bottom: 10px; }

.cx-cksum { display: flex; flex-wrap: wrap; gap: 3px 14px; margin: 6px 0 3px; }
.cx-cksum span { font-size: 11.5px; color: var(--dim); }
.cx-cksum code { color: var(--fg); font: 700 12px Consolas, monospace; font-variant-numeric: tabular-nums; }

/* ---- One "Flash" button + the transport chooser (busaflashselect.js) ---- */
.flash-combo { display: inline-flex; align-items: center; gap: 6px; }
/* The base rule above sets display, which by itself DEFEATS the `hidden` attribute
   (a class selector outranks the UA `[hidden]{display:none}`), so busaflashselect.js
   setting `combo.hidden = true` on a non-flashExport bike (e.g. the V-Max, which
   flashes via #flash-btn "Flash to ECU") left the combo "Flash" button visible —
   two flash buttons. This restores the hidden semantics. */
.flash-combo[hidden] { display: none; }
.fc-modal { width: 460px; }
.fc-list { display: flex; flex-direction: column; gap: 8px; }
.fc-opt { display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  text-align: left; width: 100%; padding: 10px 12px; background: #0f1114;
  border: 1px solid var(--edge); border-radius: 6px; color: var(--fg); cursor: pointer; }
.fc-opt:hover:not(:disabled) { border-color: var(--accent); background: #14181d; }
.fc-opt:disabled { opacity: .5; cursor: default; }
.fc-opt-label { font-weight: 700; font-size: 13.5px; }
.fc-opt-sub { font-size: 11.5px; color: var(--dim); }
.fc-badge { font-size: 10px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  color: var(--accent); border: 1px solid var(--accent); border-radius: 4px; padding: 0 5px; margin-left: 6px; }

/* ---- Busa live telemetry panel (busalivestream.js) ----
   Reuses the bf-* modal chrome deliberately: same shell, but NO danger styling
   anywhere in here. The panel is listen-only at the hardware level, and dressing
   it like the flash modal would teach customers to ignore the red. */
.bf-modal.ls-wide { width: 880px; }
.ls-safe { color: var(--ok); font-weight: 600; letter-spacing: 0; }
.ls-note { color: var(--dim); font-size: 12.5px; margin: -4px 0 14px; line-height: 1.5; }
.ls-sect { margin: 0 0 14px; }
.ls-secthead { font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
  color: var(--dim); margin: 0 0 7px; border-bottom: 1px solid var(--edge); padding-bottom: 4px; }
.ls-secthead .bf-dim { text-transform: none; letter-spacing: 0; }
.ls-empty { line-height: 1.5; padding: 4px 0 2px; }

/* value tiles */
.ls-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.ls-tile { background: #0f1114; border: 1px solid var(--edge); border-radius: 6px; padding: 7px 9px 6px; }
.ls-tile.ls-mine { border-color: #3a4553; }
.ls-tile.ls-unsure { border-style: dashed; }
.ls-tname { font-size: 11.5px; color: var(--dim); display: flex; align-items: center; gap: 4px; min-height: 18px; }
.ls-tval { font: 700 21px/1.15 Consolas, monospace; margin: 2px 0 1px;
  font-variant-numeric: tabular-nums; }
.ls-tfoot { display: flex; justify-content: space-between; gap: 6px; font-size: 10.5px; color: var(--dim); }
.ls-tsrc { font-family: Consolas, monospace; }
.ls-tedit { flex: 1; min-width: 0; background: transparent; border: none; border-bottom: 1px dashed #3a4553;
  color: var(--fg); font: inherit; padding: 1px 0; }
.ls-tedit:focus { outline: none; border-bottom-color: var(--accent); }
.ls-tdel { background: none; border: none; color: var(--dim); cursor: pointer; font-size: 14px;
  line-height: 1; padding: 0 2px; }
.ls-tdel:hover { color: var(--err); }

/* raw per-ID byte watches */
.ls-raw { display: flex; flex-direction: column; gap: 5px; }
.ls-row { display: flex; align-items: center; gap: 10px; }
.ls-row.ls-stale { opacity: .45; }
.ls-rowhead { flex: 0 0 190px; display: flex; align-items: baseline; gap: 6px; overflow: hidden; }
.ls-id { font: 700 13px Consolas, monospace; }
.ls-rowname { font-size: 11.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ls-rate { margin-left: auto; font-size: 10.5px; color: var(--dim); white-space: nowrap;
  font-variant-numeric: tabular-nums; }
.ls-bytes { display: flex; gap: 4px; }
.ls-byte { position: relative; width: 52px; background: #0f1114; border: 1px solid var(--edge);
  border-radius: 4px; padding: 2px 3px 3px; color: var(--fg); cursor: pointer; text-align: center; }
.ls-byte:hover { border-color: var(--accent); }
.ls-byte.ls-flat { opacity: .5; }            /* never moved — almost certainly padding */
.ls-byte.ls-moving { border-color: var(--amber); }
.ls-byte.ls-watched { background: #1d2530; border-color: #4a90d9; }
.ls-bidx { display: block; font-size: 9px; color: var(--dim); line-height: 1; }
.ls-bval { display: block; font: 700 13px/1.25 Consolas, monospace; font-variant-numeric: tabular-nums; }
/* horizontal fill = where this byte sits inside the range it has covered so far,
   so a sweeping value reads at a glance without watching the hex digits */
.ls-bbar { display: block; height: 3px; background: #23272e; border-radius: 2px; overflow: hidden; }
.ls-bbar i { display: block; height: 100%; width: 0%; background: var(--accent); }
.ls-brange { display: block; font: 9px Consolas, monospace; color: var(--dim); line-height: 1.3; }
.ls-missing { margin-top: 8px; font-size: 12px; }
.ls-tools { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 4px; }
.ls-chk { color: var(--dim); font-size: 12.5px; display: inline-flex; align-items: center; gap: 5px; }
.ls-chk input { width: 15px; height: 15px; accent-color: var(--accent); }

/* add a RAM cell to watch by address (js/live/discovery.js) — the typed
   equivalent of clicking a byte, plus the dev-only catalog picker under it */
.ls-add, .ls-pick { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
.ls-add-lbl, .ls-pick-lbl { color: var(--dim); font-size: 12.5px; }
.ls-add input, .ls-add select, .ls-pick select {
  background: #0f1114; color: var(--fg); border: 1px solid var(--edge);
  border-radius: 4px; padding: 3px 6px; font-size: 12.5px; }
.ls-add input:focus, .ls-add select:focus, .ls-pick select:focus { outline: none; border-color: var(--accent); }
.ls-add-addr { width: 120px; font-family: Consolas, monospace; }
.ls-add-name { width: 150px; }
.ls-add-addr::placeholder, .ls-add-name::placeholder { color: var(--dim); }
.ls-add-msg { font-size: 12px; color: var(--dim); }
.ls-add-msg.ok { color: var(--ok); }
.ls-add-msg.warn { color: var(--amber); }
.ls-pick-sel { max-width: 340px; }

/* ---------- group builder (js/groupbuilder.js) ----------
   "Pick the main map, then pick what to group with it." Same overlay + .fw-*
   wizard chrome as the flash and compare dialogs, so the three read as one
   system; only the two list idioms below are its own. */
#gb-overlay {
  position: fixed; inset: 0; z-index: 56; background: rgba(6, 8, 10, .72);
  display: flex; align-items: center; justify-content: center;
}
#gb-overlay[hidden] { display: none; } /* display:flex would defeat [hidden] */
#gb-modal {
  width: 760px; max-width: calc(100vw - 32px); max-height: calc(100vh - 32px);
  background: var(--panel); border: 1px solid var(--edge); border-radius: 8px;
  box-shadow: 0 14px 60px rgba(0, 0, 0, .7); display: flex; flex-direction: column;
}
#gb-body { flex: 1 1 auto; }
.gb-search {
  width: 100%; box-sizing: border-box; margin: 0 0 8px;
  padding: 6px 10px; font: inherit; font-size: 13px;
  background: #101216; border: 1px solid var(--edge); border-radius: 4px; color: var(--fg);
}
.gb-search:focus { outline: none; border-color: var(--accent); }
/* The map list. Scrolls inside the dialog rather than growing it — a def can
   hold 250 maps and the footer buttons have to stay reachable. */
.gb-list {
  max-height: 42vh; overflow-y: auto;
  border: 1px solid var(--edge); border-radius: 6px; padding: 4px;
}
.gb-row {
  display: flex; align-items: baseline; gap: 9px;
  padding: 4px 7px; border-radius: 4px; cursor: pointer;
}
.gb-row:hover { background: #22262e; }
.gb-row.on { background: #33281f; }
.gb-row input { flex: none; accent-color: var(--amber); margin: 0; }
.gb-name { flex: 1 1 auto; min-width: 0; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gb-meta { flex: 0 1 auto; font-size: 11.5px; color: var(--dim); white-space: nowrap; }
/* A map that CANNOT join this main. Listed, not hidden: the reason in .gb-meta
   is the whole point — "where is cylinder 3" answers itself. */
.gb-row.bad { cursor: not-allowed; opacity: .55; }
.gb-row.bad:hover { background: none; }
.gb-row.bad .gb-meta { color: #d08a6a; white-space: normal; }
/* The chosen main, pinned above the member list in step 2. */
.gb-main-chip {
  display: flex; align-items: center; gap: 9px; margin: 0 0 10px;
  padding: 7px 10px; border: 1px solid var(--amber); border-radius: 6px; background: #241d16;
}
.gb-main-lbl {
  flex: none; font-size: 9.5px; letter-spacing: .5px; text-transform: uppercase;
  color: var(--amber); border: 1px solid var(--amber); border-radius: 3px; padding: 0 4px;
}
.gb-bulk { display: flex; align-items: center; gap: 8px; margin: 0 0 8px; }
.gb-field { display: flex; align-items: flex-start; gap: 12px; margin: 0 0 10px; }
.gb-field > label { flex: 0 0 70px; padding-top: 6px; color: var(--dim); font-size: 12px; }
.gb-input {
  flex: 1 1 auto; min-width: 0; box-sizing: border-box; padding: 6px 9px;
  font: inherit; font-size: 13px; resize: vertical;
  background: #101216; border: 1px solid var(--edge); border-radius: 4px; color: var(--fg);
}
.gb-input:focus { outline: none; border-color: var(--accent); }
.gb-input:disabled { color: var(--dim); }
.gb-h { margin: 14px 0 8px; font-size: 13px; }
/* The two edit models, as cards rather than a radio pair: the consequence text
   IS the choice, and there is no default (js/groupbuilder.js). */
.gb-model {
  display: flex; gap: 10px; align-items: flex-start; cursor: pointer;
  padding: 9px 11px; margin: 0 0 8px; border: 1px solid var(--edge); border-radius: 6px;
}
.gb-model:hover { background: #22262e; }
.gb-model.on { border-color: var(--amber); background: #241d16; }
.gb-model input { flex: none; margin-top: 3px; accent-color: var(--amber); }
.gb-model-txt b { display: block; font-size: 13.5px; margin-bottom: 3px; }
.gb-model-txt span { color: var(--dim); font-size: 12.5px; line-height: 1.5; }
.gb-summary { margin-top: 12px; }
/* Pushes Delete to the left of the .fw-foot row, away from Cancel/Save. */
.gb-spacer { flex: 1 1 auto; }

/* ============================================================================
 * RESPONSIVE / MOBILE LAYER (2026-08-26)
 * ----------------------------------------------------------------------------
 * The whole app was authored desktop-first: a fixed 300px #sidebar beside a
 * flex:1 #content. On a phone the sidebar ate the screen and #content — which
 * holds the map table AND the 3D surface (both drawn by mapview.js onto a
 * width:100%/height:100% canvas that redraws via ResizeObserver) — was squeezed
 * to a sliver, so neither the table nor the 3D graph was visible. That was the
 * reported symptom.
 *
 * The renderer needs NO changes: give #content real width and it draws at that
 * width. So this layer is pure chrome —
 *   1. Below 820px the sidebar becomes an off-canvas DRAWER (position:fixed,
 *      translateX(-100%)) revealed by body.nav-open; #content then owns the
 *      full width because a fixed element leaves the #layout flex flow. The
 *      drawer OVERLAYS content, so opening/closing it never resizes the canvas.
 *   2. #sidebar-toggle (the ☰ button in the topbar) shows only here; a
 *      #nav-scrim dims the page behind the open drawer. Wiring is a small
 *      classic script at the foot of index.html (dash.html reuses the same ids).
 *   3. Bigger tap targets, tighter padding, and real <table>s (admin/live pages)
 *      get an x-scroll so wide columns can be reached instead of clipped.
 * The ids/classes here (#sidebar-toggle, #nav-scrim, body.nav-open) are the
 * contract the page script depends on — keep them in sync if you rename.
 * ========================================================================== */

/* Hamburger is desktop-hidden; the media query below reveals it. Kept out of the
   query so a page that forgets the toggle button simply never shows one. */
#sidebar-toggle { display: none; }
#nav-scrim { display: none; }
/* Phone-only in-content nav button/prompt — revealed in the <=820px query below. */
#nav-prompt { display: none; }

@media (max-width: 820px) {
  /* Tools ▾ list (app.js positionList pins it fixed + clamps it inside the
     viewport on a phone): a long item wraps inside the capped width instead of
     pushing the box past the screen edge. */
  .role-menu-item { white-space: normal; min-height: 34px; }
  /* --- Sidebar → off-canvas drawer --------------------------------------- */
  #sidebar {
    position: fixed; top: 0; left: 0; bottom: 0;
    width: 86vw; max-width: 340px; min-width: 0;
    z-index: 60; padding-top: 14px;
    transform: translateX(-100%);
    transition: transform .22s ease;
    box-shadow: 2px 0 26px rgba(0, 0, 0, .6);
    -webkit-overflow-scrolling: touch;
  }
  body.nav-open #sidebar { transform: none; }
  /* Dim backdrop behind the open drawer; tap it to close (wired in-page). */
  #nav-scrim {
    display: block; position: fixed; inset: 0; z-index: 55;
    background: rgba(0, 0, 0, .5);
    opacity: 0; pointer-events: none; transition: opacity .22s ease;
  }
  body.nav-open #nav-scrim { opacity: 1; pointer-events: auto; }
  /* A fixed sidebar leaves the flex row, so #content already spans full width;
     min-width:0 stays defensive against a stray min-content child. */
  #content { min-width: 0; }

  /* --- Show the hamburger, give it a proper tap target ------------------- */
  #sidebar-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    flex: none; font-size: 20px; line-height: 1;
    width: 40px; height: 36px; padding: 0;
  }
  /* When the in-content nav prompt is present (index.html), it OWNS the phone nav:
     hide the topbar ☰ so there is one, more visible affordance under the header. */
  body.has-nav-prompt #sidebar-toggle { display: none; }

  /* --- In-content nav button / first-run prompt (#nav-prompt) ------------- */
  /* Lives at the top-left of #content, just under the header — the corner ☰ read as
     too hidden. Collapsed it is a 40px ☰ tile; while no bike is bound (body.no-vehicle)
     it EXPANDS into a labelled prompt, animating the label's width + fade so it slides
     back down to the three lines the moment a tune is bound. */
  #nav-prompt {
    display: inline-flex; align-items: center; gap: 8px;
    margin: 8px 0 0 8px; padding: 0 0 0 9px; height: 36px;
    background: #2a2f37; border: 1px solid var(--edge); border-radius: 6px;
    color: var(--fg); cursor: pointer; overflow: hidden;
    -webkit-tap-highlight-color: transparent;
  }
  #nav-prompt:hover { background: #343a44; }
  #nav-prompt .np-lines {
    flex: none; width: 22px; text-align: center; font-size: 20px; line-height: 1;
  }
  /* The label collapses to zero width (and fades) when a bike is bound. Animating
     max-width + opacity + margin gives the slide-down-to-three-lines effect. */
  #nav-prompt .np-label {
    max-width: 0; opacity: 0; margin-right: 0; white-space: nowrap; font-size: 13px;
    transition: max-width .28s ease, opacity .22s ease, margin-right .28s ease;
  }
  body.no-vehicle #nav-prompt {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent), 0 4px 14px rgba(0, 0, 0, .4);
  }
  body.no-vehicle #nav-prompt .np-label {
    max-width: 260px; opacity: 1; margin-right: 12px;
  }
  /* A gentle one-time attention pulse while unbound, so the prompt is noticed. */
  body.no-vehicle #nav-prompt .np-lines { animation: np-pulse 1.6s ease-in-out 3; }
  @keyframes np-pulse {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.14); color: var(--accent); }
  }

  /* --- Chrome tuning ------------------------------------------------------ */
  #topbar { padding: 8px 10px; }
  #brand { font-size: 15px; }
  #grid-wrap { padding: 8px; }        /* reclaim edge space for the map */
  #table-head { padding: 8px 10px 6px; }
  /* Comfortable finger targets on the tool row without reflowing the desktop
     button sizing. The #actions verb row is trimmed ~10% below (Tim 2026-09-02);
     the map toolbar (#table-tools) keeps the roomier target. */
  #table-tools .btn { min-height: 34px; }
  /* Tune-verb row trimmed ~10% on the phone: smaller text/padding/height so the
     eight verbs pack tighter. The map toolbar buttons are left as-is. */
  #actions .btn { min-height: 30px; font-size: 11.7px; padding: 4px 10px; }
  #actions #autosave-lbl { min-height: 30px; font-size: 11.7px; }
  /* The floating cluster hugs the corner tighter on a phone, and stays a VERTICAL
     stack there — a phone has no room for the desktop horizontal perimeter row. */
  #view-fab { right: 12px; bottom: 12px; flex-direction: column; align-items: flex-end; }
  #touch-pen-mode { right: 8px; top: 8px; }

  /* --- Give the MAP the screen back (2026-08-27). Six wrapped tool rows left
     the grid ~40px tall. The toolbar becomes ONE horizontally scrollable strip
     (the same pattern #actions uses at 480px); nothing is lost, it scrolls. */
  #table-tools {
    flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch;
    padding-bottom: 12px; scrollbar-width: thin; position: relative;
  }
  #table-tools > * { flex: none; }
  /* The strip's scrollbar needs real height or it can't be grabbed at all —
     swiping the strip itself scrolls it too, but the slider must be usable.
     (iOS ignores all of this — there the minimap scrubber below is the UI.) */
  #table-tools::-webkit-scrollbar { height: 12px; }
  #table-tools::-webkit-scrollbar-track { background: #101216; border-radius: 4px; }
  #table-tools::-webkit-scrollbar-thumb { background: #3a4048; border-radius: 4px; }
  #table-tools::-webkit-scrollbar-thumb:active { background: var(--accent); }
  /* Right-edge fade = "more tools this way". A sticky flex item pinned to the
     strip's right edge; the negative margin means it costs no strip width.
     pointer-events:none so the button under it still taps. */
  #table-tools::after {
    content: ""; position: sticky; right: 0; flex: none; align-self: stretch;
    width: 26px; margin-left: -26px;
    background: linear-gradient(90deg, transparent, var(--bg));
    pointer-events: none;
  }

  /* --- Toolbar MINIMAP scrubber (app.js "toolbar minimap"). While the strip
     scrolls, the WHOLE bar appears in miniature floating just above it: each
     control a block, the lit window = what's on screen. Dragging the minimap
     slides that part of the strip into full view. Fades ~1s after the last
     touch. This is also the iOS answer, where scrollbars can't be styled. */
  #tools-minimap {
    position: fixed; z-index: 70; overflow: hidden;
    background: rgba(16, 18, 22, .94); border: 1px solid var(--edge);
    border-radius: 6px; touch-action: none;
    box-shadow: 0 4px 14px rgba(0, 0, 0, .5);
  }
  #tools-minimap[hidden] { display: none; }
  /* The miniature is a live CLONE of the strip (real buttons, real text),
     scaled down by transform (app.js sets width + scale). Inert: no pointer
     events, no listeners cloned. Its own scroll chrome is stripped. */
  #tools-minimap .mm-clone {
    position: absolute; left: 0; top: 0; pointer-events: none;
    overflow: visible !important; padding-bottom: 0 !important; margin: 0 !important;
    scrollbar-width: none;
  }
  #tools-minimap .mm-clone::after { content: none !important; }
  #tools-minimap i.mm-win {
    position: absolute; top: 0; bottom: 0; border-radius: 4px;
    background: rgba(232, 85, 46, .16); border: 1px solid var(--accent);
  }
  /* Brush mode on a phone: one finger paints, so no marquee can be dragged —
     every selection-fed control (.sel-fed, marked in index.html) is dead
     weight while the pen is out. The pen knobs, the Light/Med/Full pill (it
     drives the smoothing pen too, so it shows when the pen is Smoothing —
     see .bmode-smooth), Δ/Trace/Dead zone and Revert table stay. */
  #table-tools.brush-tool .sel-fed { display: none; }
  /* The desktop two-row split doesn't apply to the phone's horizontal scroll
     strip (it's flex-wrap:nowrap): kill the full-width break and the pen-knob
     reorder so everything stays on the one scrolling line. */
  #table-tools.brush-tool .tool-break { display: none; }
  #table-tools.brush-tool .pen-knob { order: 0; }

  /* --- Real <table> pages (admin/basebins, admin/security, live, watch,
     autotune): let wide tables scroll horizontally instead of clipping off the
     right edge. Scoped to plain data tables so the map .grid (its own overflow
     lives in #grid-wrap) is untouched. --------------------------------------- */
  table:not(.grid) {
    display: block; width: 100%;
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    white-space: nowrap;
  }

  /* Reveal the fullscreen FAB on the phone (the inline script clears [hidden] only
     where the Fullscreen API exists). */
  #fs-fab:not([hidden]) { display: inline-flex; }
  /* On the phone the identity strip fills the dead space to the right of the brand
     (order: brand → email). The email can ellipsize (min-width:0) so it never forces
     a wrap, and its flex:1 makes it eat the gap up to the corner Exit (Tim, 2026-08-29).
     Smaller text here since it is identity, not an action. Exit button keeps its size.
     margin-left:0 resets the desktop auto-margin (which would otherwise swallow the
     free space and leave the flex:1 strip zero-width). */
  /* flex-basis 0 (NOT auto) is the key: with an auto basis the strip's hypothetical
     width (email + Exit) exceeds the space left of the corner and the flex algorithm
     WRAPS it to its own line before it ever gets to shrink — which is what dropped it
     below the brand. Basis 0 makes its hypothetical width 0, so it always stays on the
     brand's row, then grows to fill the dead space up to the corner Exit; the email
     ellipsizes inside it. align-self pins it to the top so it nests in the corner. */
  #identbar { order: 1; flex: 1 1 0; min-width: 0; align-self: flex-start; margin-top: 2px; margin-left: 0; }
  /* Tools no longer lives on row 1 — it moved into the Row-2 verb strip (#actions),
     so there is no longer a #top-nav to force onto its own line here. */
  #identbar .identbar-email { flex: 1 1 auto; text-align: right; min-width: 0;
    font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* Modals already cap at calc(100vw - 32px); tighten their inset on a phone so
     the body has room to breathe. */
  .fw-modal, .cmp-modal, #tl-modal { width: calc(100vw - 20px); max-width: calc(100vw - 20px); }

  /* The My Tunes modal (and its siblings) open from BUTTONS INSIDE the drawer,
     which sits at z-index 60 (scrim 55). At the base z-index 40 the modal would
     paint behind the drawer — the reported "covered by the side screen". The
     in-page script closes the drawer as these open, but lift the overlays above
     the drawer too so there's no flash behind the .22s slide-out. */
  #tl-overlay { z-index: 65; }

  /* --- My Tunes rows: six action buttons cannot share one flex line with the
     name on a phone — the meta text ended up wrapping one character per line
     and History/Delete fell off the right edge. Let the row wrap instead:
     chip pill + name/meta keep the first line, the buttons drop to their own
     full-width line and share it as equal-width tap targets. --------------- */
  .tl-row, .tl-baserow { flex-wrap: wrap; row-gap: 8px; }
  .tl-row .tl-main { flex: 1 1 55%; }
  .tl-baserow > .tl-main, .tl-baserow > .tl-name { flex: 1 1 55%; min-width: 0; }
  .tl-row .tl-actions, .tl-baserow .tl-actions { flex: 1 1 100%; flex-wrap: wrap; }
  .tl-actions .btn { flex: 1 1 auto; min-height: 34px; }

  /* The details popup's footer carries all six tune actions (Load…Delete). .fw-foot
     does not wrap by default (its other users have 1-2 buttons), so on a phone the
     six would overflow the right edge — let them wrap and share the width like the
     list rows. #tl-detail-overlay is z-index 70 already, above the drawer. */
  #tl-detail-modal { width: calc(100vw - 20px); max-width: calc(100vw - 20px); }
  #tl-detail-modal .fw-foot { flex-wrap: wrap; justify-content: stretch; }
  #tl-detail-modal .fw-foot .btn { flex: 1 1 auto; min-height: 34px; }
}

/* Extra-tight phone width: let the tune-verb row scroll as one strip rather than
   wrapping into four stacked rows that push the map below the fold. */
@media (max-width: 480px) {
  /* TWO stacked rows of verb buttons that scroll horizontally as one strip (Tim,
     2026-08-29). A column-flow grid packs the buttons into 2 rows, column by column
     — (Undo/Redo)(Save/Auto-save)(Compare/Multi-view)(Flash/Live)… — and the whole
     block scrolls right when it overruns. Starts at the LEFT edge (justify start) so
     nothing overflows off the left where it can't be reached. */
  #actions {
    display: grid; grid-auto-flow: column;
    grid-template-rows: auto auto; grid-auto-columns: max-content;
    gap: 6px; justify-content: start; align-content: start;
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    /* Clearance so the scroll slider sits BELOW the buttons — it was hard against
       Auto-save, so a drag on the slider kept hitting the button. */
    padding-bottom: 14px;
  }
  #actions .btn, #actions .flash-combo { flex: none; width: auto; }
  #actions::-webkit-scrollbar { height: 10px; }
  #table-head h2 { font-size: 15px; }
}
/* Ownership admission uses explicit Open/Abort and Continue/Abort actions. */
.wm-dialog { max-width: min(32rem, calc(100vw - 3rem)); padding: 1.5rem; border: 1px solid #55616f; border-radius: .6rem; background: #19212b; color: #eef2f6; }
.wm-dialog::backdrop { background: #000a; }
.wm-dialog h2 { margin: 0 0 1rem; font-size: 1.25rem; }
.wm-dialog p { line-height: 1.55; }
.wm-dialog-actions { display: flex; justify-content: flex-end; gap: .75rem; margin-top: 1.5rem; }
