/* theme.css retro-OS homage theme system webamp desktop.
 Palettes CSS custom properties set on <html>; rules consume them.
 NOTE: one-liner rules intentionally use "{ ... }" (kept on one line).
 Keep braces balanced; the chat window depends on #chat-chooser { position:fixed }.
*/
:root {
 --bg: #0a0a0a; --panel: #0a0a0a; --panel2: #141414; --fg: #b8f0d0;
 --accent: #00e0a0; --accent-fg: #0a0a0a; --titlebar: #141414; --titlebar-fg: #b8f0d0;
 --border: #3a3a3a; --bw: 1px; --bs: solid; --radius: 0px;
 --font: 11px/1 monospace; --shadow: 3px 3px 0 rgba(0, 0, 0, 0.55);
 --bevel-up: #2a2a2a; --bevel-down: #0a0a0a;
}
html, body { margin: 0; height: 100%; background: var(--bg); }
body { color: var(--fg); font: var(--font); }
#app { height: 100%; position: relative; }

/* desktop icon strip (left) */
#toggles { position: fixed; left: 8px; top: 8px; z-index: 2; display: flex; flex-direction: column; gap: 10px; }
#toggles button { color: var(--fg); border: var(--bw) var(--bs) var(--border); font: var(--font); border-radius: var(--radius); }
#toggles button.dicon {
 display: flex; flex-direction: column; align-items: center; gap: 3px;
 width: 66px; padding: 7px 4px 5px; cursor: pointer;
 background: transparent; border: 1px solid transparent;
 border-radius: var(--radius); box-shadow: none;
}
#toggles button.dicon .dicn { font-size: 22px; line-height: 1; color: #00e0a0; text-shadow: 2px 2px 0 rgba(0,0,0,0.6); }
#toggles button.dicon .dlbl { font: var(--font); color: #fff; text-shadow: 1px 1px 0 #000; background: rgba(0,0,0,0.35); padding: 1px 3px; }
#toggles button.dicon:hover { border-color: var(--accent); background: rgba(255,255,255,0.06); }
#toggles button.dicon:hover .dlbl { background: var(--accent); color: var(--accent-fg); text-shadow: none; }
#toggles button.dicon:active { box-shadow: inset 2px 2px 0 rgba(0,0,0,0.4); }

/* taskbar */
#taskbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 5;
  display: flex; align-items: center; gap: 6px; padding: 3px 8px;
 background: var(--titlebar); color: var(--titlebar-fg);
 border-top: var(--bw) var(--bs) var(--border);
 font: var(--font); box-shadow: 0 -2px 0 rgba(0,0,0,0.3);
}
#tb-logo { height: 15px; width: auto; display: block; }
/* START button + menu (AUTO-PLAYLISTS button is its taskbar sibling) */
#tg-start, #tg-playlists {
 background: var(--panel); color: var(--titlebar-fg);
 border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
 font: var(--font); cursor: pointer; padding: 2px 8px;
}
#tg-start.on, #tg-start:hover, #tg-playlists.on, #tg-playlists:hover { background: var(--accent); color: var(--accent-fg); }
#start-menu, #pl-menu {
 position: fixed; left: 8px; bottom: calc(var(--tbh, 28px) + var(--msh, 0px)); z-index: 20; width: 190px;
 background: var(--panel); border: var(--bw) var(--bs) var(--border);
 box-shadow: var(--shadow); border-radius: var(--radius); padding: 3px;
 display: none;
}
.start-item {
 display: flex; align-items: center; gap: 6px; width: 100%;
 background: var(--panel2); color: var(--fg); font: var(--font);
 border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
 padding: 4px 8px; cursor: pointer; text-align: left;
}
/* the station currently on air (playlists.js marks it) */
.start-item.current { background: var(--accent); color: var(--accent-fg); }
.start-item:hover { background: var(--accent); color: var(--accent-fg); }
.start-item .start-icn { width: 14px; text-align: center; }
.start-sep { height: 0; border-top: var(--bw) var(--bs) var(--border); margin: 3px 2px; }
/* running-app taskbar buttons */
#taskbtns { display: flex; gap: 4px; flex: 0 1 auto; overflow: hidden; }
.taskbtn {
 background: var(--panel2); color: var(--fg); font: var(--font);
 border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
 cursor: pointer; padding: 2px 6px; white-space: nowrap;
 display: flex; align-items: center; gap: 3px;
}
.taskbtn.focused { background: var(--panel); box-shadow: inset 1px 1px 0 var(--bevel-up); color: var(--titlebar-fg); }
.taskbtn.minimized { opacity: 0.55; font-style: italic; }
.taskbtn:hover, .wall-swatch:hover { border-color: var(--accent); }
.tbicn { font-size: 10px; }
#tg-reset {
 background: transparent; color: var(--titlebar-fg);
 border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
 font: var(--font); cursor: pointer; padding: 0 6px;
}
#tg-reset:hover { background: var(--accent); color: var(--accent-fg); }
#tbsp { flex: 1 1 auto; }
#taskbar #desktheme, #taskbar #deskclock { margin-left: 0; background: transparent; border: none; padding: 0; color: var(--titlebar-fg); }
#desktheme { color: var(--fg); font: var(--font); padding: 0 6px; margin-left: 4px; border: var(--bw) var(--bs) var(--border); background: var(--panel2); border-radius: var(--radius); }
#deskclock { color: var(--fg); font: var(--font); padding: 0 8px; }

/* chooser windows (wallpaper, themes, skins, chat) */
#wall-chooser, #theme-chooser, #chat-chooser, #skin-chooser, #nmtv-chooser, #readme-window, #stations-chooser {
 background: var(--panel); border: var(--bw) var(--bs) var(--border);
 box-shadow: var(--shadow); border-radius: var(--radius);
}
#wall-titlebar, #theme-titlebar, #chat-titlebar, #skin-titlebar, #nmtv-titlebar, #readme-titlebar, #stations-titlebar {
 background: var(--titlebar); color: var(--titlebar-fg);
 border-bottom: var(--bw) var(--bs) var(--border);
 box-shadow: inset 1px 1px 0 var(--bevel-up);
 display: flex; align-items: center; justify-content: space-between;
 padding: 3px 4px; cursor: move; user-select: none;
}
#wall-titlebar span, #theme-titlebar span, #chat-titlebar span, #skin-titlebar span, #nmtv-titlebar span, #readme-titlebar span, #stations-titlebar span {
 color: var(--titlebar-fg); font: var(--font); letter-spacing: 1px;
}
#wall-close, #theme-close, #chat-close, #skin-close, #nmtv-close, #readme-close, #stations-close {
 background: var(--panel); color: var(--fg);
 border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
 box-shadow: 2px 2px 0 var(--bevel-down);
 font: 9px/1 monospace; width: 14px; height: 14px; cursor: pointer; padding: 0;
}
#wall-close:hover, #theme-close:hover, #chat-close:hover, #skin-close:hover, #nmtv-close:hover, #readme-close:hover, #stations-close:hover {
 background: var(--accent); color: var(--accent-fg);
}
#wall-foot, #theme-foot, #chat-foot, #nmtv-foot, #stations-foot { color: var(--fg); font: var(--font); }

/* live radio chooser: roster grid, station buttons, LIVE badge (badge rules
   below are shared with #radio-badge). Failure is shown, not hidden: a
   station that never starts is dimmed and struck through, with the reason in
   its tooltip. */
#stations-chooser { position: fixed; z-index: 12; display: none; width: 300px; }
#stations-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3px; padding: 6px; max-height: 56vh; overflow-y: auto; }
.station-btn {
 display: block; cursor: pointer; padding: 3px 5px; text-align: left; width: 100%;
 background: var(--panel2); color: var(--fg); font: var(--font);
 border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
}
.station-btn:hover { outline: 2px solid var(--accent); }
.station-btn .st-name { display: block; font: bold 10px/1.3 var(--font); letter-spacing: 1px; }
.station-btn .st-mood { display: block; font: 9px/1.3 var(--font); opacity: 0.75; }
.station-btn.current { background: var(--accent); color: var(--accent-fg); }
.station-btn.current .st-mood { opacity: 0.85; }
.station-btn.trying { opacity: 0.7; }
.station-btn.failed { opacity: 0.45; text-decoration: line-through; }
#stations-foot { padding: 4px 6px 6px; }

/* wallpaper chooser */
#wall-chooser { position: fixed; z-index: 10; display: none; width: 240px; }
/* wallpaper swatch grid (titlebar, close and frame come from the chooser
   rules above; these three rules used to live in index.html with hardcoded
   colours, which is why the wallpaper chooser ignored the theme) */
#wall-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; padding: 6px; }
.wall-swatch { width: 100%; height: 48px; border: var(--bw) var(--bs) var(--border); cursor: pointer; background-color: #000; background-repeat: no-repeat; padding: 0; }
#wall-foot { padding: 0 6px 6px; }

/* themes chooser */
#theme-chooser { position: fixed; z-index: 11; display: none; width: 230px; }
#theme-grid { display: grid; grid-template-columns: 1fr; gap: 3px; padding: 6px; }
.theme-swatch {
 display: flex; align-items: center; gap: 6px; cursor: pointer;
 padding: 4px 6px; background: var(--panel2); color: var(--fg);
 font: var(--font); text-align: left; width: 100%;
 border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
}
.theme-swatch:hover { outline: 2px solid var(--accent); }
.theme-chip { width: 14px; height: 14px; border: 1px solid var(--border); flex: 0 0 auto; }
#theme-foot { padding: 0 6px 6px; }

/* skins chooser: deal + gallery modes */
#skin-chooser { position: fixed; z-index: 11; display: none; width: 340px; }
#skin-now { color: var(--fg); font: var(--font); padding: 4px 6px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#skin-grid { display: grid; grid-template-columns: 1fr; gap: 3px; padding: 6px; }
.skin-btn {
 display: block; cursor: pointer; padding: 4px 6px; text-align: left;
 background: var(--panel2); color: var(--fg); font: var(--font); width: 100%;
 border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.skin-btn:hover { outline: 2px solid var(--accent); }
#skin-foot { display: flex; align-items: center; gap: 4px; padding: 0 6px 6px; color: var(--fg); font: var(--font); }
#skin-foot button {
 background: var(--panel2); color: var(--fg); font: var(--font); cursor: pointer;
 border: var(--bw) var(--bs) var(--border); border-radius: var(--radius); padding: 2px 6px;
}
#skin-foot button:hover { background: var(--accent); color: var(--accent-fg); }
#skin-count { margin-left: auto; color: #777; }
/* gallery mode thumb wall */
#skin-tabs { display: flex; gap: 4px; padding: 4px 6px 0; }
#skin-tabs button {
 background: var(--panel2); color: var(--fg); font: var(--font); cursor: pointer;
 border: var(--bw) var(--bs) var(--border); border-radius: var(--radius); padding: 2px 8px;
}
#skin-tabs button.on { background: var(--accent); color: var(--accent-fg); }
#skin-searchwrap { padding: 4px 6px; }
#skin-search {
 width: 100%; box-sizing: border-box; background: #0a0a0a; color: var(--fg);
 border: var(--bw) var(--bs) var(--border); font: var(--font); padding: 4px 6px;
}
#skin-grid.gal { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; padding: 6px; overflow-y: auto; max-height: 60vh; }
.skin-thumb {
 display: flex; flex-direction: column; gap: 2px; cursor: pointer;
 padding: 3px; background: var(--panel2); color: var(--fg);
 border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
}
.skin-thumb:hover { outline: 2px solid var(--accent); }
.skin-thumb img { width: 100%; height: 58px; object-fit: contain; background: #000; image-rendering: pixelated; display: block; }
.skin-thumb span { font: var(--font); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: left; }

/* chat window (shoutbox iframe) */
#chat-chooser { position: fixed; z-index: 10; display: none; width: 322px; }
#chat-frame { display: block; width: 320px; height: 380px; border: none; border-top: var(--bw) var(--bs) var(--border); background: #1a1a1a; }
#chat-foot { padding: 0 6px 6px; }

/* nmtv window (nmtv.online iframe) */
#nmtv-chooser { position: fixed; z-index: 10; display: none; width: 522px; max-width: 92vw; }
#nmtv-frame { display: block; width: 100%; height: 420px; border: none; border-top: var(--bw) var(--bs) var(--border); background: #000; }
#nmtv-tbbtns { display: flex; align-items: center; gap: 4px; }
#nmtv-external { font: 8px/1 monospace; padding: 2px 5px; cursor: pointer;
 background: var(--panel); color: var(--fg); border: var(--bw) var(--bs) var(--border); border-radius: var(--radius); box-shadow: 2px 2px 0 var(--bevel-down); }
#nmtv-external:hover { background: var(--accent); color: var(--accent-fg); }

/* library window pinned classic colors */
#app .gen-window.window { display: flex; flex-direction: column; }
#app .gen-window.window .gen-middle { flex: 1 1 auto; overflow: hidden; min-height: 0; }
#app .gen-window.window .gen-middle-center, #app .gen-window.window .gen-middle-center div { width: 100% !important; }
#app .gen-window.window .gen-middle-center { overflow-y: auto; overflow-x: hidden; max-height: 100% !important; }
#app .gen-window.window .gen-middle-center input { width: 100% !important; box-sizing: border-box; }

/* mobile touch */
* { -webkit-tap-highlight-color: transparent; }
.window .draggable { touch-action: none; }
@media (max-width: 480px) {
 #app .gen-window.window { max-width: calc(100vw - 8px); }
 #app .gen-window.window .gen-middle { max-width: 100%; }
 #taskbar button { min-height: 26px; }
}

/* M1 (mobile audit 2026-09-19): a taskbar that fits small screens.
   #taskbtns is a flex item with overflow:hidden, so its automatic minimum
   size is 0: under width pressure it silently shrank to nothing and took all
   five window buttons with it, while the sound toggle and clock slid off the
   right edge — the page never scrolls, so they were unreachable. The bar is
   now one strip that scrolls horizontally whenever its content does not fit:
   every control keeps its natural width and the clock stays pinned. The
   scroll/flex rules are deliberately universal (M1.1, future-proofing audit
   of a5bb7a4): a breakpoint would let the same collapse return the day the
   content outgrows it, and with nothing to scroll they are a no-op on wide
   screens. */
#taskbar {
 overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain;
 scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
#taskbar::-webkit-scrollbar { display: none; }
#taskbar > * { flex-shrink: 0; }        /* natural widths -> the strip overflows and scrolls */
#taskbar #deskclock {
 position: sticky; right: 0; padding: 0 6px;
 background: inherit;                    /* follow the bar's own surface (gradient included), never a flat colour */
}
@media (max-width: 1024px) {
 /* content slides under the pinned clock: a soft edge reads as "there is more" */
 #taskbar #deskclock { box-shadow: -6px 0 6px -6px rgba(0,0,0,0.35); }
}
@media (max-width: 560px) {
 #tb-logo { display: none; }             /* decorative, and the first thing to go on a phone */
 #taskbar { gap: 5px; padding: 3px 6px; }
}

/* M2 (mobile audit 2026-09-19): touch ergonomics.
   - manipulation = pan + pinch minus double-tap zoom, so a quick double tap on
     a control stops zooming the page (pinch-zoom stays for accessibility).
     The winamp drag handles keep their own touch-action: none.
   - a long press on the chrome must not start a text selection / iOS callout
     halfway through a tap.
   - iOS Safari zooms the whole page when an input under 16px takes focus and
     the library search is 11px: coarse pointers get 16px text inputs.
   - the skin's 9x9 window controls get the hit area the chrome actually has
     room for (neighbours sit 1-2px apart, the outer buttons 2px from the
     window edge): pads grow them to ~11x14 / 9x14 without overlapping a
     neighbour or stealing more than a sliver of drag space. */
#taskbar, #taskbar *, #toggles, #toggles *, #start-menu, #start-menu *, #pl-menu, #pl-menu * { touch-action: manipulation; }
/* the winamp chrome inside #app: the transport row, the status bar and the
   clutter buttons are where double taps land most; the drag handles and their
   contents keep touch-action: none (a drag must never be read as a gesture) */
#app .actions, #app .actions *, #app .webamp-status, #app .webamp-status *,
#app #clutter-bar, #app #clutter-bar *,
#app #close, #app #shade, #app #minimize, #app #play-pause,
#app #playlist-close-button, #app #playlist-shade-button, #app .gen-close { touch-action: manipulation; }
#taskbar, #toggles, #start-menu, #pl-menu, #app .title-bar,
#app .playlist-top, #app .gen-top, #app .gen-bottom {
 -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}
@media (pointer: coarse) {
 #app input:not([type="range"]),
 #app .gen-window.window .gen-middle-center input { font-size: 16px; }
 #app #minimize::before, #app #shade::before, #app #close::before,
 #app #playlist-shade-button::before, #app #playlist-close-button::before,
 #app .gen-close::before { content: ""; position: absolute; inset: -3px 0 -2px 0; }
 #app #close::before, #app #playlist-close-button::before, #app .gen-close::before { inset: -3px -2px -2px 0; }
 #app #play-pause::before { content: ""; position: absolute; inset: -4px; }
}

/* boot splash (T0-2) */
#boot-splash {
 position: fixed; inset: 0; z-index: 100; background: #000; cursor: pointer;
 display: flex; align-items: center; justify-content: center;
 transition: opacity 0.45s ease;
}
#boot-box { width: 420px; max-width: 90vw; font: 12px/1.7 monospace; color: #00e08c; }
#boot-logo {
 font: bold 28px/1 monospace; color: #b8f0d0; letter-spacing: 4px;
 margin-bottom: 14px; text-shadow: 3px 3px 0 rgba(0,224,160,0.25);
}
.boot-line { opacity: 0; animation: bootline 0.25s ease forwards; }
@keyframes bootline { to { opacity: 1; } }
.boot-ok { color: #9fe8c8; margin-right: 6px; }
#boot-bar { height: 14px; border: 1px solid #3a3a3a; margin: 14px 0 4px; background: #0a0a0a; }
#boot-fill { height: 100%; width: 0; background: #00e0a0; transition: width 0.06s linear; }
#boot-pct { float: right; color: #9fe8c8; }
#boot-hint { clear: both; color: #555; font-size: 10px; margin-top: 8px; text-align: center; }

/* desktop right-click menu (T1-2) — same items as START */
#ctx-menu {
 position: fixed; z-index: 30; width: 190px; display: none;
 background: var(--panel); border: var(--bw) var(--bs) var(--border);
 box-shadow: var(--shadow); border-radius: var(--radius); padding: 3px;
}
#ctx-menu .start-item { background: var(--panel2); color: var(--fg); }
#ctx-menu .start-item:hover { background: var(--accent); color: var(--accent-fg); }

/* focused window titlebar brightening (T1-4)
   (#main-window.focused-win .window-shade was dropped: the bundle only ever
   emits the id "playlist-window-shade", never a .window-shade class, so that
   selector clause could not match) */
#main-window.focused-win #title-bar {
 filter: brightness(1.25);
}
#playlist-window.focused-win .playlist-top,
#equalizer-window.focused-win,
.gen-window.window.focused-win { outline: 1px solid var(--accent); outline-offset: -1px; }

/* readme window */
#readme-window { position: fixed; z-index: 10; display: none; width: 440px; max-width: 92vw; }
#readme-titlebar { width: 100%; display: flex; justify-content: space-between; align-items: center; padding: 4px 6px; box-sizing: border-box; cursor: move; }
#readme-close { background: none; color: inherit; border: none; font: bold 11px/1 monospace; cursor: pointer; padding: 0 2px; }
#readme-close:hover { color: var(--accent); }
#readme-body { margin: 0; padding: 8px 10px; font: 11px/1.5 monospace; color: var(--fg); white-space: pre-wrap; max-height: 60vh; overflow-y: auto; }
#readme-foot { color: #777; font: 10px/1 monospace; padding: 4px 8px; border-top: var(--bw) var(--bs) var(--border); }

/* radio taskbar badge (T0-1) */
#radio-badge, #live-badge {
 height: 24px; padding: 0 10px; border: var(--bw) var(--bs) var(--border);
 background: var(--accent); color: var(--accent-fg);
 font: bold 10px/24px var(--font); border-radius: var(--radius);
 cursor: pointer; animation: radio-pulse 2s ease-in-out infinite;
}
#radio-badge:hover, #live-badge:hover { filter: brightness(1.2); }
#radio-note {
 height: 24px; padding: 0 10px; border: var(--bw) var(--bs) var(--border);
 background: var(--panel2); color: var(--fg);
 font: 10px/24px var(--font); border-radius: var(--radius);
}
@keyframes radio-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.65; } }


/* swipe (2026-09-21): touch scrolling for the playlist — see swipe.js.
   Classic Winamp scrolls the playlist with an 8px-wide rail and two 8x5px
   arrow buttons: mouse-sized, thumb-hostile. On coarse pointers the rail and
   the arrows get fat hit zones (the skin's pixels are untouched) and the
   track list becomes a drag surface: swipe.js turns a vertical drag into
   SET_PLAYLIST_SCROLL_POSITION, with momentum and a position chip. */
.pl-swipe-chip {
  position: absolute; right: 26px; top: 30px; z-index: 6;
  max-width: 210px; padding: 3px 8px; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; pointer-events: none;
  background: var(--panel, #111); color: var(--accent, #00e08c);
  border: var(--bw, 1px) var(--bs, solid) var(--border, #444);
  border-radius: 3px; font: 10px/1.4 var(--font, monospace);
  opacity: 0; transition: opacity .12s linear;
}
.pl-swipe-chip.on { opacity: 1; }

@media (pointer: coarse) {
  /* the row list is a touch surface: no native panning/zoom there, the
     module owns the gesture (a drag scrolls the playlist instead) */
  #app #playlist-window .playlist-tracks { touch-action: none; }

  /* fat rail: the 8px rail gets a ~26px hit zone, centred on it */
  #app #playlist-window .playlist-scrollbar { position: relative; touch-action: none; }
  #app #playlist-window .playlist-scrollbar::before {
    content: ""; position: absolute; top: 0; bottom: 0; left: -9px; right: -9px;
  }
  /* and a slim hint that this rail is grabbable (skins draw only the 8px bar) */
  #app #playlist-window .playlist-scrollbar::after {
    content: ""; position: absolute; top: 4px; bottom: 4px; left: 50%;
    width: 2px; margin-left: -1px; background: rgba(255, 255, 255, .10);
    pointer-events: none;
  }

  /* the arrow buttons keep their 8x5px glyphs; their expanded tap zones are
     transparent divs placed by swipe.js (.pl-tapzone), because the strip is
     crowded (rail above, resize handle below, menu buttons to the left) and a
     CSS-expanded hit area would steal from its neighbours */
}
.pl-tapzone {
  position: absolute; z-index: 5; background: transparent;
  touch-action: none; -webkit-tap-highlight-color: transparent;
}

/* M4 (mobile audit 2026-09-19): the icon strip runs taller than a phone
   screen — it covers the playlist's left edge on a phone, and the last icons
   fall off a landscape viewport (measured 2026-09-21: 5/8 reachable at
   844x390). On phone-like viewports the strip becomes a drawer behind the
   taskbar's ICONS button: hidden until asked for, internally scrollable so
   all nine stay reachable when the viewport is short, and always one tap
   from closing. */
#tg-drawer { display: none; }
@media (max-width: 560px), (pointer: coarse) and (max-height: 520px) {
  #tg-drawer { display: inline-block; }
  #toggles { display: none; }
  body.drawer-open #toggles {
    display: flex; position: fixed; left: 0; top: 0; bottom: calc(var(--tbh, 44px) + var(--msh, 0px)); z-index: 40;
    width: 88px; padding: 8px 6px; gap: 8px;
    /* safe-area (2026-09-26, audit §3c): the drawer lives on the LEFT edge,
       which is where the notch is in landscape. max() so the panel still
       clears the notch on the engines that report the inset correctly, and
       keeps its 6px gutter where they report 0 (iOS 26.1, WebKit 301994). */
    padding-left: max(6px, env(safe-area-inset-left, 0px));
    background: var(--panel); border-right: var(--bw) var(--bs) var(--border);
    overflow-y: auto; -webkit-overflow-scrolling: touch;
  }
  body.drawer-open::after {
    content: ""; position: fixed; inset: 0 0 calc(var(--tbh, 44px) + var(--msh, 0px)) 88px; z-index: 39;
    background: rgba(0, 0, 0, .35);
  }
}
/* a short viewport (a landscape phone) cannot stack nine icons in one
   column: three columns of three, cells the same width the two-column grid
   used, so every target keeps its size */
@media (pointer: coarse) and (max-height: 520px) {
  body.drawer-open #toggles {
    display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: minmax(44px, auto);
    width: 264px; gap: 6px;
  }
  body.drawer-open::after { inset: 0 0 calc(var(--tbh, 44px) + var(--msh, 0px)) 264px; }
}

/* M6 (touch rows, 2026-09-21): phone-like viewports run the playlist at 2x —
   26px rows and 13px type instead of the skin's mouse-sized 13px rows with
   9px type, so a thumb can read, hit and fling the list. `pl-touch` is set
   from index.html by the SAME gate as the phone layout (isPhone(): narrow,
   or a coarse pointer with no vertical room) and it is JS-driven rather than
   a media query on purpose — the gate cannot be expressed as one CSS query
   without drifting from isPhone(), and the bundle's row math plus swipe.js
   both read window.__plRow, so CSS and JS must flip together (see M6 in
   docs/improvements-roadmap.md). Titles and durations are both .track-cell
   rows. */
.pl-touch #webamp .playlist-tracks .track-cell {
 height: 26px; line-height: 26px; font-size: 13px;
}


/* M7 (2026-09-22) phone taskbar + menu chrome — portrait touch revamp.
   Gate: html.tb-touch set by index.html's isPhone() (width < 480 && coarse),
   same one-gate discipline as html.pl-touch (M6: JS gate, CSS follows the
   class). --tbh mirrors the taskbar's live (measured) height, taken from
   getBoundingClientRect on boot + resize, so the menus, the drawer panel
   and its scrim anchor to the real bar instead of hard-coded 28px/44px.
   Menu rows scale on coarse pointers (landscape phones included); no JS
   reads these sizes back. */
html { --tbh: 28px; }
html.tb-touch { --tbh: 92px; } /* fallback if the JS measure never runs */
html.tb-touch #taskbar {
  flex-wrap: wrap;             /* row 1 nav, row 2 window chips + clock */
  align-content: flex-start;
  min-height: 44px;
  padding: 3px 6px calc(3px + env(safe-area-inset-bottom, 0px));
  gap: 4px 6px;
}
html.tb-touch #taskbar > button {
  height: 44px;
  min-height: 44px;
  padding: 0 12px;
  font-size: 13px;
}
html.tb-touch #taskbar .tbicn { font-size: 14px; }
html.tb-touch #tg-drawer { width: 44px; padding: 0; font-size: 17px; }
/* narrow phones: the nav row must keep all four buttons on one line */
@media (max-width: 360px) {
  html.tb-touch #taskbar > button { padding: 0 8px; letter-spacing: -0.3px; }
}
html.tb-touch #taskbtns {
  flex: 1 1 auto;             /* row 2: chips strip, clock + mute tuck into the tail */
  order: 3;
  min-height: 38px;
  min-width: 0;                /* beat the M1 universal flex-shrink:0 so the
                                  strip can shrink and scroll within its row */
  overflow-x: auto;            /* 5 chips at full labels overflow 390px — scroll */
  scrollbar-width: none;
  max-width: calc(100% - 104px); /* never push the mute toggle or the clock
                                  off row 2 (sound 40 + clock 48 + 3 gaps 12
                                  = 100px reserved + 4px slack) */
}
html.tb-touch #taskbtns::-webkit-scrollbar { display: none; }
html.tb-touch .taskbtn {
  height: 38px; min-height: 38px; min-width: 40px;
  font: 12px/1 monospace;      /* the bar's 11px var is mouse-sized; 12 mono reads */
  gap: 4px;
}
html.tb-touch .taskbtn .tb-lbl { padding-right: 4px; }
html.tb-touch #deskclock {
  order: 4;
  min-height: 38px;
  font: 12px/1 monospace;    /* keep digits tabular and row-consistent */
  display: flex; align-items: center;  /* center the time in its 38px slot */
  margin-left: auto;         /* pinned to the row end, chips keep the strip */
  padding: 0 calc(8px + env(safe-area-inset-right, 0px)) 0 8px;
}
html.tb-touch #tg-reset, html.tb-touch #desktheme { display: none; }
html.tb-touch #tbsp { display: none; }   /* spacer steals row-1 slack: no */

/* the sounds.js mute toggle: desktop keeps its inline-era look via the class */
#tg-sound.tb-sound {
  background: transparent; color: var(--titlebar-fg);
  border: var(--bw) var(--bs) var(--border);
  font: 10px/1 monospace; cursor: pointer;
  padding: 2px 6px; border-radius: var(--radius);
}
/* phone: the mute toggle joins the chip row, icon-only; the chips strip
   scrolls within the space that remains */
html.tb-touch #tg-sound {
  order: 2; height: 38px; min-height: 38px; min-width: 40px; padding: 0 10px;
  display: flex; align-items: center; justify-content: center; /* glyph dead-center */
}
html.tb-touch #tg-sound .sndlbl { display: none; }
html.tb-touch #tg-sound .sndicn { font-size: 14px; }

/* a landscape phone (short viewport) cannot afford two rows: the bar stays
   ONE row and the chips strip scrolls, M1-style.

   Tap floors (2026-09-26, audit §3e): this block used to compress the bar to
   40px and the chips to 36px, which is under the 44px floor the portrait bar
   meets everywhere. A landscape phone has the same thumbs as a portrait one,
   so keep 44px — the height it costs is exactly what the landscape sheet
   stopped wasting (P0a). The bar is 44 + padding, so --tbh re-measures and
   everything anchored to it follows. */
@media (pointer: coarse) and (max-height: 520px) {
  html.tb-touch #taskbar { flex-wrap: nowrap; min-height: 44px; }
  html.tb-touch #taskbar > button { height: 44px; min-height: 44px; padding: 0 10px; }
  html.tb-touch #taskbtns { flex: 0 1 auto; order: 0; overflow-x: auto; }
  html.tb-touch .taskbtn { height: 44px; min-height: 44px; }
  html.tb-touch #deskclock { order: 0; min-height: 44px; }
  html.tb-touch #tg-sound { height: 44px; min-height: 44px; }
}

/* menus sit flush above the (taller) taskbar; big touch rows on any coarse
   pointer (portrait via tb-touch, landscape via the coarse media) */
html.tb-touch #start-menu, html.tb-touch #pl-menu, html.tb-touch #ctx-menu {
  width: min(320px, calc(100vw - 16px));
  max-height: calc(100vh - var(--tbh) - var(--msh, 0px) - 12px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
@media (pointer: coarse) {
  .start-item { min-height: 40px; font-size: 13px; padding: 0 12px; }
  .start-icn { width: 18px; font-size: 15px; }
  .start-sep { margin: 4px 2px; }
}


/* T0 (2026-09-22) NeXTSTEP homage: tab titlebars for the generic windows.
   Gate: html.theme-next, off by default — the classic Winamp chrome stays the
   reference look. One CSS block, no fork changes, no markup change: win.js
   already renders <div id="*-titlebar"><span>TITLE</span><button>x</button>.
   The NeXT look: a raised tab sitting on a bevelled body, the title centred in
   the tab, a squarish close widget on the right, and the 1px light/dark bevel
   pair that reads as moulded plastic. */
html.theme-next #wall-titlebar,
html.theme-next #theme-titlebar,
html.theme-next #skin-titlebar,
html.theme-next #chat-titlebar,
html.theme-next #nmtv-titlebar,
html.theme-next #readme-titlebar,
html.theme-next #stations-titlebar {
  display: flex; align-items: center; gap: 8px;
  box-sizing: border-box;           /* all six compute the same 30px */
  height: 30px; min-height: 30px;
  padding: 3px 4px 3px 10px;
  background: linear-gradient(180deg, var(--panel) 0%, var(--panel2) 100%);
  border: 1px solid var(--border);
  border-bottom: 0;
  font: 11px/1.2 var(--font);
  letter-spacing: 0.04em;
  cursor: move;              /* the whole tab is the drag handle */
}
html.theme-next #wall-titlebar > span,
html.theme-next #theme-titlebar > span,
html.theme-next #skin-titlebar > span,
html.theme-next #chat-titlebar > span,
html.theme-next #nmtv-titlebar > span,
html.theme-next #readme-titlebar > span,
html.theme-next #stations-titlebar > span {
  flex: 1 1 auto;
  text-align: center;         /* NeXT centred the title in the tab */
  color: var(--fg);
  padding: 1px 8px 2px;
  background: linear-gradient(180deg, var(--panel2), var(--panel));
  border: 1px solid var(--border);
  border-top-color: var(--bevel-up);
  border-left-color: var(--bevel-up);
  border-bottom-color: var(--border);
  border-right-color: var(--border);
  box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.06);
}
html.theme-next #wall-titlebar > button,
html.theme-next #theme-titlebar > button,
html.theme-next #skin-titlebar > button,
html.theme-next #chat-titlebar > button,
html.theme-next #nmtv-titlebar > button,
html.theme-next #readme-titlebar > button,
html.theme-next #stations-titlebar > button {
  appearance: none;
  flex: 0 0 auto;
  width: 18px; height: 18px;
  padding: 0; margin: 0;
  font: 12px/16px var(--font);
  color: var(--fg);
  background: linear-gradient(135deg, var(--panel2), var(--panel));
  border: 1px solid var(--border);
  border-top-color: var(--bevel-up);
  border-left-color: var(--bevel-up);
  cursor: pointer;
}
html.theme-next #wall-titlebar > button:active,
html.theme-next #theme-titlebar > button:active,
html.theme-next #skin-titlebar > button:active,
html.theme-next #chat-titlebar > button:active,
html.theme-next #nmtv-titlebar > button:active,
html.theme-next #readme-titlebar > button:active,
html.theme-next #stations-titlebar > button:active {
  border-top-color: var(--border);
  border-left-color: var(--border);
  border-bottom-color: var(--bevel-up);
  border-right-color: var(--bevel-up);
}
/* the body sits under the tab: pull it up so the tab reads as sitting ON it */
html.theme-next #wall-chooser,
html.theme-next #theme-chooser,
html.theme-next #skin-chooser,
html.theme-next #chat-chooser,
html.theme-next #nmtv-chooser,
html.theme-next #readme-window {
  border-top: 0;
}

/* ===== P0 floors (2026-09-24): phone now-playing strip + full-player sheet
   From docs/research/mobile-ui-retro-report-2026-09-24.md §6 P0-1/P0-2 — a
   mini strip docked on the taskbar that expands into a fixed-anatomy full
   player. Skins may restyle these surfaces (same vars as the chrome); the
   slot and geometry are ours. Phone-only: html.tb-touch, the same one-gate
   discipline as pl-touch. --msh is the measured strip height (index.html
   syncTbh); the fallback keeps geometry sane if the JS measure never runs. */
html { --msh: 0px; }
html.tb-touch { --msh: 56px; }

#ministrip { display: none; }
html.tb-touch #ministrip {
  display: flex; align-items: center; gap: 6px;
  position: fixed; left: 0; right: 0; bottom: var(--tbh, 92px); z-index: 18;
  height: var(--msh, 56px); box-sizing: border-box;
  /* safe-area (2026-09-26, audit §3c): viewport-fit=cover is on, so in
     landscape the notch clips the strip's left edge. max() keeps the inset
     even where env() reports 0 (iOS 26.1 re-opened bug 301994 pins the inset
     to 0) — the strip already clears --tbh, which carries the bottom inset,
     so only the horizontal pair is applied here. */
  padding: 0 max(6px, env(safe-area-inset-right, 0px)) 0 max(6px, env(safe-area-inset-left, 0px));
  background: var(--titlebar); color: var(--titlebar-fg);
  border-top: var(--bw) var(--bs) var(--border);
  box-shadow: 0 -2px 0 rgba(0, 0, 0, 0.3);
}
/* left region: art + title/artist; one tap opens the sheet */
#ms-open {
  display: flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0;
  height: 100%; padding: 0 4px; background: transparent; border: 0;
  color: inherit; font: var(--font); text-align: left; cursor: pointer;
}
#ms-art {
  width: 40px; height: 40px; flex: 0 0 auto; object-fit: cover;
  background: var(--panel2); border: var(--bw) var(--bs) var(--border);
}
#ms-meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
#ms-title { font-size: 14px; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#ms-artist { font-size: 12px; line-height: 1.2; opacity: 0.8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#ms-play, #ms-next, #ms-viz {
  flex: 0 0 auto; width: 44px; height: 44px;      /* WCAG 2.5.5 / HIG 44px */
  background: var(--panel2); color: var(--fg);
  border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
  font-size: 15px; line-height: 1; cursor: pointer;
}
#ms-prog { position: absolute; left: 0; top: 0; height: 2px; width: 0%; background: var(--accent); }
/* the visualizer glyph (2026-09-26): a fourth 44px target in the strip, so it
   must be IN the selector above, not a rule of its own — added standalone it
   inherited the 21px default button height and failed the 44px floor
   (measured: 21px, caught by PHASE=viz). It costs a 44px slot in a 56px bar,
   so the strip's left region shrinks rather than the bar growing; --msh
   re-measures either way. */
#ms-viz { color: var(--accent); }

/* full-player sheet: expands above strip + taskbar; fixed anatomy */
#npsheet { display: none; }
body.np-sheet #npsheet {
  display: flex; flex-direction: column; align-items: center;
  position: fixed; left: 0; right: 0; top: 0;
  bottom: calc(var(--tbh, 92px) + var(--msh, 56px));
  z-index: 19; box-sizing: border-box;
  /* safe-area (2026-09-26, audit §3c): the sheet is edge-to-edge under
     viewport-fit=cover, so the horizontal insets protect the art and the
     scrubber. The bottom inset is NOT applied — the sheet already stops at
     --tbh - --msh, and --tbh carries the taskbar's bottom inset, so adding
     it here would double-pad. Left inset on the landscape panel instead. */
  padding: 0 max(16px, env(safe-area-inset-right, 0px)) 10px max(16px, env(safe-area-inset-left, 0px));
  background: var(--panel); color: var(--fg);
  border-bottom: var(--bw) var(--bs) var(--border);
  overscroll-behavior: contain; touch-action: none;
  -webkit-user-select: none; user-select: none;
  animation: np-rise 180ms ease-out;
}
@keyframes np-rise { from { transform: translateY(24px); opacity: 0.35; } to { transform: none; opacity: 1; } }
@media (prefers-reduced-motion: reduce) { body.np-sheet #npsheet { animation: none; } }
.nps-grab { width: 100%; display: flex; justify-content: flex-end; }
#nps-close { width: 44px; height: 44px; background: transparent; border: 0; color: var(--fg); font-size: 22px; line-height: 1; cursor: pointer; }
/* The art is the only elastic part of the column: it carries flex-shrink with
   min-height:0 so a short viewport (a landscape phone leaves 390 - 47 - 56 =
   287px of sheet) shrinks the art instead of pushing the transport row out of
   the sheet's own box. Measured 2026-09-26 at 844x390: the column wanted
   388px, the box was 287px, and #nps-play landed at y 440 — 50px below the
   viewport with overflow-y:visible, so no scroll could reach it. See
   docs/mobile-ux-audit-2026-09-26.md §1. */
#nps-art {
  width: min(78vw, 46vh, 320px); aspect-ratio: 1; object-fit: cover;
  flex: 0 1 auto; min-height: 0;      /* the sheet's height budget lives here */
  margin: 2px 0; background: var(--panel2); border: var(--bw) var(--bs) var(--border);
}
.nps-meta { width: 100%; text-align: center; margin: 8px 0 0; }
#nps-title { font-size: 16px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#nps-artist { font-size: 13px; opacity: 0.85; margin-top: 2px; }
.nps-scrub { width: min(92vw, 420px); margin-top: 8px; }
#nps-seek { width: 100%; height: 44px; margin: 0; background: transparent; accent-color: var(--accent); }
.nps-times { display: flex; justify-content: space-between; font-size: 12px; opacity: 0.85; margin-top: -4px; }
.nps-transport { display: flex; align-items: center; justify-content: center; gap: 18px; margin-top: 8px; }
#nps-prev, #nps-next { width: 48px; height: 48px; font-size: 18px; }
#nps-play { width: 68px; height: 68px; font-size: 22px; }
#nps-prev, #nps-next, #nps-play {
  background: var(--panel2); color: var(--fg);
  border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
  line-height: 1; cursor: pointer;
}
/* pressed = sunken bevel, the same trick the classic chrome uses */
#ministrip button:active, #npsheet button:active {
  border-top-color: var(--bevel-down); border-left-color: var(--bevel-down);
  border-bottom-color: var(--bevel-up); border-right-color: var(--bevel-up);
}
/* the sheet's visualizer entry: a full-width row under the transport, so it
   reads as "a mode of this player" and not as a fifth transport button */
.nps-viz {
  margin-top: 10px; min-height: 44px; width: 100%;
  background: var(--panel2); color: var(--accent);
  border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
  font: 12px/1 var(--font); letter-spacing: 1px; cursor: pointer;
}
/* focus is never invisible on these surfaces (WCAG 2.4.7 / 2.4.11) */
#ministrip button:focus-visible, #npsheet button:focus-visible, #npsheet input:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* ===== Landscape sheet: a right-side panel, not a bottom sheet =====
   docs/mobile-ux-audit-2026-09-26.md §1 + the retro dossier's P1-9
   ("landscape sheet -> right-side panel"). A bottom sheet in landscape eats
   the height that makes landscape worth having: at 844x390 the sheet box is
   only 390 - 47 - 56 = 287px, and the portrait column wants 388px, so the
   transport row left the box entirely (measured: #nps-play bottom 440 in a
   390px viewport, elementFromPoint at its centre null, no scroll path).

   Gate: a SHORT viewport, not "coarse pointer" — the sheet is phone-only
   (html.tb-touch, set by index.html's isPhone()), and a short window can be
   reached with a mouse too (a desktop window narrowed under 480px wide).
   560px keeps every portrait phone in the column layout: the shortest one we
   support is 320x568, and 375x667 / 390x844 are well clear. Those portrait
   sheets keep the elastic-art rule above as their safety net.

   Geometry: a panel pinned to the right edge, full height above the strip.
   Column 1 is the art (a square sized off BOTH axes so it can never crowd
   out the controls); column 2 stacks close / title / scrub / transport, with
   the scrub row as the 1fr so the transport pins to the bottom edge. */
@media (max-height: 560px) {
  body.np-sheet #npsheet {
    left: auto; right: 0; top: 0;
    bottom: calc(var(--tbh, 92px) + var(--msh, 56px));
    width: min(72vw, 460px);
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 12px;
    padding: 0 12px 10px;
    border-bottom: 0;
    border-left: var(--bw) var(--bs) var(--border);
    animation-name: np-slide;   /* entering from the side it lives on */
  }
  @keyframes np-slide { from { transform: translateX(28px); opacity: 0.35; } to { transform: none; opacity: 1; } }
  .nps-grab { grid-column: 2; grid-row: 1; width: auto; align-self: start; }
  #nps-art {
    grid-column: 1; grid-row: 1 / 5; align-self: center;
    width: min(30vw, 34vh); height: auto; min-height: 0; margin: 0;
  }
  .nps-meta { grid-column: 2; grid-row: 2; text-align: left; margin: 0; align-self: end; }
  .nps-scrub { grid-column: 2; grid-row: 3; width: 100%; margin-top: 4px; align-self: start; }
  .nps-transport {
    grid-column: 2; grid-row: 4; margin-top: 0; align-self: end;
    flex-wrap: wrap;              /* a narrow panel wraps instead of clipping */
    gap: 12px 14px;
  }
}

/* ===== MILKDROP fullscreen surface (2026-09-26, visualizer.js) ==========
   Spec: docs/research/mobile-ui-retro-report-2-2026-09-24.md §1.1 + the
   retro dossier's P0-3/P0-4. Three rules drive everything below:

   1. It is a CSS full-viewport surface, not an element-fullscreen one.
      iOS has no element fullscreen for non-video content, so on an iPhone
      this IS the mechanism; the fork still calls requestFullscreen() behind
      document.fullscreenEnabled, which makes it a true fullscreen on
      Android/iPad. `100dvh` not `100vh` — there is no dvh anywhere else in
      this codebase and the sheet drifts against a collapsing URL bar
      without it (audit §3h). The inset pair keeps the notch off the rows.

   2. It holds NO canvas. The fork's own visualizer canvas goes into its
      FULLSCREEN display mode underneath; this layer is chrome over it, so
      nothing here can desync the renderer from its own React tree.

   3. The chrome is transient, never modal. Shown on entry and on tap,
      auto-hidden after 3s WHILE PLAYING, never auto-hidden while paused
      (a paused visualizer has no auto-hide affordance problem, but the x is
      the only exit — hiding it would trap you), and instant — no fade —
      under prefers-reduced-motion. The 24px corner badge stays as a reveal
      target and a permanent reminder the mode has an exit. */
html.tb-touch.tb-vis #taskbar,
html.tb-touch.tb-vis #ministrip,
html.tb-touch.tb-vis #toggles,
html.tb-touch.tb-vis #start-menu,
html.tb-touch.tb-vis #pl-menu,
html.tb-touch.tb-vis #ctx-menu,
html.tb-touch.tb-vis #npsheet,
html.tb-touch.tb-vis body.np-sheet #npsheet { display: none !important; }

#tbvis { display: none; }
html.tb-touch.tb-vis #tbvis {
  display: block;
  position: fixed; inset: 0; width: 100%; height: 100dvh;
  z-index: 60;
  touch-action: none; -webkit-user-select: none; user-select: none;
  -webkit-tap-highlight-color: transparent;
  color: var(--fg);
}
/* the two static retro layers: a 1px scanline tile at <=8% and a vignette.
   pointer-events:none, no rAF-driven overlay — the visualizer is the only
   thing allowed to animate here. */
#tbvis .tbvis-scan, #tbvis .tbvis-vig { position: absolute; inset: 0; pointer-events: none; }
#tbvis .tbvis-scan {
  background: repeating-linear-gradient(to bottom,
    rgba(0,0,0,.55) 0 1px, rgba(0,0,0,0) 1px 3px);
  opacity: .08;
}
#tbvis .tbvis-vig {
  background: radial-gradient(ellipse at center,
    rgba(0,0,0,0) 45%, rgba(0,0,0,.55) 100%);
}
/* the corner badge: the reveal target when the chrome is hidden, and the
   only permanent sign of the mode. A sunken well rather than a text-shadow,
   because a shadow blooms into unreadability on a bright visualizer. */
#tbvis::after {
  content: ""; position: absolute; right: 0; bottom: 0;
  width: 26px; height: 26px; pointer-events: auto;
  background: var(--panel); opacity: .7;
  border-top: var(--bw) var(--bs) var(--border);
  border-left: var(--bw) var(--bs) var(--border);
  transition: opacity 200ms ease;
}
#tbvis.tbvis-hide::after { opacity: 0; }
#tbvis.tbvis-hide::after:hover { opacity: 1; }

#tbvis .tbvis-row {
  position: absolute; left: 0; right: 0;
  display: flex; align-items: center; gap: 10px;
  padding: 6px max(10px, env(safe-area-inset-right, 0px))
           6px max(10px, env(safe-area-inset-left, 0px));
  background: var(--panel);
  transition: opacity 220ms ease, transform 220ms ease;
}
#tbvis .tbvis-top {
  top: 0; padding-top: max(6px, env(safe-area-inset-top, 0px));
  border-bottom: var(--bw) var(--bs) var(--border);
}
#tbvis .tbvis-bottom {
  bottom: 0; padding-bottom: max(6px, env(safe-area-inset-bottom, 0px));
  border-top: var(--bw) var(--bs) var(--border);
  flex-wrap: wrap;
}
#tbvis button {
  min-width: 44px; min-height: 44px;   /* the floor, not a target */
  background: var(--panel2); color: var(--fg);
  border: var(--bw) var(--bs) var(--border); border-radius: var(--radius);
  font: 14px/1 var(--font); cursor: pointer;
}
#tbvis button:active {
  border-top-color: var(--bevel-down); border-left-color: var(--bevel-down);
  border-bottom-color: var(--bevel-up); border-right-color: var(--bevel-up);
}
#tbvis button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
#tbvis-exit { font-size: 18px; }
#tbvis .tbvis-meta {
  flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px;
}
#tbvis-preset { font: 12px/1.2 monospace; color: var(--accent);
                white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#tbvis-track  { font: 13px/1.2 var(--font); opacity: .85;
                white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#tbvis .tbvis-badge {
  font: 10px/1 monospace; letter-spacing: 1px; color: var(--accent-fg);
  background: var(--accent); padding: 4px 6px; border-radius: var(--radius);
}
#tbvis .tbvis-gap { flex: 1 1 auto; min-width: 0; }
#tbvis .tbvis-big { min-width: 68px; min-height: 68px; font-size: 22px; }
/* the transient state: chrome out, badge out, one tap brings it back */
#tbvis.tbvis-hide .tbvis-row { opacity: 0; pointer-events: none; }
#tbvis.tbvis-hide .tbvis-top { transform: translateY(-8px); }
#tbvis.tbvis-hide .tbvis-bottom { transform: translateY(8px); }
#tbvis .tbvis-toast {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  font: 13px/1 monospace; color: var(--accent);
  background: var(--panel); padding: 8px 12px; border-radius: var(--radius);
  border: var(--bw) var(--bs) var(--border);
  opacity: 0; pointer-events: none; transition: opacity 160ms ease;
}
#tbvis .tbvis-toast.tbvis-toast-on { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  #tbvis .tbvis-row, #tbvis::after, #tbvis .tbvis-toast { transition: none; }
  #tbvis.tbvis-hide .tbvis-row { transform: none; }
}
/* landscape: one bar, not two — the visualizer's height is the whole point
   of rotating, and a top+bottom pair eats 100px of a 390px screen */
@media (max-height: 560px) {
  #tbvis .tbvis-top { display: none; }
  #tbvis .tbvis-bottom { flex-wrap: nowrap; }
  #tbvis .tbvis-big { min-width: 56px; min-height: 56px; }
}
