/* DO PE Remote — dark, metal, one screen, no images, no fonts, no external requests.
 *
 * The buttons are metal since 2026-11: a translucent light-falls-from-the-top
 * sheen over a vertical gradient, so no pixel carries exactly the base colour
 * any more (tests/render-pixels.py matches by colour RANGE for that reason).
 * Everything structural below is unchanged:
 *
 * Sizing rule, because this must never scroll or clip: .pad is a grid that is
 * exactly one viewport tall (100dvh, so a mobile URL bar cannot make the page
 * taller than the screen) with rows [nav] [view] [status], where the view row is
 * minmax(0, 1fr) — it can shrink to whatever is left. The remote view inside it
 * is [volume] [D-pad] [Back] with the D-pad row minmax(0, 1fr), and the D-pad's
 * own rows are minmax(0, 1fr) too. Buttons therefore scale to the viewport
 * instead of deciding its height.
 *
 * The first version used aspect-ratio: 1/1 on the D-pad. That made its height come
 * from its WIDTH, which on a portrait phone is taller than the space left over, so
 * overflow:hidden clipped the Back button off the bottom — the "overflows and
 * clips" that was reported. Nothing here may size an element from the other axis. */
:root {
  --bg: #0b0b0d;
  --fg: #e8e8ea;
  --btn: #1c1c22;                 /* base tone of the primary button metal */
  --btn-hi: #33333e;              /* light strikes the top edge ... */
  --btn-lo: #0f0f14;              /* ...and falls off the bottom */
  --btn2-hi: #3d3d49;             /* the secondary family (Back, view tab,
                                     presets) sits a shade lighter */
  --btn2-lo: #13131a;
  --accent: #d8b341;              /* gold, for the active states */
  --accent-hi: #f4dc8e;
  --accent-lo: #8a6a1c;
  --fail: #e0533d;
  --gap: clamp(.4rem, 1.4vh, .9rem);
  /* The metal itself: one sheen layer shared by every family, composed over a
     per-family gradient. Custom properties hold whole <image> values, so each
     button class just says `background: var(--btn-metal)`. The sheen is what
     makes it read as brushed metal rather than a flat fade: a bright top edge,
     a quick falloff, then a darkening that pools at the bottom rim. */
  --sheen: linear-gradient(180deg,
      rgba(255, 255, 255, .16) 0%, rgba(255, 255, 255, .05) 16%,
      rgba(255, 255, 255, 0)   40%, rgba(0, 0, 0, .10) 68%,
      rgba(0, 0, 0, .34)      100%);
  --btn-metal: var(--sheen),
               linear-gradient(180deg, var(--btn-hi), var(--btn) 52%, var(--btn-lo));
  --btn-metal-down: var(--sheen),
               linear-gradient(180deg, var(--btn), var(--btn-lo) 72%);
  --btn2-metal: var(--sheen),
               linear-gradient(180deg, var(--btn2-hi), #23232b 52%, var(--btn2-lo));
  --accent-metal: var(--sheen),
               linear-gradient(180deg, var(--accent-hi), var(--accent) 52%, var(--accent-lo));
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--fg);
  font: 1rem/1.3 system-ui, sans-serif;
  overflow: hidden;                      /* one screen, never scrollable */
  overscroll-behavior: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

.hidden { display: none !important; }

/* --- one metal skin for every button on the page --------------------------- */
/* Composed from the tokens in :root: sheen over gradient. The secondary family
   (Back, view tab, presets) overrides to --btn2-metal below; the active states
   go gold. Box shadows do the rest of the metallic read: a bright top edge
   (light catching the rim), a dark bottom rim, and a soft drop so the button
   sits ABOVE the page instead of being painted on it. */
.navbtn, .dbtn, .vbtn, .mbtn, .backbtn, .pbtn {
  background: var(--btn-metal);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10),
              inset 0 -1px 0 rgba(0, 0, 0, .50),
              0 2px 5px rgba(0, 0, 0, .45);
}

.pad {
  height: 100vh;                          /* fallback, then the real thing */
  height: 100dvh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;   /* nav, view, status */
  justify-items: stretch;
  gap: var(--gap);
  padding: var(--gap);
  padding-top: calc(var(--gap) + env(safe-area-inset-top, 0px));
  padding-bottom: calc(var(--gap) + env(safe-area-inset-bottom, 0px));
  padding-left: calc(var(--gap) + env(safe-area-inset-left, 0px));
  padding-right: calc(var(--gap) + env(safe-area-inset-right, 0px));
}

/* --- the two views share one grid row; exactly one is ever visible ---------- */
.view {
  min-height: 0;                          /* the difference between fit and clip */
  display: grid;
  gap: var(--gap);
  justify-items: stretch;
}

#remoteView { grid-template-rows: auto minmax(0, 1fr) auto; }  /* vol, pad, back */

/* Thumb-sized targets in explicit rows: fixed heights, not 1fr, so a tall phone
   does not turn them into quarter-screen targets. The ONE exception is the eighth
   row, the equaliser's band list: four volume rows plus a separator plus one row
   of presets plus ten band rows cannot fit any phone, so the list is minmax(0,1fr)
   and is what scrolls (like the library list in the Flix view — the page itself
   still never does). */
.mixer {
  grid-template-rows: repeat(4, auto) auto auto minmax(0, 1fr) auto;
}

.mixrow {
  display: grid;
  grid-template-columns: minmax(4rem, 1fr) auto minmax(4rem, auto) auto;
  align-items: center;
  gap: var(--gap);
}

/* A level that cannot be read (nothing playing, app not running) is dimmed.
   It is NOT shown as 0 %, which would read as "muted", and the buttons stay
   live so a press reports the reason instead of looking broken. */
.mixrow.off { opacity: .45; }

.mixname {
  font-size: clamp(.8rem, 2.2vh, 1rem);
  font-weight: 600;
  opacity: .9;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.mbtn {
  width: clamp(3rem, 13vw, 3.75rem);
  height: clamp(2.5rem, 7vh, 3.25rem);
  border: 0;
  border-radius: 12px;
  color: var(--fg);
  font-size: clamp(1.1rem, 3.2vh, 1.6rem);
  line-height: 1;
  touch-action: manipulation;
}

.mixread { min-width: 4rem; height: clamp(2.5rem, 7vh, 3.25rem); }

/* The value a tap WANTS, shown dimmer until the graph's own read-back lands:
   same aim/measurement split as the studio page's [data-state] styling, for the
   same reason (pw-cli cannot tell us whether a write landed). */
.mixread.aim { opacity: .55; }

/* A dB readout is wider than a percent one ("+12.0 dB"), and the equaliser rows
   are separated from the levels because they are not per-app. */
.mixrow.eq .mixread { min-width: 5.2rem; }

/* Band rows are tighter than the volume rows: ten of them share the leftover
   height, and a held band still moves 1 dB per request, so a smaller button loses
   no control. */
.mixrow.eq .mbtn {
  width: clamp(2.5rem, 11vw, 3.25rem);
  height: clamp(1.9rem, 5vh, 2.5rem);
  font-size: clamp(.95rem, 2.6vh, 1.3rem);
}

.mixeq .mixread { height: clamp(1.9rem, 5vh, 2.5rem); }

/* The equaliser's one-tap curves: a row of small word-buttons that wraps onto a
   second line on a narrow phone. Never a hold — a repeated preset is nonsense,
   and app.js wires it that way. */
.eqpresets {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: calc(var(--gap) / 2);
}

.pbtn {
  min-height: clamp(1.9rem, 5vh, 2.4rem);
  min-width: 2.75rem;                 /* "Flat" must still be a 44px target
                                         (render-check's MIN_W, measured) */
  border: 0;
  border-radius: 10px;
  background: var(--btn2-metal);
  color: var(--fg);
  font-size: clamp(.68rem, 1.9vh, .85rem);
  font-weight: 600;
  padding: 0 .55rem;
  white-space: nowrap;
  touch-action: manipulation;
}

/* The second thing on this page allowed to scroll (the library list is the
   first): minmax(0,1fr) above hands it the leftover height. Its rows are built
   by app.js from the studio state; the markup carries no band list at all. */
.mixeq {
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  display: grid;
  grid-auto-rows: min-content;
  align-content: start;
  gap: calc(var(--gap) / 2);
  padding-right: 2px;
}

/* The high-pass: its Gain is published and ignored (every biquad shares the same
   eleven ports; bq_highpass derives its coefficients from Freq and Q alone), so
   it gets a note in place of buttons — a control there would be the honest-looking
   lie eq/10-dope-eq.conf's header warns about. Its corner still moves when a
   preset sets it, which is why the frequency stays on screen. */
.mixrow.nogain { opacity: .55; }

.eqnote {
  grid-column: 2 / -1;
  justify-self: end;
  font-size: clamp(.62rem, 1.7vh, .78rem);
  opacity: .8;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mixsep {
  margin: .15rem 0;
  border: 0;
  border-top: 1px solid var(--btn);
  opacity: .5;
}

.mixhint {
  margin: 0;
  text-align: center;
  font-size: clamp(.65rem, 1.7vh, .78rem);
  opacity: .55;
}

/* A screen that is wide AND short: two columns for the four volume rows, then
   the presets and the (scrolling) band list span both. The one-column portrait
   layout is what a phone holds. */
@media (max-height: 480px) and (min-width: 600px) {
  .mixer {
    grid-template-columns: 1fr 1fr;
    column-gap: calc(var(--gap) * 2);
    grid-template-rows: auto auto auto minmax(0, 1fr);
  }
  .mixsep, .mixhint { display: none; }
  .eqpresets, .mixeq { grid-column: 1 / -1; }
}

/* --- top navigation: five uniform icon buttons ----------------------------- */
/* The pills used to be words, and words made the row non-uniform: "Wallpaper"
   is 57 px of label at 320 CSS px, so the four workspace cells could not be
   equal and the label font needed a container-query ladder (cqi) plus a
   measured 1.3fr ratio just to avoid clipping. Icons need none of that — five
   identical cells, one glyph centred in each — and the freed complexity is
   spent on tap size instead. The glyphs are inline SVG (no fonts, no images,
   no external requests), drawn with currentColor so the active/pressed states
   keep inverting the icon along with the button. Whether a word fits its cell
   is no longer a question this row can ask; tests/text-fit-check.py still
   measures the row (it now reports the empty-text case), and render-check.py
   still asserts every cell is a 44px+ target. */
.nav {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--gap);
}

.navbtn {
  height: clamp(2.5rem, 7vh, 3.5rem);
  border: 0;
  border-radius: 12px;
  color: var(--fg);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  touch-action: manipulation;
}

/* One glyph size for every cell: this is what "uniformly sized" means here —
   the five icons are the same box, whatever their shapes. Sized from vh (like
   every other control on this page) so a landscape phone with a short screen
   still leaves room for the D-pad row below. */
.navbtn svg {
  width: clamp(1.3rem, 3.9vh, 1.9rem);
  height: clamp(1.3rem, 3.9vh, 1.9rem);
  display: block;
}

/* Active = gold metal: the same sheen over the accent gradient, so the lit
   tile reads as the same material in a different finish. */
.navbtn.active {
  background: var(--accent-metal);
  color: #14140f;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .40),
              inset 0 -1px 0 rgba(0, 0, 0, .35),
              0 2px 5px rgba(0, 0, 0, .45);
}
.navbtn:disabled { opacity: .5; }

/* The view tab is not a workspace, so it never takes .active: its own state is
   "the mixer is showing", which is what aria-pressed says — and the same
   attribute swaps its two glyphs (sliders while the D-pad shows, a D-pad while
   the mixer does), so the picture and the accessible name cannot drift apart:
   app.js sets both from one variable. */
.viewbtn { background: var(--btn2-metal); }
.viewbtn[aria-pressed="true"] {
  background: var(--accent-metal);
  color: #14140f;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .40),
              inset 0 -1px 0 rgba(0, 0, 0, .35),
              0 2px 5px rgba(0, 0, 0, .45);
}
.viewbtn .icon-remote { display: none; }
.viewbtn[aria-pressed="true"] .icon-mixer { display: none; }
.viewbtn[aria-pressed="true"] .icon-remote { display: block; }

/* Pressed = the metal sinks. ONE shared rule for every button class, placed
   after the active rules so a pressed active button still gives tactile
   feedback instead of sitting on its gold. The drop shadow goes and an inset
   shadow takes over — the surface is pushed in, not lifted up. */
.navbtn.pressed, .navbtn:active,
.viewbtn.pressed, .viewbtn:active,
.dbtn.pressed, .dbtn:active,
.vbtn.pressed, .vbtn:active,
.mbtn.pressed, .mbtn:active,
.pbtn.pressed, .pbtn:active,
.backbtn.pressed, .backbtn:active {
  background: var(--btn-metal-down);
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, .65),
              0 1px 0 rgba(255, 255, 255, .05);
}

/* --- volume: one compact row; it eats height only once, never per press -- */
.volrow {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: stretch;
  gap: var(--gap);
}

.vbtn {
  height: clamp(2rem, 5.5vh, 2.75rem);
  border: 0;
  border-radius: 12px;
  color: var(--fg);
  font-size: clamp(1.05rem, 3vh, 1.5rem);
  line-height: 1;
  touch-action: manipulation;
}

.vread {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 3.6rem;
  padding: 0 .5rem;
  border-radius: 12px;
  background: #15151a;
  font-size: clamp(.8rem, 2.1vh, 1rem);
  font-variant-numeric: tabular-nums;
  opacity: .85;
}

/* --- D-pad: fills the leftover row; centre cell is OK --------------------- */
.dpad {
  min-height: 0;                          /* the difference between fit and clip */
  width: min(100%, 58vh);                 /* near-square, but never the taller axis */
  justify-self: center;
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: repeat(3, minmax(0, 1fr));
  grid-template-areas:
    ".    up    ."
    "left ok  right"
    ".    down  .";
}

.dbtn {
  min-height: 0;                          /* scaled by the row, never a floor */
  min-width: 0;
  border: 0;
  border-radius: 12px;
  color: var(--fg);
  font-size: clamp(.9rem, 3.4vh, 1.6rem);
  line-height: 1;
  touch-action: manipulation;            /* no double-tap zoom */
}

.dbtn.up    { grid-area: up; }
.dbtn.left  { grid-area: left; }
.dbtn.ok    { grid-area: ok; font-size: clamp(.8rem, 2.2vh, 1.1rem); font-weight: 700; }
.dbtn.right { grid-area: right; }
.dbtn.down  { grid-area: down; }

/* --- Back: its own row, because a stray key in a corner is not a D-pad ---- */
.backbtn {
  height: clamp(2.25rem, 6vh, 3rem);
  border: 0;
  border-radius: 12px;
  background: var(--btn2-metal);
  color: var(--fg);
  font-size: clamp(.85rem, 2.2vh, 1.05rem);
  font-weight: 600;
  touch-action: manipulation;
}

/* --- status line: a silent failure must be visible from the phone -------- */
.status {
  margin: 0;
  min-height: 1.25em;
  font-size: clamp(.68rem, 1.7vh, .8rem);
  opacity: .75;
  text-align: center;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.status.fail { color: var(--fail); opacity: 1; }

/* --- key prompt ---------------------------------------------------------- */
.gate {
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--gap);
  padding: 1.25rem;
  padding-top: calc(1.25rem + env(safe-area-inset-top, 0px));
  padding-bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
}

.gate .hint { margin: 0; font-size: .85rem; opacity: .7; }

#keyInput {
  min-height: 44px;
  padding: .7rem .9rem;
  border: 1px solid #33333c;
  border-radius: 12px;
  background: #131318;
  color: var(--fg);
  font-size: 1rem;
}

#loginBtn {
  min-height: 44px;
  border: 0;
  border-radius: 12px;
  background: var(--accent-metal);
  color: #14140f;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .40),
              inset 0 -1px 0 rgba(0, 0, 0, .35),
              0 2px 5px rgba(0, 0, 0, .45);
  font-size: 1.05rem;
  font-weight: 700;
  touch-action: manipulation;
}

.gatetsep {
  text-align: center;
  opacity: .5;
}

#joinBtn {
  min-height: 44px;
  border: 0;
  border-radius: 12px;
  background: var(--btn2-metal);
  color: var(--fg);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10),
              inset 0 -1px 0 rgba(0, 0, 0, .50),
              0 2px 5px rgba(0, 0, 0, .45);
  font-size: 1rem;
  font-weight: 600;
  touch-action: manipulation;
}
