:root {
  --lcd-bg: #0c0904;
  --lcd-fg: #2ecc71;
  --lcd-fg-dim: rgba(46, 204, 113, 0.5);
  --metal-1: #3a3d42;
  --metal-2: #212225;
  --metal-hi: #63666c;
  --chrome-1: #cfd3d8;
  --chrome-2: #83888e;
  --accent: #2ecc71;
  --accent-glow: rgba(46, 204, 113, 0.5);
  --danger: #ff5d5d;
  --page-bg-color: #201f21;
  --page-bg-image: linear-gradient(155deg, #2c2b2e 0%, #1e1d1f 45%, #252426 100%);

  /* ===== Mobile theme tokens (light default — see .mobile-theme-dark) =====
     A completely separate light/gray/white/red system pulled from the app
     icon, not the desktop chrome-frame skins — mobile's own card-based
     layout has nothing to do with brushed metal or wood grain. */
  --mobile-surface: #e7e7ea;
  --mobile-card: #ffffff;
  --mobile-ink: #17181a;
  --mobile-ink-dim: #8a8c8f;
  --mobile-seg-active-bg: #17181a;
  --mobile-seg-active-text: #ffffff;
  --mobile-btn-bg: #ffffff;
  --mobile-red: #ff4d3a;

  /* ===== Skin tokens (Midnight Drive defaults) ===== */
  --frame-bg: linear-gradient(180deg, #fbfcfd 0%, #d2d5d8 9%, #93979b 22%, #e7e9eb 36%, #71757a 58%, #babdc0 76%, #4b4e52 100%);
  --dial-ring: #55585c;
  --panel-bg: repeating-linear-gradient(135deg, #17181a 0px, #17181a 2px, #121314 3px, #121314 5px);
  --panel-radius: 8px;
  --ctrl-grad-1: #484b50;
  --ctrl-grad-2: #2a2c2f;
  --ctrl-hover-1: #53565b;
  --ctrl-hover-2: #303235;
  --ctrl-text: #cfd3d8;
  --preset-grad-1: #2e2e2e;
  --preset-grad-2: #161616;
  --preset-hover-1: #383838;
  --preset-hover-2: #1c1c1c;
  --preset-text: #e8dcc8;
  --knob-grad-1: #8b8f95;
  --knob-grad-2: #4a4d51;
  --knob-grad-3: #1c1d1f;
  --knob-face-1: #46484c;
  --knob-face-2: #1a1b1d;
  --knob-indicator-color: #1a1a1a;
  --brand-text: #ffffff;
}

/* Brushed Chrome — the dark body is gone entirely; light brushed-metal panel
   and controls, with dark text for contrast against the now-light surfaces. */
body.skin-chrome {
  --frame-bg: linear-gradient(180deg, #fdfeff 0%, #d8dbdd 9%, #8f969c 22%, #eef0f1 36%, #6d7580 58%, #c6cace 76%, #474b50 100%);
  --dial-ring: #71757a;
  --panel-bg: linear-gradient(163deg, #e6e8ea 0%, #b9bcc0 45%, #989b9f 100%);
  --panel-radius: 12px;
  --ctrl-grad-1: #eef0f1;
  --ctrl-grad-2: #a7abae;
  --ctrl-hover-1: #ffffff;
  --ctrl-hover-2: #b7bcc0;
  --ctrl-text: #2a2c2e;
  --preset-grad-1: #eef0f1;
  --preset-grad-2: #a7abae;
  --preset-hover-1: #ffffff;
  --preset-hover-2: #b7bcc0;
  --preset-text: #2a2c2e;
  --knob-grad-1: #ffffff;
  --knob-grad-2: #c3c6c9;
  --knob-grad-3: #71757a;
  --knob-face-1: #d8dadd;
  --knob-face-2: #8a8d91;
  --brand-text: #000000;
}

/* Soft Wood — warm wood-grain panel, chrome buttons and knobs. */
body.skin-wood {
  --frame-bg: linear-gradient(180deg, #fbfcfd 0%, #d2d5d8 9%, #93979b 22%, #e7e9eb 36%, #71757a 58%, #babdc0 76%, #4b4e52 100%);
  --dial-ring: #71757a;
  --panel-bg: repeating-linear-gradient(3deg, #3b2719 0px, #362314 4px, #402c1d 8px, #382517 12px, #3b2719 16px, #443021 20px);
  --panel-radius: 12px;
  --ctrl-grad-1: #eef0f1;
  --ctrl-grad-2: #a7abae;
  --ctrl-hover-1: #ffffff;
  --ctrl-hover-2: #b7bcc0;
  --ctrl-text: #2a2c2e;
  --preset-grad-1: #eef0f1;
  --preset-grad-2: #a7abae;
  --preset-hover-1: #ffffff;
  --preset-hover-2: #b7bcc0;
  --preset-text: #2a2c2e;
  --knob-grad-1: #ffffff;
  --knob-grad-2: #c3c6c9;
  --knob-grad-3: #71757a;
  --knob-face-1: #d8dadd;
  --knob-face-2: #8a8d91;
  --brand-text: #ffffff;
}

/* G'Rage Gray — matte molded plastic, gray knobs and buttons. */
body.skin-plastic {
  --frame-bg: linear-gradient(180deg, #9a9da1 0%, #7d8084 55%, #64676b 100%);
  --dial-ring: #45484c;
  --panel-bg: linear-gradient(165deg, #6b6e72 0%, #57595c 100%);
  --panel-radius: 16px;
  --ctrl-grad-1: #84878b;
  --ctrl-grad-2: #54575b;
  --ctrl-hover-1: #92959a;
  --ctrl-hover-2: #606368;
  --ctrl-text: #e6e6e6;
  --preset-grad-1: #75787c;
  --preset-grad-2: #46484c;
  --preset-hover-1: #84878b;
  --preset-hover-2: #54575b;
  --preset-text: #e6e6e6;
  --knob-grad-1: #9a9da1;
  --knob-grad-2: #6b6e72;
  --knob-grad-3: #45484c;
  --knob-face-1: #6b6e72;
  --knob-face-2: #3a3c3f;
  --knob-indicator-color: #2a2b2d;
  --brand-text: #000000;
}

/* Mobile's own light/dark toggle (see the sun/moon badge in the marquee) —
   applied on <html> since it's a page-wide theme, not a per-skin swap like
   the desktop tokens above. */
:root.mobile-theme-dark {
  --mobile-surface: #1c1d1f;
  --mobile-card: #2b2c2f;
  --mobile-ink: #f0efec;
  --mobile-ink-dim: #9a9ca0;
  --mobile-seg-active-bg: #f0efec;
  --mobile-seg-active-text: #17181a;
  --mobile-btn-bg: #34363a;
}

/* ===== Background scenes — a windshield view behind the player, independent
   of skin (which is the player's own finish). Real photos now (see
   .bg-media below + the background-strip picker), not the flat CSS
   gradients this used to fall back to. ===== */

/* Sits behind everything as either a looping video or a static image for the
   chosen background. The gradient body background above still paints
   underneath it (visible briefly while the media loads, or if it fails).
   Shared by #bgVideo (the four built-in scenes and a custom video upload)
   and #bgImage (a custom image upload) — only one is ever visible at a
   time. No oversized bleed here (unlike the mobile ::before background
   below) — this element is hidden entirely on mobile, so there's no
   toolbar-collapse edge case to buffer against, and every extra vw/vh of
   bleed is source resolution wasted on upscaling past what the viewport
   actually shows. */
.bg-media {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: -1;
  pointer-events: none;
}
.bg-media[hidden] { display: none; }
/* #bgImage2 layers on top of #bgImage (later in DOM = painted later, same
   z-index) for the "Day Cycle" background — its opacity is driven by JS to
   the current position between two time-of-day keyframes, and this
   transition just smooths each 60s recalculation into a continuous fade
   instead of a visible per-tick jump. */
#bgImage2 { transition: opacity 3s linear; }
/* Video/custom-upload backgrounds are desktop-only for now (mobile is getting
   its own overhaul later) — applyBackground() already skips wiring these up
   on mobile, this is just a hard guarantee regardless of JS state. */
:root.is-mobile-device .bg-media { display: none !important; }
/* Same desktop-only scope for the upload UI and the option it enables — no
   custom-upload picker on mobile until the mobile overhaul. */
:root.is-mobile-device .custom-bg-upload,
:root.is-mobile-device #customBackgroundOption { display: none; }

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  overflow: hidden;
  /* Belt-and-suspenders against double-tap-to-zoom — some mobile browsers
     honor this even when they're inconsistent about the viewport meta tag. */
  touch-action: manipulation;
}

/* The flat gradient lives on <html>, not <body>. Painting order puts a
   non-positioned element's own background (body, here) ABOVE a negative
   z-index descendant (.bg-media) no matter what the DOM/z-index says —
   only the canvas (html's own background, when html specifies one) paints
   further back than that. Keeping it on html is what lets the real photo
   backgrounds show through instead of always being covered by the gradient. */
html {
  background-color: var(--page-bg-color);
  background-image: var(--page-bg-image);
  background-size: 100% 100%;
}

body {
  background: transparent;
  color: #e8e6e2;
  font-family: 'Oswald', 'Segoe UI', sans-serif;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
}

/* Portrait: the player fills the screen edge-to-edge, no margin/padding
   around it at all — the old version padded body here, which left a gap
   around the unit exposing body's "none" background (i.e. html's dark
   gradient showing through as a visible dark border on every side). The
   safe-area clearance moves onto .radio-unit's own padding below instead,
   so it's inset content on the SAME surface, not a gap in front of a
   different-colored one. */
:root.is-mobile-device body {
  align-items: stretch;
  padding: 0;
  background: var(--mobile-surface);
  position: relative;
}
/* Long-press drives edit mode on mobile, so the native text-selection/copy
   callout that a long-press would otherwise trigger has to be switched off —
   otherwise every jiggle-mode entry fights the OS selection menu. Real form
   fields opt back in below so typing/pasting still works normally. */
:root.is-mobile-device * {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
:root.is-mobile-device input,
:root.is-mobile-device textarea {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}
.stage {
  width: 100%;
  max-width: 600px;
  /* Headroom the hanging air freshener occupies. It is absolutely positioned
     above the player, so it contributes nothing to any ancestor's measured
     height — which meant applyFitScale() sized the player as if the freshener
     weren't there and, on a laptop-height screen, scaled it to a size that
     pushed the freshener up behind the accessory menu.

     Reserving it here puts that space back into the layout, so the fit-scaler
     accounts for it again. It is reserved unconditionally rather than only
     when something is hung, so picking a freshener still never moves or
     resizes the player. 144px = the image's 138.8px rotated bounding height
     (130px square swayed +/-4deg) plus its 4px of daylight. */
  padding-top: 144px;
}
/* Mobile hides the accessory entirely, so it needs none of that headroom. */
:root.is-mobile-device .stage { padding-top: 0; }

/* ===== Radio unit shell ===== */

.unit-wrap { position: relative; padding-top: 4px; }
/* Mounting screws holding the bezel in place — same chrome-screw look as
   .face-screw below, just at the frame's top corners instead of the unit's
   bottom edge, so the two read as one consistent hardware detail. */
.mount-tab {
  position: absolute; top: 6px; width: 9px; height: 9px; border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--ctrl-hover-1), var(--ctrl-grad-2) 70%);
  box-shadow: 0 1px 2px rgba(0,0,0,0.6);
  z-index: 3;
}
.mount-tab::after {
  content: ''; position: absolute; inset: 2.5px 1px; background: #0a0a0a; opacity: 0.8; border-radius: 1px;
}
.mount-tab.left { left: 10px; } .mount-tab.right { right: 10px; }

.chrome-frame {
  position: relative;
  overflow: hidden;
  background: var(--frame-bg);
  border-radius: 12px;
  padding: 5px;
  box-shadow: 0 24px 48px rgba(0,0,0,0.55), 0 0 0 1px rgba(0,0,0,0.4);
}
.chrome-frame.has-sheen::before {
  content: ''; position: absolute; top: 0; left: -40%; width: 35%; height: 100%;
  background: linear-gradient(75deg, transparent, rgba(255,255,255,0.35), transparent);
  transform: skewX(-18deg); pointer-events: none; z-index: 2;
}
.chrome-frame.has-sheen::after {
  content: ''; position: absolute; top: 0; left: 60%; width: 16%; height: 100%;
  background: linear-gradient(75deg, transparent, rgba(255,255,255,0.18), transparent);
  transform: skewX(-18deg); pointer-events: none; z-index: 2;
}
/* Wood and plastic are matte finishes — no glossy sweep across the bezel. */
body.skin-wood .chrome-frame.has-sheen::before,
body.skin-wood .chrome-frame.has-sheen::after,
body.skin-plastic .chrome-frame.has-sheen::before,
body.skin-plastic .chrome-frame.has-sheen::after {
  content: none;
}
.radio-unit {
  position: relative;
  padding: 18px 20px 34px;
  background: var(--panel-bg);
  border-radius: var(--panel-radius);
}
/* The login/register card reuses .radio-unit for its shell but should never
   pick up a skin panel or chrome frame — skin is a per-account preference
   nobody has picked yet at that screen. Instead it gets its own fixed
   frosted-glass look, floating over whichever random standard photo is
   showing behind it (see showAuthScreen() in app.js). */
.auth-panel {
  background: rgba(255, 255, 255, 0.09);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 18px;
}

.brand-plate {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  margin-bottom: 12px;
}
.brand-plate .line { flex: 1; height: 1px; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.18), transparent); }

/* The auth screen's own brand row (login/register) — a separate, simpler
   markup from .brand-plate above since it has no dial/line dividers, but it
   needs the same flex treatment or the LED just stacks under the wordmark
   instead of sitting beside it. */
.brand-row { display: flex; justify-content: center; margin-bottom: 18px; }
.brand-group { display: flex; align-items: center; gap: 8px; }

.brand {
  font-family: Georgia, 'Times New Roman', serif;
  font-weight: 700;
  font-style: italic;
  letter-spacing: 0.5px;
  font-size: 16px;
  color: var(--brand-text);
}

.power-led {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #3a1010;
  box-shadow: inset 0 0 2px #000;
}
.power-led.on {
  background: #ff5d3a;
  box-shadow: 0 0 6px 2px rgba(255, 93, 58, 0.7), inset 0 0 2px #400;
}
/* The brand-plate LED is the Keeper easter egg (see app.js): clicking it opens
   the lighthouse game. The ::after is an invisible tap target around the 8px
   dot, so a thumb can find it without the light itself getting any bigger. */
#powerLed { position: relative; cursor: pointer; }
#powerLed::after { content: ''; position: absolute; inset: -14px; border-radius: 50%; }

.main-row { display: flex; align-items: flex-end; gap: 14px; }
/* Without an explicit flex-basis/min-width, a long nowrap marquee string's
   intrinsic width leaks all the way up through this flex chain and balloons
   the whole player — min-width:0 lets it shrink to the row's actual space. */
.dial-col { flex: 1; min-width: 0; }

.tab-row { display: flex; justify-content: space-between; gap: 6px; margin-bottom: 6px; }
.deck-tab-btn {
  flex: 1;
  background: linear-gradient(180deg, var(--ctrl-grad-1) 0%, var(--ctrl-grad-2) 100%);
  border: 1px solid var(--dial-ring);
  border-bottom: none;
  border-radius: 5px 5px 0 0;
  padding: 4px 6px;
  font-size: 8.5px;
  letter-spacing: 0.8px;
  color: var(--ctrl-text);
  font-weight: 700;
  font-family: 'Oswald', sans-serif;
  cursor: pointer;
  text-transform: uppercase;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.08);
}
.deck-tab-btn.active {
  color: #ff5d3a;
  text-shadow: 0 0 4px rgba(255,93,58,0.6);
  border-color: #ff5d3a;
}
.deck-tab-btn:active { transform: translateY(1px); box-shadow: inset 0 2px 4px rgba(0,0,0,0.5); }

/* Page 1/2/3 sits in the tab row now, sized exactly like its neighbors. */
.page-toggle.deck-tab-btn-sized {
  flex: 1;
  height: auto;
  background: linear-gradient(180deg, var(--ctrl-grad-1) 0%, var(--ctrl-grad-2) 100%);
  border: 1px solid var(--dial-ring);
  border-bottom: none;
  border-radius: 5px 5px 0 0;
  padding: 4px 6px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.08);
}
.page-toggle.deck-tab-btn-sized:active { transform: translateY(1px); box-shadow: inset 0 2px 4px rgba(0,0,0,0.5); }
.page-toggle.deck-tab-btn-sized .page-toggle-seg {
  font-size: 8.5px;
  font-family: 'Oswald', sans-serif;
  color: var(--ctrl-text);
  opacity: 0.7;
}
.page-toggle.deck-tab-btn-sized .page-toggle-seg.active {
  color: #ff5d3a;
  opacity: 1;
  text-shadow: 0 0 4px rgba(255,93,58,0.6);
}


.edit-btn { display: none; } /* replaced by the corner edit badge */

/* ===== Corner badges (edit / pop-out / info / account) ===== */

.badge-row { position: absolute; right: 10px; bottom: 8px; display: flex; gap: 6px; }
/* No edit/reorder mode on mobile — drag-to-reorder presets stays a
   desktop-only path for now. */
:root.is-mobile-device #editModeBtn { display: none !important; }
.corner-badge {
  position: relative;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--ctrl-hover-1), var(--ctrl-grad-2) 75%);
  border: none;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.4), inset 0 1px 1px rgba(255,255,255,0.3);
  display: flex; align-items: center; justify-content: center;
  color: var(--accent);
  font-family: 'Oswald', sans-serif;
  font-weight: 700;
  font-size: 9px;
  line-height: 1;
  cursor: pointer;
}
/* The monogram letter is plain text content, so it picks up the base
   .corner-badge color (--accent, green) unless overridden — matching it to
   --ctrl-text instead lines it up with the edit/mini-player icons next to
   it, which are drawn from --ctrl-text via their ::after shapes. */
.corner-badge.account-badge { color: var(--ctrl-text); }
.corner-badge:active { transform: translateY(1px); }
.corner-badge.active { box-shadow: 0 0 0 1px var(--accent), 0 0 6px rgba(46,204,113,0.6); }
/* Pencil: a tapered body (clip-path point = the tip) with a small
   accent-colored band at the blunt end standing in for the eraser, angled
   like a pencil actually held to write. */
.corner-badge.edit::after {
  content: '';
  width: 12px; height: 3px;
  clip-path: polygon(0% 50%, 22% 0%, 100% 0%, 100% 100%, 22% 100%);
  background: linear-gradient(90deg, var(--ctrl-text) 0%, var(--ctrl-text) 78%, var(--accent) 78%, var(--accent) 100%);
  transform: rotate(-45deg);
}
/* Mini-player: one shape (same tapered-clip-path technique as the pencil
   above) — a shaft with a triangular arrowhead, pointing up and out, the
   universal "pop out into its own window" gesture. */
.corner-badge.miniplayer-badge::after {
  content: '';
  width: 12px; height: 3px;
  clip-path: polygon(0% 35%, 0% 65%, 60% 65%, 60% 100%, 100% 50%, 60% 0%, 60% 35%);
  background: var(--ctrl-text);
  transform: rotate(-45deg);
}
/* Weather: opens /weather in a new tab. It is an <a>, so it needs the
   underline killed — there is no text to underline today, but a browser's
   forced-colors or reader mode can still draw one through the glyph.
   The sun is sized and coloured like the white-noise badge's wave rather
   than the accent green, so all four icons down here read as one set. */
.corner-badge.weather-badge { text-decoration: none; }
.corner-badge.weather-badge svg { width: 11px; height: 11px; color: var(--ctrl-text); }
:root.is-mobile-device .hint-text { display: none; }
/* #accountBtn is JS-reparented into the marquee badge column on mobile, and
   there's no edit badge or info badge left there either — nothing else in
   this row shows on mobile, so it collapses to nothing. */
:root.is-mobile-device .badge-row { display: none; }
.corner-badge.account-badge.has-badge::after {
  content: '';
  position: absolute;
  top: -2px;
  right: -2px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #ff5d3a;
  box-shadow: 0 0 4px 1px rgba(255, 93, 58, 0.7), 0 0 0 1.5px #1a1b1d;
}

.face-screw {
  position: absolute; bottom: 9px; width: 7px; height: 7px; border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--ctrl-hover-1), var(--ctrl-grad-2) 70%);
  box-shadow: 0 1px 1px rgba(0,0,0,0.6);
}
.face-screw::after { content: ''; position: absolute; inset: 2px 0.5px; background: #0a0a0a; opacity: 0.8; border-radius: 1px; }
.face-screw.left { left: 9px; }

/* ===== Knobs (desktop only — mobile relies on hardware volume) ===== */

.knob-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding-top: 6px;
  flex: 0 0 auto;
}
.knob-chrome {
  width: 62px; height: 62px; border-radius: 50%; position: relative;
  cursor: grab;
  box-shadow: 0 3px 6px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(0,0,0,0.4);
  touch-action: none;
  user-select: none;
}
.knob-chrome.big { width: 74px; height: 74px; }
.knob-chrome:active { cursor: grabbing; }
/* The power knob only ever clicks between two fixed positions — it's never
   dragged, so it shouldn't invite that with a grab cursor. */
#powerKnob, #powerKnob:active { cursor: pointer; }
.knob-knurl {
  position: absolute; inset: 0; border-radius: 50%;
  background: repeating-conic-gradient(var(--knob-grad-1) 0deg 4deg, var(--knob-grad-3) 4deg 8deg);
}
.knob-dome {
  position: absolute; inset: 9px; border-radius: 50%;
  background: radial-gradient(circle at 33% 28%, var(--knob-face-1), var(--knob-grad-2) 45%, var(--knob-face-2) 100%);
  box-shadow: inset 0 -3px 5px rgba(0,0,0,0.4), inset 0 2px 3px rgba(255,255,255,0.3);
}
.knob-indicator {
  position: absolute;
  top: 12%;
  left: 50%;
  width: 3px;
  height: 30%;
  background: var(--knob-indicator-color);
  border-radius: 2px;
  /* Pivot must land on the knob's actual visual center (50% of the whole
     face), expressed relative to this element's own box: its top edge sits
     at 12% of the face and it's 30% tall, so center-of-face falls at
     (50-12)/30 = 126.7% of the indicator's own height. The old 170% pivoted
     below true center, so the line swung off-axis instead of turning in
     place. */
  transform-origin: 50% 126.7%;
  transform: translateX(-50%) rotate(0deg);
}
/* Only the power knob snaps between two fixed positions on click — give it
   alone a turning animation. The volume knob is dragged continuously, where
   a transition would make it visibly lag behind the pointer. */
#powerKnob .knob-indicator {
  transition: transform 0.2s ease;
}
.knob-label {
  font-size: 9px;
  letter-spacing: 1.5px;
  color: var(--ctrl-text);
  opacity: 0.7;
  font-weight: 600;
}

/* Neither main-row knob shows on mobile — power lives in the big red button
   in .mobile-ctrl-row instead, and there's no volume knob on mobile at all. */
:root.is-mobile-device .knob-col:has(#volumeKnob),
:root.is-mobile-device .knob-col:has(#powerKnob) {
  display: none;
}

/* Portrait: stretch the whole chassis chain down to the preset grid so it
   fills the available height instead of sitting as a fixed-height block with
   empty space below it. Landscape relaxes this back to a scrollable card —
   see the landscape media query at the end of the file. */
:root.is-mobile-device .unit-wrap { height: 100%; }
:root.is-mobile-device .chrome-frame { height: 100%; }
:root.is-mobile-device .radio-unit {
  height: 100%;
  display: flex;
  flex-direction: column;
}
:root.is-mobile-device .main-row {
  flex: 1;
  min-height: 0;
  align-items: stretch;
}
:root.is-mobile-device .dial-col {
  display: flex;
  flex-direction: column;
}
/* Visual order only — DOM order stays whatever's simplest for the shared
   markup/JS (desktop reads the same elements top-to-bottom in their original
   spots), so mobile just resequences them: marquee (right under the
   wordmark — time/weather now live inside it), seek + power, band/page/add,
   then the presets filling whatever's left. */
:root.is-mobile-device .dial-assembly { order: 1; flex: none; }
:root.is-mobile-device .mobile-ctrl-row { order: 2; }
:root.is-mobile-device .tab-row { order: 3; }
:root.is-mobile-device .preset-row-2col {
  order: 4;
  flex: 1;
  min-height: 0;
}

/* =====================================================================
   Mobile: light/gray/white/red app design, pulled from the app icon —
   a completely different visual system from desktop's chrome-frame skins.
   No brushed-metal bezel, no exposed screws; a flat full-bleed surface
   instead, with the real marquee/presets/buttons restyled as cards. See
   applyMobileTheme()/the .mobile-theme-dark tokens for the light/dark
   toggle itself. ===================================================== */
:root.is-mobile-device .chrome-frame {
  background: var(--mobile-surface);
  padding: 0;
  border-radius: 0;
  box-shadow: none;
}
:root.is-mobile-device .chrome-frame.has-sheen::before,
:root.is-mobile-device .chrome-frame.has-sheen::after,
:root.is-mobile-device .mount-tab,
:root.is-mobile-device .face-screw {
  display: none;
}
:root.is-mobile-device .radio-unit {
  background: var(--mobile-surface);
  border-radius: 0;
  padding: calc(env(safe-area-inset-top, 0px) + 28px) 18px calc(env(safe-area-inset-bottom, 0px) + 16px);
}

/* ----- Brand row: wordmark right under the status bar, dot to the right —
   tight margin so the marquee sits right underneath it. -----*/
:root.is-mobile-device .brand-plate { margin-bottom: 8px; }
:root.is-mobile-device .brand-plate .line { display: none; }
:root.is-mobile-device .brand { color: var(--mobile-ink); font-size: 17px; }

/* ----- Marquee: same dark LCD look as desktop already has (no change to
   --lcd-bg/--lcd-fg needed) — just hide the FM scale/duplicate clock mobile
   doesn't use, and drop the outer chrome-bezel ring (mobile has no bezel to
   match anymore). Reads left-to-right as three lanes: time/weather (reusing
   the marquee's own green + font, not a separate card), the scrolling
   station name + now-playing, then the profile/theme badges — all real
   flex children now, not absolutely-positioned overlays, so nothing needs
   manual margin math to keep the scrolling text from running under them. */
:root.is-mobile-device .lcd-scale,
:root.is-mobile-device .lcd-scale-yt,
:root.is-mobile-device .lcd-scale-sp,
:root.is-mobile-device .lcd-clock {
  display: none;
}
:root.is-mobile-device .dial-assembly {
  padding: 16px 14px 16px 16px;
  /* Keep the inset "recessed screen" shadow, drop the two outer-ring
     components (0 0 0 1px #000, 0 0 0 2px var(--dial-ring)) that read as a
     bezel border here. */
  box-shadow: inset 0 3px 8px rgba(0,0,0,0.9);
}
:root.is-mobile-device .dial-readout-row {
  align-items: center;
  /* The border/padding-top above belongs to desktop's scale-then-readout
     divider — with .lcd-scale hidden on mobile there's nothing above it to
     divide from, so it read as a stray line floating over the marquee. */
  border-top: none;
  padding-top: 0;
}
.marquee-util {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-family: 'VT323', monospace;
  color: var(--lcd-fg);
  border-right: 1px solid rgba(46, 204, 113, 0.18);
}
:root.is-mobile-device .marquee-util {
  display: flex;
  flex: 0 0 44px;
  align-self: stretch;
  padding-right: 8px;
}
.marquee-util .mobile-time { font-size: 17px; line-height: 1; }
.marquee-util .mobile-weather {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.marquee-util .mobile-weather-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
}
.marquee-util .mobile-weather-icon svg { width: 100%; height: 100%; display: block; }
.marquee-util .mobile-weather-temp { font-size: 14px; line-height: 1; }
:root.is-mobile-device .lcd-marquee-wrap {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
}
:root.is-mobile-device .lcd-marquee-text { font-size: 36px; }
.lcd-marquee-sub {
  display: none;
  font-family: 'VT323', monospace;
  font-size: 13px;
  letter-spacing: 0.5px;
  color: var(--lcd-fg);
  opacity: 0.6;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
:root.is-mobile-device .lcd-marquee-sub { font-size: 23px; }
:root.is-mobile-device .lcd-marquee-sub { display: block; }
.lcd-marquee-sub.empty { display: none; }
.mobile-marquee-badges {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 7px;
}
:root.is-mobile-device .mobile-marquee-badges {
  display: flex;
  flex: 0 0 44px;
  align-self: stretch;
}
:root.is-mobile-device .mobile-marquee-badges .corner-badge,
:root.is-mobile-device .mobile-marquee-badges .theme-toggle-badge {
  position: static;
  width: 32px;
  height: 32px;
  font-size: 13px;
  background: var(--mobile-card);
  color: var(--mobile-ink);
  box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.theme-toggle-badge {
  border-radius: 50%;
  border: none;
  padding: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: inherit;
}

/* ----- Seek + power row ----- */
/* Reshaped from circles into the same rounded rectangles as the Band / pages /
   Add row below, and sitting directly on top of it. The circles were 58px and
   66px with 14px margins either side — 94px of height for three controls. At
   the tab row's own height that becomes 34px, freeing 60px, which is more than
   the widget bar above costs. The preset grid ends up slightly taller than it
   was before any of this. */
.mobile-ctrl-row {
  display: none;
  align-items: stretch;
  gap: 8px;
  /* Same 8px below as between the buttons, so this row and the Band/pages/Add
     row read as a pair rather than one block. */
  margin: 10px 0 8px;
}
:root.is-mobile-device .mobile-ctrl-row { display: flex; }
.mobile-seek-btn,
.mobile-power-btn {
  border: none;
  /* Matches .deck-tab-btn / .page-toggle exactly. */
  border-radius: 10px;
  flex: 1 1 0;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.mobile-seek-btn {
  font-size: 14px;
  background: var(--mobile-btn-bg);
  color: var(--mobile-ink);
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.mobile-power-btn {
  font-size: 16px;
  background: var(--mobile-btn-bg);
  color: var(--mobile-ink);
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.mobile-power-btn.on {
  background: radial-gradient(circle at 35% 30%, #ff7a68, var(--mobile-red) 70%);
  color: #fff;
  /* No outer halo ring now that it's a rectangle in a row of rectangles — it
     would read as a misaligned border against its neighbours. The fill still
     makes it the only red control on the row. */
  box-shadow: 0 3px 10px rgba(255,77,58,0.4);
}

/* ----- Band / Page / Add: segmented pills + a square add button, instead
   of desktop's three plain tab buttons. ----- */
:root.is-mobile-device .tab-row {
  gap: 8px;
  margin-bottom: 0;
}
:root.is-mobile-device .deck-tab-btn,
:root.is-mobile-device .page-toggle {
  background: var(--mobile-card);
  border: none;
  border-radius: 10px;
  box-shadow: 0 2px 5px rgba(0,0,0,0.05);
  padding: 3px;
  height: auto;
}
.band-label-mobile { display: none; }
:root.is-mobile-device .band-label-desktop { display: none; }
:root.is-mobile-device #modeBtn {
  flex: 1.3;
  display: flex;
  padding: 3px;
}
:root.is-mobile-device .band-label-mobile {
  display: block;
  flex: 1;
  text-align: center;
  padding: 8px 0;
  font-size: 10px;
  font-weight: 600;
  border-radius: 7px;
  color: var(--mobile-ink-dim);
  text-transform: none;
  letter-spacing: 0.2px;
}
:root.is-mobile-device .band-label-mobile.active {
  background: var(--mobile-seg-active-bg);
  color: var(--mobile-seg-active-text);
}
:root.is-mobile-device .page-toggle .page-toggle-seg {
  border-radius: 7px;
  color: var(--mobile-ink-dim);
  font-size: 10px;
}
:root.is-mobile-device .page-toggle .page-toggle-seg.active {
  background: var(--mobile-seg-active-bg);
  color: var(--mobile-seg-active-text);
  opacity: 1;
  text-shadow: none;
}
:root.is-mobile-device #addStationBtn {
  flex: none;
  width: 40px;
  padding: 0;
  font-size: 0;
  background: var(--mobile-red);
  box-shadow: 0 3px 8px rgba(255,77,58,0.3);
}
:root.is-mobile-device #addStationBtn::after {
  content: '+';
  font-size: 19px;
  color: #fff;
  font-family: 'Oswald', sans-serif;
}

/* ----- Preset cards: rounded light/dark cards instead of desktop's dark
   rectangular buttons — the jiggle/remove-× edit-mode styling further down
   already targets .preset-btn generically, so it carries over unchanged. */
:root.is-mobile-device .preset-btn {
  height: auto;
  font-size: 12px;
  padding: 8px 4px;
  border-radius: 10px;
  border: none;
  background: var(--mobile-card);
  color: var(--mobile-ink);
  box-shadow: 0 2px 5px rgba(0,0,0,0.08);
}
:root.is-mobile-device .preset-btn .preset-fullname {
  color: var(--mobile-ink-dim);
  font-size: 10.5px;
  line-height: 1.25;
  /* Three lines total per card (call sign + this wrapping to 2) instead of
     desktop's single truncated line — there's room for it now that the
     cards are taller, so a longer friendly name doesn't get cut off. */
  white-space: normal;
  text-overflow: initial;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
:root.is-mobile-device .preset-btn.playing {
  box-shadow: 0 0 0 2px var(--mobile-red), 0 2px 5px rgba(0,0,0,0.08);
}
:root.is-mobile-device .preset-btn-empty {
  background: transparent;
  border: 1.5px dashed rgba(128,128,128,0.35);
  box-shadow: none;
}

/* ===== Dial (was "LCD") ===== */

.dial-assembly {
  flex: 1; min-width: 0;
  background: var(--lcd-bg);
  border-radius: 5px;
  padding: 9px 13px 8px;
  box-shadow: inset 0 3px 8px rgba(0,0,0,0.9), 0 0 0 1px #000, 0 0 0 2px var(--dial-ring);
  position: relative;
  overflow: hidden;
}

.lcd-scanlines {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    0deg,
    rgba(0,0,0,0.15) 0px,
    rgba(0,0,0,0.15) 1px,
    transparent 2px,
    transparent 3px
  );
  pointer-events: none;
}

.lcd-scale {
  position: relative;
  display: flex;
  justify-content: space-between;
  font-family: 'VT323', monospace;
  font-size: 13px;
  letter-spacing: 0.5px;
  color: rgba(46,204,113,0.55);
  padding-bottom: 11px;
  margin-bottom: 6px;
  border-bottom: 1px solid rgba(46,204,113,0.25);
  font-variant-numeric: tabular-nums;
}
/* Minor ruler ticks under the major frequency numbers, so it reads as a
   continuous dial scale rather than six floating digits. */
.lcd-scale::before {
  content: '';
  position: absolute;
  left: 1px; right: 1px; bottom: 0;
  height: 6px;
  background: repeating-linear-gradient(
    90deg,
    rgba(46,204,113,0.45) 0px, rgba(46,204,113,0.45) 1px,
    transparent 1px, transparent 13.5px
  );
}
.lcd-scale-indicator {
  position: absolute;
  top: -5px;
  left: 50%;
  width: 2.5px;
  height: 26px;
  background: #ff5a1e;
  border-radius: 1px;
  box-shadow: 0 0 8px rgba(255,90,30,0.95), 0 0 2px rgba(255,90,30,1);
  transition: left 0.25s ease, opacity 0.25s ease;
}
.lcd-scale-indicator::before {
  content: '';
  position: absolute;
  top: -3px;
  left: 50%;
  width: 6px;
  height: 6px;
  background: #ff5a1e;
  border-radius: 50%;
  transform: translateX(-50%);
  box-shadow: 0 0 5px rgba(255,90,30,0.9);
}

.lcd-scale-yt {
  justify-content: center;
  gap: 8px;
  color: #ff4d4d;
  border-bottom-color: rgba(255,77,77,0.3);
}
.lcd-scale-yt .yt-dot {
  color: #ff2d2d;
  text-shadow: 0 0 6px rgba(255,45,45,0.8);
  font-size: 10px;
}
.lcd-scale-yt .yt-label { letter-spacing: 2px; }

/* Same row, amber where YouTube is red. Deliberately not Spotify's brand
   green: the LCD is already green for Radio, so a green Spotify band reads as
   "no band change happened". Amber is the third colour a real receiver would
   have had anyway. There is no frequency scale to draw for either of these
   bands — a playlist has no place on a dial — so both are a lit dot and a
   word where the numbers would be. */
.lcd-scale-sp {
  justify-content: center;
  gap: 8px;
  color: #ffb02e;
  border-bottom-color: rgba(255,176,46,0.3);
}
.lcd-scale-sp .sp-dot {
  color: #ff9e0b;
  text-shadow: 0 0 6px rgba(255,158,11,0.8);
  font-size: 10px;
}
.lcd-scale-sp .sp-label { letter-spacing: 2px; }

.dial-readout-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  border-top: 1px solid rgba(255,255,255,0.06);
  padding-top: 5px;
}

.lcd-marquee-wrap {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  position: relative;
}
.lcd-marquee-text {
  display: inline-block;
  font-family: 'VT323', monospace;
  font-size: 22px;
  letter-spacing: 1px;
  color: var(--lcd-fg);
  text-shadow: none;
  white-space: nowrap;
}
.lcd-marquee-wrap.marquee .lcd-marquee-text {
  animation: marquee 20s linear infinite;
  padding-left: 100%;
}
/* Nothing selected (or the active station is paused, which drops the
   preset's own "playing" ring) — the marquee should go blank to match,
   not keep showing stale station info. Visibility (not display:none) keeps
   the row's height stable so nothing reflows when it clears. */
.lcd-marquee-wrap.no-selection .lcd-marquee-text,
.lcd-marquee-wrap.no-selection .lcd-marquee-sub {
  visibility: hidden;
}

.lcd-clock {
  flex: 0 0 auto;
  font-family: 'VT323', monospace;
  font-size: 16px;
  letter-spacing: 0.5px;
  color: var(--lcd-fg);
  font-variant-numeric: tabular-nums;
}

/* A second readout row under the marquee, holding the four at-a-glance
   widgets. Divided into equal cells the same way the frequency scale above
   divides itself, so the LCD reads as one instrument rather than a display
   with a toolbar bolted under it.

   Each cell is deliberately narrow (~1/4 of the dial), so everything written
   into them is abbreviated to fit — see stripFit() in app.js, which picks the
   longest form that actually measures short enough rather than trusting a
   character count. Cells that overflow anyway ellipsize instead of pushing
   their neighbours around. */
.lcd-strip {
  display: flex;
  align-items: center;
  margin-top: 6px;
  padding-top: 5px;
  border-top: 1px solid rgba(46, 204, 113, 0.18);
  font-family: 'VT323', monospace;
  /* 14px rather than the marquee's 15: with five cells sharing ~360px, one
     step down is what keeps the sprites and the AM/PM on screen instead of
     stripFit falling back to a barer form. VT323 is a pixel face designed for
     small sizes, so it stays crisp here. */
  font-size: 14px;
  color: var(--lcd-fg-dim);
  min-height: 21px;
}
.lcd-strip-cell {
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Five cells across ~360px leaves ~72px each, so horizontal padding is
     expensive here — every 2px back is a character that stays legible or a
     sprite that survives stripFit's fallback. */
  padding: 0 3px;
  border-right: 1px solid rgba(46, 204, 113, 0.14);
  /* buttons: strip the UA chrome so they read as printed LCD text */
  background: none;
  border-top: 0;
  border-left: 0;
  border-bottom: 0;
  font: inherit;
  color: inherit;
  letter-spacing: 0.5px;
  line-height: 1.25;
}
.lcd-strip-cell:last-child { border-right: 0; }
/* Equal fifths gave the scores cell ~71px, which is one team's worth. These
   weights hand it the slack the shorter cells don't need — sunrise/sunset is
   a sprite and five characters, the flag is one word — so it can carry both
   abbreviations, both scores and its sport sprite without the marquee having
   to grow. They sum to 5, so the row still fills exactly. */
#stripDate    { flex-grow: 1; }
#stripWeather { flex-grow: 0.8; }
#stripSports  { flex-grow: 1.5; }
#stripSun     { flex-grow: 0.8; }
#stripFlag    { flex-grow: 0.9; }
button.lcd-strip-cell { cursor: pointer; }
button.lcd-strip-cell:hover,
button.lcd-strip-cell:focus-visible { color: var(--lcd-fg); outline: none; }
button.lcd-strip-cell:focus-visible { text-decoration: underline; }
/* The value inside a cell is the bright part; its label stays dim. */
.lcd-strip-cell b { color: var(--lcd-fg); font-weight: normal; }
.lcd-strip-cell .wx-ico {
  display: inline-block;
  vertical-align: -2px;
  margin-right: 2px;
}
.lcd-strip-cell .wx-ico svg { display: block; width: 13px; height: 13px; }
/* A team that's playing right now — same live red as the YouTube band. */
.lcd-strip-cell.is-live b { color: #ff4d4d; }
/* A national half-staff order is worth spotting without reading it, so the
   cell shifts off the LCD's green to amber — semantic colour, deliberately
   distinct from both the green and the live red above. The word still reads
   HALF; the colour is a second signal, not the only one. */
.lcd-strip-cell.is-half b,
.lcd-strip-cell.is-half .wx-ico { color: #e2a33c; }
/* The strip is a desktop-only row; the mobile chassis has its own badges. */
/* The strip lives inside .dial-assembly, which on mobile IS the marquee — so
   it needs no repositioning here, only sizing. Four cells rather than desktop's
   five: weather already has a permanent home in the marquee's left column
   (.marquee-util), and showing it twice would waste a quarter of the bar. */
:root.is-mobile-device #stripWeather { display: none; }
:root.is-mobile-device .lcd-strip {
  font-size: 14px;
  margin-top: 8px;
  padding-top: 6px;
  min-height: 20px;
}

/* Marquee + clock switch to the same red as the YouTube scale row
   (.lcd-scale-yt) the instant Band flips over — repointing --lcd-fg here
   instead of hardcoding a second red keeps both in sync automatically. */
body.bank-youtube {
  --lcd-fg: #ff4d4d;
  --lcd-fg-dim: rgba(255, 77, 77, 0.5);
}

/* Same trick for the Spotify band, pointing at the amber in .lcd-scale-sp. */
body.bank-spotify {
  --lcd-fg: #ffb02e;
  --lcd-fg-dim: rgba(255, 176, 46, 0.5);
}

@keyframes marquee {
  0% { transform: translateX(0); }
  100% { transform: translateX(-100%); }
}

/* ===== Presets ===== */

.page-toggle {
  width: 100%;
  height: 32px;
  display: flex;
  border: none;
  padding: 0;
  background: linear-gradient(180deg, var(--preset-grad-1), var(--preset-grad-2));
  border-radius: 6px;
  cursor: pointer;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.08),
    inset 0 -2px 3px rgba(0,0,0,0.5),
    0 1px 0 rgba(0,0,0,0.6);
}
.page-toggle:hover { background: linear-gradient(180deg, var(--preset-hover-1) 0%, var(--preset-hover-2) 100%); }
.page-toggle:active { box-shadow: inset 0 2px 4px rgba(0,0,0,0.7); }
.page-toggle-seg {
  flex: 1;
  height: 100%;
  color: var(--preset-text);
  opacity: 0.55;
  font-family: 'Oswald', sans-serif;
  font-weight: 700;
  font-size: 11px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.page-toggle-seg.active {
  color: var(--accent);
  opacity: 1;
  text-shadow: none;
}
.preset-row {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 5px;
  margin-top: 10px;
}
:root.is-mobile-device .preset-row-2col {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* 1fr rows, not a fixed cap: the grid lives in a flex:1 slot that exactly
     fills whatever's left below the marquee/controls/tabs, so stretching
     rows to fill it is what guarantees all 12 presets are always on screen
     with zero scrolling, on any device height — a fixed px cap could
     overflow (forcing a scroll) on a shorter screen or leave a dead gap on
     a taller one. */
  grid-template-rows: repeat(4, minmax(0, 1fr));
  gap: 8px;
}

.preset-btn {
  position: relative;
  width: 100%;
  min-width: 0;
  background: linear-gradient(180deg, var(--preset-grad-1) 0%, var(--preset-grad-2) 100%);
  border: 1px solid var(--dial-ring);
  border-radius: 4px;
  color: var(--preset-text);
  font-family: 'Oswald', sans-serif;
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.2px;
  cursor: pointer;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.08),
    0 1px 0 rgba(0,0,0,0.6);
  height: 40px;
  padding: 8px 3px 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
}
.preset-btn::before {
  content: '';
  position: absolute;
  top: 4px;
  left: 50%;
  transform: translateX(-50%);
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: #4a1a12;
}
.preset-btn:hover { background: linear-gradient(180deg, var(--preset-hover-1) 0%, var(--preset-hover-2) 100%); }
.preset-btn:active { box-shadow: inset 0 2px 4px rgba(0,0,0,0.7); }
/* Unprogrammed slots — a real radio always shows its full bank of buttons,
   it just doesn't have anything on the ones you haven't set yet. */
.preset-btn.preset-btn-empty {
  background: transparent;
  border: 1px dashed var(--dial-ring);
  box-shadow: none;
  cursor: default;
  pointer-events: none;
}
.preset-btn.preset-btn-empty::before { display: none; }

.preset-btn .preset-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  max-width: 100%;
}

/* Friendly full station name, shown beneath the short call-sign label —
   only rendered when it actually adds information (see renderPresets). */
.preset-btn .preset-fullname {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  max-width: 100%;
  font-size: 8.5px;
  font-weight: 500;
  letter-spacing: 0.1px;
  opacity: 0.6;
}

.preset-btn .preset-name-input {
  width: 100%;
  max-width: 100%;
  background: #17181a;
  border: 1px solid var(--accent);
  border-radius: 3px;
  color: #eee;
  font-family: 'Oswald', sans-serif;
  font-size: 11px;
  font-weight: 600;
  text-align: center;
  padding: 3px 2px;
}

.preset-btn.playing::before {
  background: #ff5d3a;
  box-shadow: 0 0 5px 2px rgba(255,93,58,0.9);
}

.preset-btn .remove-x {
  position: absolute;
  top: -5px;
  right: -5px;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: #2a2b2d;
  color: var(--danger);
  font-size: 12px;
  line-height: 15px;
  text-align: center;
  font-weight: 700;
  display: none;
  box-shadow: 0 0 0 1px #000;
}

.preset-row.edit-mode .preset-btn .remove-x { display: block; }
.preset-row.edit-mode .preset-btn { cursor: grab; }
.preset-row.edit-mode .preset-btn:active { cursor: grabbing; }
.preset-row.edit-mode .preset-btn .remove-x { cursor: pointer; }
.preset-row.edit-mode .preset-btn:active .remove-x { cursor: pointer; }
.preset-btn.dragging { opacity: 0.35; }

@keyframes jiggle {
  0% { transform: rotate(-1deg); }
  50% { transform: rotate(1.2deg); }
  100% { transform: rotate(-1deg); }
}
.preset-row.edit-mode .preset-btn,
.preset-row.dnd-active .preset-btn {
  animation: jiggle 0.22s ease-in-out infinite;
}
.preset-row.edit-mode .preset-btn:nth-child(3n+1),
.preset-row.dnd-active .preset-btn:nth-child(3n+1) { animation-delay: -0.07s; }
.preset-row.edit-mode .preset-btn:nth-child(3n+2),
.preset-row.dnd-active .preset-btn:nth-child(3n+2) { animation-delay: -0.14s; }
.preset-row.edit-mode .preset-btn.dragging,
.preset-row.dnd-active .preset-btn.dragging { animation: none; }

/* ----- Cassette-deck skin picker — replaces the "Appearance" dropdown that
   used to live buried in the account menu. Sits just beneath the player's
   hint-text, styled like a little tape deck: the same LCD-window treatment
   as the dial display, with prev/next arrows to pop in the "next tape"
   (skin). Desktop only — mobile keeps its own skin picker in the account
   panel since it's a different visual system (photoreal receiver skins,
   not this chrome-frame look). ----- */
.cassette-deck {
  display: flex;
  align-items: stretch;
  gap: 6px;
  max-width: 280px;
  margin: 14px auto 0;
}
:root.is-mobile-device .cassette-deck { display: none !important; }
.cassette-arrow {
  flex: none;
  width: 22px;
  background: linear-gradient(180deg, var(--ctrl-grad-1) 0%, var(--ctrl-grad-2) 100%);
  border: 1px solid var(--dial-ring);
  border-radius: 5px;
  color: var(--ctrl-text);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}
.cassette-arrow:hover { background: linear-gradient(180deg, var(--ctrl-hover-1) 0%, var(--ctrl-hover-2) 100%); }
.cassette-arrow:active { box-shadow: inset 0 2px 4px rgba(0,0,0,0.7); }
.cassette-shell {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--lcd-bg);
  border-radius: 5px;
  padding: 5px 10px;
  box-shadow: inset 0 3px 8px rgba(0,0,0,0.9), 0 0 0 1px #000, 0 0 0 2px var(--dial-ring);
}
.cassette-label {
  flex: 1;
  min-width: 0;
  font-family: 'VT323', monospace;
  font-size: 17px;
  letter-spacing: 0.5px;
  color: var(--accent);
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tuner-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-top: 10px;
}
.tuner-counter {
  font-size: 10px;
  letter-spacing: 0.5px;
  color: #6b6d70;
  min-width: 34px;
  text-align: center;
}

.tuner-btn {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: linear-gradient(180deg, #3c3e43 0%, #242527 100%);
  border: none;
  color: #7d8288;
  font-size: 10px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.06),
    inset 0 -1px 2px rgba(0,0,0,0.5);
}
.tuner-btn:hover { color: var(--accent); }
.tuner-btn:active { box-shadow: inset 0 1px 3px rgba(0,0,0,0.7); }

.tuner-add-btn {
  background: none;
  border: none;
  color: #7d8288;
  font-family: 'Oswald', sans-serif;
  font-weight: 500;
  font-size: 10.5px;
  letter-spacing: 0.5px;
  padding: 5px 8px;
  cursor: pointer;
  border-radius: 3px;
}
.tuner-add-btn:hover {
  color: var(--accent);
  background: rgba(46,204,113,0.08);
}
.tuner-stop-btn { color: var(--danger); }
.tuner-stop-btn:hover {
  color: #ff8080;
  background: rgba(255,93,93,0.08);
}

.tuner-save-row {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin-top: 10px;
}
.tuner-save-row input {
  flex: 0 1 200px;
  background: transparent;
  border: none;
  border-bottom: 1px solid #3a3b3e;
  border-radius: 0;
  color: #eee;
  padding: 5px 4px;
  font-size: 12px;
  font-family: 'Oswald', sans-serif;
  text-align: center;
}
.tuner-save-row input:focus { outline: none; border-bottom-color: var(--accent); }

/* ===== Chrome buttons ===== */

.chrome-btn {
  background: linear-gradient(180deg, var(--chrome-1) 0%, var(--chrome-2) 100%);
  border: none;
  border-radius: 4px;
  color: #16171a;
  font-family: 'Oswald', sans-serif;
  font-weight: 600;
  font-size: 13px;
  padding: 8px 14px;
  cursor: pointer;
  box-shadow: 0 1px 0 rgba(255,255,255,0.5) inset, 0 2px 3px rgba(0,0,0,0.4);
}
.chrome-btn:hover { filter: brightness(1.05); }
.chrome-btn:active { filter: brightness(0.95); box-shadow: inset 0 2px 4px rgba(0,0,0,0.3); }
.chrome-btn.small { font-size: 11px; padding: 6px 10px; }
.chrome-btn.primary { background: linear-gradient(180deg, #baffd4, var(--accent)); }
.chrome-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  filter: none;
}

/* ===== Add-station panel ===== */

.panel-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  display: none;
  align-items: center;
  justify-content: center;
  padding: 20px;
  z-index: 10;
}
.panel-backdrop.open { display: flex; }

.add-station-panel {
  background: #26282b;
  border: 1px solid #3a3b3e;
  border-radius: 8px;
  width: 100%;
  max-width: 560px;
  max-height: 86vh;
  overflow-y: auto;
  padding: 18px 20px 20px;
  box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}
/* Only the actual "Add a Station" modal is all-caps — the tuner wizard and info
   panels reuse .add-station-panel for shared card styling but aren't in scope.
   Browsers also reset text-transform on form controls by default, so inheritance
   alone wouldn't reach buttons/inputs/selects even if scoped more broadly. */
#addStationBackdrop .add-station-panel {
  text-transform: uppercase;
}
#addStationBackdrop .chrome-btn,
#addStationBackdrop .tab-btn,
#addStationBackdrop select {
  text-transform: uppercase;
}
/* Placeholder ghost text gets the caps treatment, but the actual <input> does not —
   forcing text-transform on live typed text hides the real casing as you type,
   so what looks fine in the box can silently save in the wrong case. */
#addStationBackdrop input::placeholder { text-transform: uppercase; }

.add-station-panel.tuner-wizard-panel {
  max-width: 320px;
}
/* The account panel reuses .tuner-wizard-panel purely for its card styling,
   not its narrow wizard-step width — it holds password fields, several
   dropdowns with long labels, and invite/reset-request rows with buttons,
   all of which feel cramped at 320px. */
.add-station-panel.account-panel {
  max-width: 440px;
}

.info-list {
  margin: 0;
  padding: 0 0 0 18px;
  list-style: disc;
  color: #ccc;
  font-size: 13px;
  line-height: 1.6;
}
.info-list li { margin-bottom: 10px; }
.info-list li:last-child { margin-bottom: 0; }
.info-list strong { color: var(--accent); }

.wizard-select {
  width: 100%;
  background: #17181a;
  border: 1px solid #3a3b3e;
  border-radius: 4px;
  color: #eee;
  font-family: 'Oswald', sans-serif;
  font-size: 13px;
}
.wizard-select option {
  padding: 5px 8px;
}
.wizard-select:focus { outline: 1px solid var(--accent); }

.tuner-wizard-btn {
  width: 100%;
  margin-top: 10px;
}

.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-weight: 600;
  letter-spacing: 1px;
  margin-bottom: 14px;
  color: var(--chrome-1);
}

.panel-tabs {
  display: flex;
  gap: 6px;
  margin-bottom: 14px;
}

.tab-btn {
  flex: 1;
  background: #303235;
  border: 1px solid #3a3b3e;
  color: #ccc;
  padding: 8px 6px;
  font-size: 12px;
  font-family: 'Oswald', sans-serif;
  font-weight: 600;
  border-radius: 4px;
  cursor: pointer;
}
.tab-btn.active {
  background: var(--accent);
  color: #08170f;
  border-color: var(--accent);
}

.tab-content.hidden { display: none; }
.hidden { display: none !important; }

.tab-hint {
  font-size: 12px;
  color: #9a9a9a;
  margin: 0 0 8px;
  line-height: 1.4;
}

.row-inline {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}
.row-inline input {
  flex: 1;
  background: #17181a;
  border: 1px solid #3a3b3e;
  border-radius: 4px;
  color: #eee;
  padding: 9px 10px;
  font-size: 13px;
  font-family: 'Oswald', sans-serif;
}
.row-inline input:focus { outline: 1px solid var(--accent); }

.row-inline select {
  flex: 1;
  background: #17181a;
  border: 1px solid #3a3b3e;
  border-radius: 4px;
  color: #eee;
  padding: 9px 10px;
  font-size: 13px;
  font-family: 'Oswald', sans-serif;
  min-width: 0;
}
.row-inline select:focus { outline: 1px solid var(--accent); }
.row-inline select.state-select-compact { flex: 0 0 108px; }

.result-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 260px;
  overflow-y: auto;
}

.result-item {
  background: #2c2e31;
  border: 1px solid #343538;
  border-radius: 6px;
  padding: 9px 10px;
  display: flex;
  align-items: center;
  gap: 10px;
}
.result-item img {
  width: 26px;
  height: 26px;
  border-radius: 3px;
  object-fit: cover;
  background: #111;
  flex-shrink: 0;
}
.result-info { flex: 1; min-width: 0; }
.result-name { font-size: 13px; font-weight: 600; color: #eee; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.result-meta { font-size: 11px; color: #999; }
.result-actions { display: flex; gap: 6px; flex-shrink: 0; }

.save-bar {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid #343538;
  display: flex;
  gap: 8px;
}
.save-bar.hidden { display: none; }
.save-bar input {
  flex: 1;
  background: #17181a;
  border: 1px solid #3a3b3e;
  border-radius: 4px;
  color: #eee;
  padding: 9px 10px;
  font-size: 13px;
  font-family: 'Oswald', sans-serif;
}

.status-text {
  font-size: 12px;
  color: #999;
  margin: 4px 0;
}
.status-text.error { color: var(--danger); }

/* iOS Safari auto-zooms on focusing any input/select with font-size under 16px —
   bump mobile form fields to 16px so focusing them never triggers page zoom. */
:root.is-mobile-device .row-inline input,
:root.is-mobile-device .row-inline select,
:root.is-mobile-device .wizard-select,
:root.is-mobile-device .save-bar input,
:root.is-mobile-device .tuner-save-row input,
:root.is-mobile-device .preset-btn .preset-name-input {
  font-size: 16px;
}
/* .row-inline packs a flexible input alongside a fixed-width state select
   and a button — on a narrow phone those don't all fit and overflow the
   panel. Stacking them removes any dependency on exact content widths. */
:root.is-mobile-device .row-inline { flex-direction: column; }
:root.is-mobile-device .row-inline select.state-select-compact { flex: 1 1 auto; width: 100%; }

/* ===== Instructional hint ===== */

.hint-text {
  text-align: center;
  font-size: 9px;
  line-height: 1.5;
  color: var(--ctrl-text);
  letter-spacing: 0.1px;
  margin: 10px 2px 0;
  opacity: 0.4;
}
.hint-text strong { font-weight: 600; }

/* ===== Power state ===== */

.radio-unit.powered-off .lcd-marquee-text,
.radio-unit.powered-off .lcd-scale,
.radio-unit.powered-off .lcd-scale-yt,
.radio-unit.powered-off .lcd-scale-sp,
.radio-unit.powered-off .lcd-scale-indicator {
  opacity: 0.12;
}
.radio-unit.powered-off .knob-col,
.radio-unit.powered-off .tab-row,
.radio-unit.powered-off .preset-row,
.radio-unit.powered-off .tuner-controls,
.radio-unit.powered-off .tuner-save-row,
.radio-unit.powered-off .mobile-seek-btn {
  opacity: 0.25;
  pointer-events: none;
}
/* The power knob/button itself must stay clickable while off — it's the
   only control that turns things back on. */
.radio-unit.powered-off #powerKnob,
.radio-unit.powered-off .mobile-power-btn {
  pointer-events: auto;
}

/* ===== Auth screen ===== */

.auth-screen {
  width: 100%;
  max-width: 420px;
}

.auth-panel {
  padding: 28px 24px 24px;
}

.auth-heading {
  font-family: 'Oswald', sans-serif;
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--accent);
  text-align: center;
  margin: 2px 0 22px;
  padding-bottom: 16px;
  border-bottom: 1px solid rgba(255,255,255,0.06);
}
/* Frosted-glass treatment for the login/register/forgot-password screen
   only — the Change Password form in the account panel reuses these same
   .auth-* classes but keeps its original dark-box look, so everything here
   is scoped under #authScreen rather than touching the base classes. */
#authScreen .auth-heading {
  font-weight: 400;
  font-size: 15px;
  letter-spacing: 0.5px;
  text-transform: none;
  color: rgba(255, 255, 255, 0.85);
  border-bottom-color: rgba(255, 255, 255, 0.15);
}

.auth-field {
  margin-bottom: 14px;
}

.auth-label {
  display: block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: #7d8288;
  margin-bottom: 6px;
}
#authScreen .auth-label {
  text-transform: none;
  font-weight: 500;
  letter-spacing: 0.2px;
  font-size: 11px;
  color: rgba(255, 255, 255, 0.65);
}

.auth-input {
  display: block;
  width: 100%;
  background: #17181a;
  border: 1px solid #3a3b3e;
  border-radius: 4px;
  color: #eee;
  padding: 10px 12px;
  font-size: 13px;
  font-family: 'Oswald', sans-serif;
}
.auth-input:focus { outline: 1px solid var(--accent); }
:root.is-mobile-device .auth-input { font-size: 16px; }
#authScreen .auth-input {
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 8px;
  color: #fff;
}
#authScreen .auth-input::placeholder { color: rgba(255, 255, 255, 0.4); }
#authScreen .auth-input:focus { outline: none; border-color: rgba(255, 255, 255, 0.7); }

.auth-submit {
  width: 100%;
  margin-top: 8px;
  padding: 11px 14px;
  font-size: 13px;
  letter-spacing: 0.5px;
}
/* Overrides .chrome-btn.primary's green gradient just on the login/register/
   forgot-password screen — the same combo class is reused for the account
   panel's "Update Password" button, which keeps the app's normal green. */
#authScreen .chrome-btn.primary {
  background: rgba(255, 255, 255, 0.16);
  border: 1px solid rgba(255, 255, 255, 0.5);
  color: #fff;
}

.auth-error {
  color: var(--danger);
  font-size: 12px;
  text-align: center;
  margin: 12px 0 0;
}

.auth-switch {
  text-align: center;
  font-size: 12px;
  color: #8a8d92;
  margin: 22px 0 0;
  padding-top: 18px;
  border-top: 1px solid rgba(255,255,255,0.06);
}
.auth-switch-secondary {
  margin-top: 10px;
  padding-top: 0;
  border-top: none;
}
#authScreen .auth-switch {
  color: rgba(255, 255, 255, 0.6);
  border-top-color: rgba(255, 255, 255, 0.15);
}
#authScreen .link-btn { color: #fff; text-decoration: underline; }
#authScreen .link-btn:hover { color: rgba(255, 255, 255, 0.75); }

.auth-status {
  font-size: 12px;
  text-align: center;
  color: var(--accent);
  margin: 12px 0 0;
  line-height: 1.5;
}
#authScreen .auth-status { color: #fff; }

/* ===== Frosted-glass panels: Add a Station, its state-picker sub-step,
   the station-down replacement picker, and the Account/profile panel — all
   reuse the exact login-screen palette (rgba-white glass, translucent
   inputs, white/transparent buttons) instead of the app's usual dark+green
   card, so every panel that floats over the player reads as one calmer,
   glassy family. Applies equally on mobile (no is-mobile-device gate) since
   nothing here is layout, only color. */
#addStationBackdrop .add-station-panel,
#tunerWizardBackdrop .add-station-panel,
#stationDownBackdrop .add-station-panel,
#accountBackdrop .add-station-panel {
  background: rgba(255, 255, 255, 0.09);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 18px;
}
#addStationBackdrop .panel-header,
#tunerWizardBackdrop .panel-header,
#stationDownBackdrop .panel-header,
#accountBackdrop .panel-header {
  color: rgba(255, 255, 255, 0.85);
}
#addStationBackdrop .tab-hint,
#tunerWizardBackdrop .tab-hint,
#stationDownBackdrop .tab-hint,
#accountBackdrop .tab-hint,
#addStationBackdrop .status-text,
#tunerWizardBackdrop .status-text,
#stationDownBackdrop .status-text {
  color: rgba(255, 255, 255, 0.65);
}
#addStationBackdrop .row-inline input,
#addStationBackdrop .row-inline select,
#addStationBackdrop .save-bar input,
#addStationBackdrop .wizard-select,
#tunerWizardBackdrop .wizard-select,
#accountBackdrop .wizard-select,
#accountBackdrop .auth-input {
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 8px;
  color: #fff;
}
/* The white text above applies to the closed select box, which sits on the
   frosted panel — but an open dropdown's <option> list is drawn by the OS
   on its own (light) background, where inherited white text is invisible
   until highlighted. These need their own explicit dark-on-light colors. */
#addStationBackdrop .row-inline select option,
#addStationBackdrop .wizard-select option,
#tunerWizardBackdrop .wizard-select option,
#accountBackdrop .wizard-select option {
  background: #26282b;
  color: #f0efec;
}
#addStationBackdrop .row-inline input::placeholder,
#addStationBackdrop .save-bar input::placeholder,
#accountBackdrop .auth-input::placeholder {
  color: rgba(255, 255, 255, 0.4);
}
#addStationBackdrop .row-inline input:focus,
#addStationBackdrop .row-inline select:focus,
#addStationBackdrop .save-bar input:focus,
#addStationBackdrop .wizard-select:focus,
#tunerWizardBackdrop .wizard-select:focus,
#accountBackdrop .wizard-select:focus,
#accountBackdrop .auth-input:focus {
  outline: none;
  border-color: rgba(255, 255, 255, 0.7);
}
#addStationBackdrop .tab-btn {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.2);
  color: rgba(255, 255, 255, 0.7);
}
#addStationBackdrop .tab-btn.active {
  background: rgba(255, 255, 255, 0.85);
  color: #17181a;
  border-color: rgba(255, 255, 255, 0.85);
}
#addStationBackdrop .chrome-btn,
#tunerWizardBackdrop .chrome-btn,
#stationDownBackdrop .chrome-btn,
#accountBackdrop .chrome-btn {
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.35);
  color: #fff;
  box-shadow: none;
}
#addStationBackdrop .chrome-btn:hover,
#tunerWizardBackdrop .chrome-btn:hover,
#stationDownBackdrop .chrome-btn:hover,
#accountBackdrop .chrome-btn:hover {
  background: rgba(255, 255, 255, 0.2);
  filter: none;
}
#addStationBackdrop .chrome-btn.primary,
#tunerWizardBackdrop .chrome-btn.primary,
#accountBackdrop .chrome-btn.primary {
  background: rgba(255, 255, 255, 0.16);
  border: 1px solid rgba(255, 255, 255, 0.5);
  color: #fff;
}
#accountBackdrop .account-logout-btn { background: rgba(255, 255, 255, 0.12); }
#addStationBackdrop .result-item,
#stationDownBackdrop .result-item {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.15);
}
#addStationBackdrop .result-name,
#stationDownBackdrop .result-name { color: #fff; }
#addStationBackdrop .result-meta,
#stationDownBackdrop .result-meta { color: rgba(255, 255, 255, 0.55); }
#addStationBackdrop .save-bar { border-top-color: rgba(255, 255, 255, 0.15); }
#accountBackdrop .panel-divider { border-top-color: rgba(255, 255, 255, 0.15); }
#accountBackdrop .link-btn { color: #fff; text-decoration: underline; }
#accountBackdrop .link-btn:hover { color: rgba(255, 255, 255, 0.75); }
#accountBackdrop .account-username { color: rgba(255, 255, 255, 0.65); }
#accountBackdrop .account-username strong,
#accountBackdrop .auth-success { color: #fff; }
#accountBackdrop .invite-row {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.15);
}
#accountBackdrop .sfx-toggle-switch input:checked + .sfx-toggle-track { background: rgba(255, 255, 255, 0.85); }
#accountBackdrop .sfx-toggle-switch input:checked + .sfx-toggle-track::after { background: #17181a; }
#accountBackdrop .info-list { color: rgba(255, 255, 255, 0.65); }
#accountBackdrop .info-list strong { color: #fff; }

.link-btn {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font-family: inherit;
  font-size: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.link-btn:hover { color: #baffd4; }

/* ===== Account panel ===== */

.account-username {
  font-size: 13px;
  color: #ccc;
  margin: 0 0 12px;
}
.account-username strong { color: var(--accent); }

.account-logout-btn {
  width: 100%;
  background: linear-gradient(180deg, #484b50 0%, #2a2c2f 100%);
  color: #ffb3b3;
}

.panel-divider {
  border: none;
  border-top: 1px solid #3a3b3e;
  margin: 18px 0 14px;
}

.account-toggle-btn {
  display: block;
  font-size: 12px;
}

#changePasswordForm {
  margin-top: 14px;
}

.auth-success {
  color: var(--accent);
  font-size: 12px;
  text-align: center;
  margin: 12px 0 0;
}

.invite-header { margin-bottom: 6px; }

.invite-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 200px;
  overflow-y: auto;
  margin-top: 10px;
}

.invite-row {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #2c2e31;
  border: 1px solid #343538;
  border-radius: 6px;
  padding: 8px 10px;
}

.invite-code {
  flex: 1;
  font-family: 'VT323', monospace;
  font-size: 16px;
  letter-spacing: 1px;
  color: var(--accent);
}

.invite-status {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--accent);
}
.invite-status.used { color: #888; }

.invite-empty {
  color: #777;
  font-size: 12px;
  padding: 10px 4px;
  text-align: center;
}

/* ===== Appearance / sound settings ===== */

.settings-select {
  margin-top: 10px;
  padding: 9px 10px;
}
/* Mobile has no accessory rendering at all (the player fills the whole
   screen, no room for a hanging freshener) — the picker would just be dead
   weight there. */
:root.is-mobile-device #accessorySection { display: none; }
/* Desktop's skin picker moved out of this menu entirely — see .cassette-deck
   in the player itself. Mobile keeps this dropdown; it's a different skin
   system (photoreal receiver images) with nowhere else to live yet. */
:root:not(.is-mobile-device) #appearanceSettingsSection { display: none; }
/* The hardware skin fills the entire screen with no page background ever
   visible behind it, so the old gradient/photo background picker has
   nothing to show on mobile — same treatment as accessories above. */
:root.is-mobile-device #backgroundSection { display: none; }

/* Hangs from an implied rearview mirror above the unit.

   This used to make room for itself by growing .unit-wrap's top padding from
   4px to 148px, which shoved the whole player down the screen every time an
   accessory was picked. There is already empty space between the accessory
   strip and the player, so the freshener now hangs up into that instead: it
   is pulled fully above its own top edge with translateY(-100%), which takes
   it out of the layout's way entirely. The player does not move.

   The -100% lives in the sway keyframes rather than in `transform` here,
   because an animation replaces the whole property — a transform declared on
   the rule would simply be overridden the moment the sway starts. */
.unit-wrap { padding-top: 4px; }
.accessory-hang {
  display: none;
  position: absolute;
  bottom: 100%;      /* sit on the frame's top edge... */
  margin-bottom: 4px; /* ...with a little daylight under it */
  left: 50%;
  width: 130px;
  height: auto;
  transform-origin: 50% -15px;
  z-index: 3;
  filter: drop-shadow(0 6px 8px rgba(0,0,0,0.45));
  animation: accessory-sway 4.5s ease-in-out infinite;
}
.unit-wrap.has-accessory .accessory-hang { display: block; }
@keyframes accessory-sway {
  0%, 100% { transform: translateX(-50%) rotate(-4deg); }
  50% { transform: translateX(-50%) rotate(4deg); }
}
@media (prefers-reduced-motion: reduce) {
  .accessory-hang { animation: none; }
}
/* Doesn't read well on the smaller mobile chassis — desktop-only for now. */
:root.is-mobile-device .accessory-hang { display: none !important; }
.accessory-hang { cursor: pointer; }

/* ----- Accessory picker strip — pinned to the very top of the viewport
   (not the unit itself, so it stays put regardless of how the player below
   is sized/centered), always visible rather than tucked in a settings menu.
   Only ever lists accessories NOT currently hanging — the active one is
   the one actually hanging on the unit, clicking it there un-hangs it and
   returns it here (see #accessoryHang's click handler in app.js). ----- */
.accessory-strip-wrap {
  position: fixed;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  /* Below .panel-backdrop (10): these strips are page furniture and must
     never sit on top of an open dialog. */
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.accessory-strip-wrap.hidden { display: none; }
:root.is-mobile-device .accessory-strip-wrap { display: none !important; }
/* Groovy 1970s marquee — Lobster reads as hand-painted van lettering, which
   is the whole point of calling out the fresheners instead of just showing
   the strip on its own. */
.accessory-strip-label {
  font-family: 'Lobster', 'Segoe UI', cursive;
  font-size: 21px;
  line-height: 1;
  color: #fff;
  text-shadow: 0 2px 5px rgba(0,0,0,0.65);
  transform: rotate(-2deg);
  pointer-events: none;
}
.accessory-strip {
  display: flex;
  gap: 12px;
}
.accessory-strip-item {
  width: 46px;
  height: 46px;
  padding: 4px;
  border-radius: 9px;
  border: 1px solid rgba(255,255,255,0.12);
  background-color: rgba(20, 20, 22, 0.55);
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  cursor: pointer;
  backdrop-filter: blur(2px);
  transition: transform 0.12s ease, border-color 0.12s ease;
}
.accessory-strip-item:hover {
  border-color: rgba(255,255,255,0.3);
  transform: translateY(1px) scale(1.05);
}

/* ----- Background picker strip — same "always visible, no menu" idea as
   the accessory strip above, pinned to the bottom of the viewport instead
   of the top. Each thumbnail previews its actual photo (background-image),
   the active one gets a lit border, and the last tile is always the "+"
   upload option rather than a separate button buried in a menu. ----- */
.background-strip-wrap {
  position: fixed;
  bottom: 14px;
  left: 50%;
  transform: translateX(-50%);
  /* Below .panel-backdrop (10): these strips are page furniture and must
     never sit on top of an open dialog. */
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  max-width: 92vw;
}
.background-strip-wrap.hidden { display: none; }
:root.is-mobile-device .background-strip-wrap { display: none !important; }
/* Matches .hint-text's own styling exactly (inherits the same Oswald body
   font) — same quiet, informational voice as the blurb below the player. */
.background-strip-label {
  font-size: 9px;
  letter-spacing: 0.1px;
  color: var(--accent);
  opacity: 0.4;
  text-align: center;
}
.background-strip {
  display: flex;
  gap: 8px;
  max-width: 92vw;
  overflow-x: auto;
  padding: 2px;
}
.background-strip-error {
  position: fixed;
  bottom: 62px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  background: rgba(20,20,22,0.85);
  padding: 4px 10px;
  border-radius: 4px;
  white-space: nowrap;
}
:root.is-mobile-device .background-strip-error { display: none !important; }
.background-strip-item {
  flex: none;
  width: 56px;
  height: 40px;
  border-radius: 6px;
  border: 2px solid rgba(255,255,255,0.15);
  background-color: rgba(20, 20, 22, 0.55);
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
  cursor: pointer;
  transition: transform 0.12s ease, border-color 0.12s ease;
  position: relative;
  overflow: hidden;
}
.background-strip-item:hover {
  border-color: rgba(255,255,255,0.4);
  transform: translateY(-2px);
}
.background-strip-item.active {
  border-color: var(--accent);
  box-shadow: 0 0 8px rgba(46,204,113,0.6);
}
/* These two tiles keep a fixed dark background regardless of skin (unlike
   real controls, which sit on the skin's own light/dark face) — so their
   text/icon needs a fixed light color too, not --ctrl-text (which flips
   dark on chrome/wood to stay readable against THEIR light surfaces, and
   would go invisible here). */
.background-strip-item.none-item {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #e8e6e2;
  font-size: 9px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  background-image: none !important;
}
.background-strip-item.upload-item {
  display: flex;
  align-items: center;
  justify-content: center;
  background-image: none !important;
  font-size: 20px;
  color: #e8e6e2;
  line-height: 1;
}
.background-strip-item.upload-item::after {
  content: '+';
}
/* A synthetic option rather than one specific photo, so it gets a swatch
   sweeping through the four times of day it actually cycles between
   (dawn/day/dusk/night) instead of a single freeze-framed thumbnail. */
.background-strip-item.daycycle-item {
  display: flex;
  align-items: center;
  justify-content: center;
  background-image: none !important;
  background: linear-gradient(90deg, #f4a261 0%, #4fc3f7 34%, #6b4a8a 67%, #0b0f22 100%);
  color: #fff;
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  text-shadow: 0 1px 2px rgba(0,0,0,0.6);
}
.background-strip-remove {
  position: absolute;
  top: 1px;
  right: 1px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.65);
  color: #fff;
  font-size: 11px;
  line-height: 14px;
  text-align: center;
  cursor: pointer;
}
.background-strip-remove:hover {
  background: rgba(200, 40, 40, 0.85);
}

/* Groups the holiday photos behind one tile instead of flooding the main
   strip for the ~3 months a season is active — its thumbnail is just
   whichever active seasonal entry happens to be first, with a scrim + label
   over top so it still reads as a category rather than a single photo. */
.background-strip-item.seasonal-item::after {
  content: attr(data-tile-label);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  font-family: 'Oswald', sans-serif;
  font-size: 7.5px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-align: center;
}
/* Opens upward from the seasonal tile as a vertical column of that season's
   thumbnails — .background-strip-wrap is already `position: fixed`, so it's
   the containing block this sits against regardless of where the tile
   itself scrolled to inside the strip. */
.seasonal-flyout {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: rgba(20, 20, 22, 0.9);
  padding: 8px;
  border-radius: 8px;
  border: 1px solid rgba(255,255,255,0.15);
  backdrop-filter: blur(4px);
  max-height: 60vh;
  overflow-y: auto;
  z-index: 21;
}
.seasonal-flyout.hidden { display: none; }

/* Names which set is open — four tiles share this one flyout. */
.seasonal-flyout-label {
  font-family: 'Oswald', sans-serif;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.75);
  text-align: center;
  padding-bottom: 2px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.15);
  margin-bottom: 2px;
}

/* ===== Strip picker =====
   The team picker, anchored under
   whichever opened it. Fixed-position so it escapes the LCD's clipping, and
   styled as a piece of the chassis rather than a browser dialog. */
.strip-picker {
  position: fixed;
  /* Above the strips (5), below .panel-backdrop (10) — opening a dialog
     closes the picker anyway, so it must never cover one. */
  z-index: 8;
  width: 288px;
  max-height: min(60vh, 420px);
  display: flex;
  flex-direction: column;
  background: var(--panel-bg, #202325);
  border-radius: 10px;
  padding: 10px 10px 8px;
  box-shadow: 0 22px 46px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(0, 0, 0, 0.45);
  font-family: 'Oswald', sans-serif;
  color: #e8ecea;
}
.strip-picker.hidden { display: none; }
.strip-picker-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.strip-picker-title {
  font-size: 12px; letter-spacing: 1.4px; text-transform: uppercase; color: rgba(255,255,255,0.72);
}
.strip-picker-close {
  background: none; border: 0; color: rgba(255,255,255,0.55);
  font-size: 20px; line-height: 1; cursor: pointer; padding: 0 2px;
}
.strip-picker-close:hover { color: #fff; }
.strip-picker-tabs { display: flex; gap: 4px; margin: 8px 0 6px; }
.pick-tab {
  flex: 1 1 0; padding: 5px 4px; cursor: pointer;
  font-family: 'Oswald', sans-serif; font-size: 11px; letter-spacing: 1px; text-transform: uppercase;
  color: rgba(255,255,255,0.6); background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.08); border-radius: 5px;
}
.pick-tab.on { background: var(--lcd-fg); border-color: var(--lcd-fg); color: #0c0904; }
.pick-tab-fav { flex: 0 0 34px; font-size: 13px; }
.strip-picker-search {
  width: 100%; margin-bottom: 6px; padding: 6px 8px;
  font-family: 'Oswald', sans-serif; font-size: 13px;
  color: #fff; background: rgba(0,0,0,0.3);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 5px;
}
.strip-picker-search::placeholder { color: rgba(255,255,255,0.35); }
.strip-picker-list { overflow-y: auto; flex: 1 1 auto; min-height: 0; }
.pick-row { display: flex; align-items: stretch; gap: 2px; }
.pick-main {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline; gap: 7px;
  text-align: left; padding: 6px 7px; cursor: pointer;
  background: none; border: 0; border-radius: 5px;
  font-family: 'Oswald', sans-serif; font-size: 13px; color: #dfe5e2;
}
.pick-main:hover { background: rgba(255,255,255,0.07); }
.pick-row.chosen .pick-main { background: rgba(46, 204, 113, 0.16); color: #fff; }
.pick-abbrev {
  flex: 0 0 34px; font-size: 11px; letter-spacing: 0.8px;
  color: var(--lcd-fg); font-variant-numeric: tabular-nums;
}
.pick-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-sub { flex: 0 0 auto; font-size: 11px; color: rgba(255,255,255,0.45); }
.pick-star {
  flex: 0 0 26px; background: none; border: 0; cursor: pointer;
  color: rgba(255,255,255,0.3); font-size: 15px; line-height: 1;
}
.pick-star.on { color: #f5c451; }
.pick-star:hover { color: #f5c451; }
.pick-empty { margin: 10px 4px; font-size: 12px; color: rgba(255,255,255,0.45); }
.pick-main:focus-visible, .pick-star:focus-visible, .pick-tab:focus-visible,
.strip-picker-close:focus-visible { outline: 2px solid var(--lcd-fg); outline-offset: -2px; }

/* ===== White noise panel =====
   A second, narrower cabinet that slides out from behind the player's right
   edge. It's a sibling of .chrome-frame (not inside it — that clips) and sits
   a layer below, so the "from behind" read is real rather than simulated.
   Every surface here pulls from the same skin variables as the player, so it
   re-skins along with it for free. Desktop only: mobile's layout is
   full-bleed with nowhere to slide out to. */
.chrome-frame { z-index: 2; }

.wn-panel {
  position: absolute;
  /* Clears .unit-wrap's 4px top padding so the panel lines up with the frame.
     This tracked a live variable while that padding grew to make room for a
     hung accessory; the accessory now hangs above the layout instead, so the
     padding is a constant again and so is this. */
  top: 4px;
  left: 100%;
  height: calc(100% - 4px);
  width: 132px;
  z-index: 1;
  /* The hairline gap the panel settles into once open. */
  margin-left: 4px;
  background: var(--frame-bg);
  border-radius: 12px;
  padding: 5px;
  box-shadow: 0 24px 48px rgba(0,0,0,0.55), 0 0 0 1px rgba(0,0,0,0.4);
  /* Parked fully behind the player, offset back by its own width plus the
     gap, so nothing peeks out until it's opened. */
  transform: translateX(calc(-100% - 4px));
  opacity: 0;
  transition: transform 0.52s cubic-bezier(0.22, 0.9, 0.3, 1), opacity 0.2s ease 0.04s;
  pointer-events: none;
}
.wn-panel.open {
  transform: translateX(0);
  opacity: 1;
  pointer-events: auto;
}
@media (prefers-reduced-motion: reduce) {
  .wn-panel { transition: opacity 0.15s ease; }
}
.wn-face {
  background: var(--panel-bg);
  border-radius: var(--panel-radius);
  height: 100%;
  padding: 10px 9px 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* The panel is exactly as tall as the player, whose height isn't fixed
     (the tuner rows appear and disappear). Rather than hand-picking button
     sizes that fit today's height and overflow at some other one, the five
     buttons share whatever is left after the title and knob have taken
     theirs — so the knob is always fully on the panel. */
  overflow: hidden;
}
.wn-title {
  font-size: 9px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: #8b8f94;
  text-align: center;
}
.wn-btn {
  /* flex-basis 0 so all five divide the leftover space evenly rather than
     each demanding its content height and pushing the knob off the end. */
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(180deg, var(--ctrl-grad-1) 0%, var(--ctrl-grad-2) 100%);
  border: 1px solid var(--dial-ring);
  border-radius: 5px;
  color: var(--ctrl-text);
  font-family: 'Oswald', sans-serif;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2px;
  line-height: 1.1;
  padding: 4px 3px;
  cursor: pointer;
}
.wn-btn:hover { background: linear-gradient(180deg, var(--ctrl-hover-1) 0%, var(--ctrl-hover-2) 100%); }
.wn-btn:active { transform: translateY(1px); }
.wn-btn.playing {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 0 8px var(--accent-glow);
  color: #dff7e8;
}
.wn-knob-col {
  /* Never squeezed — the buttons above give way instead. */
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding-top: 4px;
}
.knob-chrome.wn-knob { width: 46px; height: 46px; }
.knob-chrome.wn-knob .knob-dome { inset: 7px; }
.wn-knob-col .knob-label { font-size: 8.5px; }

/* The toggle badge, top-right of the player's face — same 18px as the
   account/edit badges in the opposite corner. */
.corner-badge.wn-toggle {
  position: absolute;
  top: 9px;
  right: 10px;
}
.corner-badge.wn-toggle svg { width: 11px; height: 11px; color: var(--ctrl-text); }

:root.is-mobile-device .wn-panel,
:root.is-mobile-device .corner-badge.wn-toggle { display: none !important; }

.sfx-toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 10px;
  font-size: 13px;
  color: #ccc;
  cursor: pointer;
}

.sfx-toggle-switch {
  position: relative;
  display: inline-block;
  width: 38px;
  height: 22px;
  flex-shrink: 0;
}
.sfx-toggle-switch input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
  z-index: 1;
}
.sfx-toggle-track {
  position: absolute;
  inset: 0;
  background: #3a3b3e;
  border-radius: 999px;
  transition: background 0.15s;
}
.sfx-toggle-track::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #cfd3d8;
  box-shadow: 0 1px 2px rgba(0,0,0,0.5);
  transition: transform 0.15s;
}
.sfx-toggle-switch input:checked + .sfx-toggle-track { background: var(--accent); }
.sfx-toggle-switch input:checked + .sfx-toggle-track::after { transform: translateX(16px); background: #08170f; }

/* ===== Mobile, rotated to landscape ===== */
/* Portrait stacks everything in one column; landscape has the opposite
   problem (width to spare, not much height), so it re-flows into a real
   bespoke layout instead of just letting the portrait card scroll: the
   wordmark gets its own full-width row up top, then power/seek sit in a
   rail to the RIGHT of the marquee only (stacked vertically — power on
   top, then the two seek arrows keeping their natural left/right-pointing
   glyphs, not rotated) — not a full-height sidebar, so band/page/add and
   the preset grid both get the full screen width below it. Nothing here
   is a new element — .main-row and .dial-col (pure structural wrappers,
   not styled themselves) are set to display:contents, which erases their
   own box but keeps their real children (brand-plate, dial-assembly,
   mobile-ctrl-row, tab-row, preset-row-2col) in place, so they can all
   become direct grid items of .radio-unit and get placed exactly where
   this layout needs them — no HTML changes, no JS changes (12 presets is
   a fixed constant regardless of column count, so the existing
   minPresetSlots()/presetColumns() logic doesn't need to know landscape
   uses 4 columns instead of 3). This block must stay last in the file so
   it wins source-order ties against the portrait mobile rules above. */
@media (orientation: landscape) {
  /* .stage's 600px cap exists for desktop's wide monitors — plenty of
     landscape phones are wider than that (667px+), so left unchecked it
     left real empty gutters on both sides instead of using the screen. */
  :root.is-mobile-device .stage { max-width: none; }

  :root.is-mobile-device .main-row,
  :root.is-mobile-device .dial-col {
    display: contents;
  }
  :root.is-mobile-device .radio-unit {
    display: grid;
    grid-template-columns: 1fr 44px;
    grid-template-rows: auto auto auto 1fr;
    grid-template-areas:
      "brand brand"
      "marquee rail"
      "tabs tabs"
      "presets presets";
    column-gap: 10px;
    row-gap: 6px;
    padding: calc(env(safe-area-inset-top, 0px) + 6px) calc(env(safe-area-inset-right, 0px) + 12px)
      calc(env(safe-area-inset-bottom, 0px) + 8px) calc(env(safe-area-inset-left, 0px) + 12px);
  }

  /* ----- Brand: its own row, full width, above everything else ----- */
  :root.is-mobile-device .brand-plate { grid-area: brand; margin-bottom: 0; }
  :root.is-mobile-device .brand { font-size: 15px; }

  /* ----- Rail: power on top, then the two seek arrows in their natural
     DOM order (prev, next) — with power pulled to the front via order,
     that reads as power / seek-left / seek-right top-to-bottom, arrows
     still pointing left and right rather than rotated. All three the same
     size, and small enough that this rail doesn't force the marquee row
     next to it any taller than the marquee itself actually needs. ----- */
  :root.is-mobile-device .mobile-ctrl-row {
    grid-area: rail;
    flex-direction: column;
    align-self: stretch;
    justify-content: center;
    gap: 5px;
    margin: 0;
  }
  /* Same rounded-rectangle shape as portrait and as the tab row, but kept as a
     vertical rail beside the marquee rather than given a row of its own. A row
     here would cost about 10px off every preset row; the rail uses height the
     marquee row already occupies, so the grid stays exactly as deep as it is
     today. */
  :root.is-mobile-device .mobile-seek-btn,
  :root.is-mobile-device .mobile-power-btn {
    width: 100%;
    height: 30px;
    border-radius: 9px;
    font-size: 11px;
    flex: 0 0 30px;
  }
  :root.is-mobile-device #mobilePowerBigBtn { order: -1; }

  /* ----- Marquee: same three lanes as portrait, shorter than before —
     it only has to be as tall as the (now much leaner) rail beside it. */
  :root.is-mobile-device .dial-assembly {
    grid-area: marquee;
    padding: 6px 12px;
    min-height: 0;
  }
  :root.is-mobile-device .marquee-util { flex: 0 0 42px; gap: 3px; padding-right: 7px; }
  :root.is-mobile-device .marquee-util .mobile-time { font-size: 14px; }
  :root.is-mobile-device .marquee-util .mobile-weather-icon { width: 15px; height: 15px; }
  :root.is-mobile-device .marquee-util .mobile-weather-temp { font-size: 11px; }
  :root.is-mobile-device .lcd-marquee-text { font-size: 20px; }
  :root.is-mobile-device .lcd-marquee-sub { font-size: 12px; }
  :root.is-mobile-device .mobile-marquee-badges { flex: 0 0 30px; gap: 5px; }
  :root.is-mobile-device .mobile-marquee-badges .corner-badge,
  :root.is-mobile-device .mobile-marquee-badges .theme-toggle-badge {
    width: 22px;
    height: 22px;
    font-size: 9px;
  }

  /* ----- Band / Page / Add: full width, not offset by the rail ----- */
  :root.is-mobile-device .tab-row { grid-area: tabs; margin-bottom: 0; }
  :root.is-mobile-device .deck-tab-btn,
  :root.is-mobile-device .page-toggle { padding: 2px; }
  :root.is-mobile-device .band-label-mobile { padding: 5px 0; }

  /* ----- Presets: full width now too — wider-than-tall grid (4x3) to
     match the aspect ratio, instead of portrait's 3x4. Still exactly 12
     slots either way, and still flex/grid-stretched to fill whatever
     height is actually left, so it can never force a scroll. Tighter
     padding/type than portrait's cards — landscape hands presets less
     height per row (3 rows instead of 4, but also a header row above
     them), so the 3 lines of text need to fit a shorter card. */
  :root.is-mobile-device .preset-row-2col {
    grid-area: presets;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-template-rows: repeat(3, minmax(0, 1fr));
  }
  :root.is-mobile-device .preset-btn { padding: 4px 4px; font-size: 11px; }
  :root.is-mobile-device .preset-btn .preset-fullname { font-size: 9px; line-height: 1.15; }
}


/* Spotify lives in the account panel but is a desktop-only feature: the band
   it feeds isn't part of the mobile layout, and the Web Playback SDK wants a
   DRM-capable desktop browser. Hidden rather than absent so the markup can
   stay in one place. */
:root.is-mobile-device .spotify-section { display: none; }
