/* Ported from playhook @ c348f4246752286b594c8a8eddd2253ea88b0f12 : src/renderer/styles.css
   Everything game-specific (Play button, info panel, confirm/error views, the select-game list) is gone;
   what remains is the launcher's empty screen, kept 1:1 except for the browser-specific fixes marked
   BROWSER below. See PORTED-FROM.md. */

/* Bundled UI font: M PLUS Rounded 1c. BROWSER: the launcher ships the full CJK .ttf files (13.7 MB for
   four weights); a public site cannot. These are Google Fonts' own latin woff2 subsets (~21 KB each),
   self-hosted so the page never touches a third-party origin (and font-src stays 'self'). */
@font-face {
  font-family: 'M PLUS Rounded 1c';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('./fonts/MPLUSRounded1c-Light.woff2') format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'M PLUS Rounded 1c';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('./fonts/MPLUSRounded1c-Regular.woff2') format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'M PLUS Rounded 1c';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('./fonts/MPLUSRounded1c-Medium.woff2') format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'M PLUS Rounded 1c';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url('./fonts/MPLUSRounded1c-ExtraBold.woff2') format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* One design pixel (mockup is 1920x1080). BROWSER: the launcher pins this to vh alone — safe there,
     since its window is always fullscreen 16:9 or wider. A browser window can be any shape, and a
     vh-only pixel lets the bar run off the side of a narrow one. Taking the SMALLER of "1/1080 of the
     height" and "1/1920 of the width" keeps the mockup's proportions and always fits. */
  --px: min(0.0925926vh, 0.0520833vw);
  --bg: #101014;
  /* Palette fallbacks; #app gets --d1/--d2 from the hero background's two dominant colors.
     BROWSER: the launcher ships a generic pair here, because its hero changes with every card — so for
     the first ~100-300ms (until the canvas read finishes) it shows colors that then jump. This site has
     exactly one, unchanging wallpaper, so these ARE its computed colors, read back from
     dominant-color.ts against public/wallpaper.webp. The runtime pass then confirms the same values and
     nothing shifts. Recompute them if the wallpaper is ever replaced. */
  --d1: rgb(12, 10, 27);
  --d2: rgb(131, 111, 149);
  --fg: #ffffff;
}

* {
  box-sizing: border-box;
  /* Kiosk/launcher UI: no text selection and no text caret cursor. */
  user-select: none;
  -webkit-user-select: none;
  cursor: default;
}

/* Cursor hidden while the gamepad is in use, or after the launcher's 5s idle timeout (class toggled from
   controls.ts). The descendant selector's specificity beats the per-element cursor rules (buttons'
   pointer); a real mouse move restores it. */
html.cursor-hidden,
html.cursor-hidden * {
  cursor: none;
}

html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
}

body {
  font-family: 'M PLUS Rounded 1c', 'Segoe UI', system-ui, sans-serif;
  color: var(--fg);
  background: var(--bg);
}

#app {
  position: relative;
  width: 100vw;
  height: 100vh;
  /* BROWSER: 100vh ignores the collapsing address bar on mobile Safari/Chrome, so the bar jumps as it
     hides. dvh tracks the real viewport; the vh line above stays as the fallback. */
  height: 100dvh;
  background-color: var(--bg);
  overflow: hidden;
  /* Establish a stacking context so the z-index:-1 hero paints ABOVE #app's own background-color
     (otherwise the opaque bg covers it → black screen) but below the UI. */
  isolation: isolate;
}

/* Hero background: two stacked layers inside #hero, cross-faded GTA-5-style. The z-index lives ONLY on
   the container, so the WHOLE hero sits one stacking level below the UI — the layers themselves carry no
   z-index (they stack by DOM order inside #hero), which sidesteps the opacity-stacking-context pitfall. */
#hero {
  position: absolute;
  inset: 0;
  z-index: -1;
}

/* Each hero layer paints one image (background-image set per-layer from JS) and slowly drifts to one
   side and back (Ken Burns / GTA ping-pong), always. The direction (--pan-x) is randomized per image
   from JS on the incoming layer. Both layers run bg-pan perpetually; opacity 0/1 (toggled via
   .is-active) cross-fades between them. NOTE: opacity:0 does NOT pause the animation, so JS
   force-restarts bg-pan on the incoming layer to start its drift from zero. */
.hero-layer {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  transition: opacity 1s ease;
  transform-origin: center;
  will-change: transform, opacity;
  animation: bg-pan 30s ease-in-out infinite alternate;
}

.hero-layer.is-active {
  opacity: 1;
}

/* Ping-pong drift + a gentle zoom in/out, synced over the cycle (Ken Burns). The min scale (1.06)
   keeps headroom so the small ±1.5% pan never reveals the edges. */
@keyframes bg-pan {
  from {
    transform: scale(1.06) translateX(0);
  }
  to {
    transform: scale(1.12) translateX(var(--pan-x, 1.5%));
  }
}

/* ── Bottom bar ──────────────────────────────────────────────────────────── */

#bottom-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: calc(300 * var(--px));
  /* Vertical gradient of d1: transparent at top → 0.75 → solid at bottom (mockup stops). */
  background: linear-gradient(
    to top,
    var(--d1) 0%,
    color-mix(in srgb, var(--d1) 75%, transparent) 58%,
    transparent 100%
  );
  /* BROWSER: the launcher only writes the unprefixed property (Chromium is all it targets). Safari did
     not ship it unprefixed until 18, and this page is public. */
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  -webkit-mask-image: linear-gradient(to top, #000 0%, #000 55%, transparent 100%);
  mask-image: linear-gradient(to top, #000 0%, #000 55%, transparent 100%);
  transition: opacity 0.3s ease;
}

.bar-content {
  position: absolute;
  left: calc(50 * var(--px));
  right: calc(50 * var(--px));
  bottom: calc(50 * var(--px));
  height: calc(92 * var(--px));
}

/* ── Play button ────────────────────────────────────────────────────────────
   Ported from the launcher: geometrically .rect-button at the LEFT edge, its only visual distinction
   being the pulsing ring on hover/focus. Shown ONLY on a game screen: the site's home screen IS the
   launcher's idle screen, and the launcher hides Play there (styles.css `#app[data-phase='idle']`). That
   also leaves mockups 1-3 untouched. The button is decorative — see index.html. */

#app[data-route='home'] .play-button {
  display: none;
}

.play-button {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(92 * var(--px));
  height: calc(92 * var(--px));
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: calc(10 * var(--px));
  background: transparent;
  color: var(--d2);
  cursor: pointer;
  transition:
    background 0.2s ease,
    color 0.2s ease,
    transform 0.12s ease;
}

/* :hover gated on a live cursor — see the note on .text-button:hover. */
html:not(.cursor-hidden) .play-button:hover,
.play-button.is-focused {
  background: var(--d2);
  color: var(--d1);
}

.play-button:active,
.play-button.is-pressed {
  transform: scale(0.9);
}

/* The keyboard focus ring (gated on html.kbd-focus) is defined once, next to .rect-button/.text-button. */

/* Pulse ring — a rounded square echoing the button (kept on hover/focus per design). */
.play-button::after {
  content: '';
  position: absolute;
  inset: calc(-7 * var(--px));
  border: calc(3 * var(--px)) solid var(--d2);
  border-radius: calc(17 * var(--px));
  opacity: 0;
  transition: opacity 0.2s ease;
}

html:not(.cursor-hidden) .play-button:hover::after,
.play-button.is-focused::after {
  opacity: 1;
  animation: pulse-ring 1.1s ease-in-out infinite;
}

@keyframes pulse-ring {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.09);
  }
}

.icon-play {
  width: calc(36 * var(--px));
  height: calc(48 * var(--px));
  fill: currentColor;
  /* Optical centering: a triangle looks off-center when geometrically centered; nudge right
     (+4px in the mockup, where the triangle's center sits 4px right of the button's center). */
  transform: translateX(calc(4 * var(--px)));
  /* Like .icon-more: the icon must not intercept the pointer, or the global `* { cursor: default }`
     would override the button's pointer cursor over it. */
  pointer-events: none;
}

/* ── Rect (More) button — normal: no background, content d2; fills with d2 on hover/focus ── */

.rect-button {
  position: absolute;
  right: 0;
  top: 0;
  width: calc(92 * var(--px));
  height: calc(92 * var(--px));
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: calc(10 * var(--px));
  background: transparent;
  color: var(--d2);
  cursor: pointer;
  transition:
    background 0.2s ease,
    color 0.2s ease,
    opacity 0.3s ease,
    transform 0.12s ease;
}

/* :hover gated on a live cursor — see the note on .text-button:hover. */
html:not(.cursor-hidden) .rect-button:hover,
.rect-button.is-focused {
  background: var(--d2);
  color: var(--d1);
}

/* Press feedback (mouse :active, or gamepad A via .is-pressed): smooth scale down. */
.rect-button:active,
.rect-button.is-pressed,
.text-button:active,
.text-button.is-pressed {
  transform: scale(0.9);
}

/* …except the search box: clicking it means "put the caret here", not "fire an action", and a field that
   shrinks under the pointer while you aim at a character is worse than no feedback. */
.input-button:active {
  transform: none;
}

/* The launcher suppresses the native ring outright — .is-focused is its single focus visual, and a
   kiosk has no Tab key. BROWSER: a public page does, and a keyboard user who does not know about the
   arrow keys would otherwise navigate blind. So the ring comes back — but ONLY while the user is
   actually driving with Tab (html.kbd-focus, set from controls.ts), never for a mouse, a gamepad, or a
   focus WE moved: those already show the .is-focused fill, and a ring on top is just noise. Gating on
   our own flag rather than on :focus-visible alone is what keeps an alt-tab from resurrecting the ring
   — the browser re-applies :focus-visible to whatever it restores focus to, our flag does not. */
.rect-button,
.text-button,
.play-button {
  outline: none;
}

html.kbd-focus .rect-button:focus-visible,
html.kbd-focus .text-button:focus-visible,
html.kbd-focus .play-button:focus-visible {
  outline: calc(3 * var(--px)) solid var(--d2);
  outline-offset: calc(4 * var(--px));
}

/* More: three dots, 56×16 in the mockup, centered in the 92×92 button. overflow:visible so the last
   dot (its right edge sits exactly on the viewBox boundary) isn't clipped by the SVG viewport. */
.icon-more {
  width: calc(56 * var(--px));
  height: calc(16 * var(--px));
  fill: currentColor;
  overflow: visible;
}

/* Icons don't intercept pointer events, so hover/click target the button itself (otherwise the
   global `* { cursor: default }` would override the button's pointer cursor over the icon). */
.icon-more {
  pointer-events: none;
}

/* ── Title & status ───────────────────────────────────────────────────────
   BROWSER: in the launcher these two are state-driven — the title only lifts and the status only fades
   in while a game is busy, and both slide back on the idle screen. The site has no states: both lines
   are permanently visible, so the busy-layout values are written unconditionally and the per-phase
   selectors are gone. What DOES vary is the left slot: `left: 124` (play 92 + gap 32) on a game screen,
   `left: 0` on home, where Play is hidden and the slot is free — the same swap the launcher makes by
   phase, keyed on the route instead. */

.title {
  position: absolute;
  left: calc(124 * var(--px));
  top: 0;
  height: calc(92 * var(--px));
  display: flex;
  align-items: center;
  white-space: nowrap;
  font-weight: 800;
  font-size: calc(32 * var(--px));
  line-height: calc(48 * var(--px));
  color: var(--fg);
  /* Lifted so the status forms a second line below it (the launcher's busy layout). */
  transform: translateY(calc(-18 * var(--px)));
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

.status {
  position: absolute;
  left: calc(124 * var(--px));
  top: 0;
  height: calc(92 * var(--px));
  display: flex;
  align-items: center;
  white-space: nowrap;
  font-weight: 400;
  font-size: calc(24 * var(--px));
  line-height: calc(36 * var(--px));
  color: var(--fg);
  /* Pre-positioned below center so it forms the second line under the lifted title. */
  transform: translateY(calc(18 * var(--px)));
  opacity: 1;
  transition: opacity 0.3s ease;
}

/* Home: no Play button, so both lines move into the empty left slot. */
#app[data-route='home'] .title,
#app[data-route='home'] .status {
  left: 0;
}

/* ── Popup (the menu) ───────────────────────────────────────────────────────
   ONE frosted veil + ONE right-side column (450px, measured off the Figma InfoBlockContainer — the
   launcher's own column is 550). The launcher switches four views in here by data-view; the site uses
   two of them, `details` and `select-game`. Toggled open with .is-open. */

.popup {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.popup-veil {
  position: absolute;
  inset: 0;
  /* Horizontal gradient: transparent on the left (hero visible) → solid d1 on the right. */
  background: linear-gradient(to right, transparent 0%, var(--d1) 100%);
  /* BROWSER: prefixed twin for Safari < 18 (see #bottom-bar). */
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  /* Mask so the frosted blur only appears on the right, fading in from the center. */
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 58%);
  mask-image: linear-gradient(to right, transparent 0%, #000 58%);
  opacity: 0;
  transition: opacity 0.35s ease;
}

/* Right column: 450px wide, inset 50px on right/top/bottom. The action stack is pushed to the bottom
   (margin-top:auto on .popup-actions). Slides in + fades with the veil. */
.popup-column {
  position: absolute;
  right: calc(50 * var(--px));
  top: calc(50 * var(--px));
  bottom: calc(50 * var(--px));
  width: calc(450 * var(--px));
  display: flex;
  flex-direction: column;
  text-align: right;
  transform: translateX(calc(40 * var(--px)));
  opacity: 0;
  transition:
    transform 0.35s ease,
    opacity 0.35s ease;
}

.popup.is-open {
  pointer-events: auto;
}
.popup.is-open .popup-veil {
  opacity: 1;
}
.popup.is-open .popup-column {
  transform: none;
  opacity: 1;
}

/* ── Action stack ───────────────────────────────────────────────────────────
   Pinned to the column bottom (margin-top:auto), as a vertical stack aligned to the right edge — a
   longer label grows LEFT, the right edge stays fixed. Default focus is the BOTTOM button. */
.popup-actions {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.actions-group {
  display: none;
  flex-direction: column;
  align-items: flex-end;
  gap: calc(8 * var(--px));
  /* A flex item's default `min-height: auto` is its content height, which would let the game list push
     Search and Close off the bottom of the column no matter what min-height the list itself carries.
     The launcher sets this only on the select-game group; here it is harmless on both. */
  min-height: 0;
}
#popup[data-view='details'] .actions-group[data-group='details'],
#popup[data-view='select-game'] .actions-group[data-group='select-game'] {
  display: flex;
}

/* ONLY the game list may claim the column's leftover height (the details stack stays content-sized and
   pinned to the bottom). Overflow stays VISIBLE here on purpose, so the scrollbar thumb can sit outside
   the list's right edge. */
#popup[data-view='select-game'] .actions-group[data-group='select-game'] {
  flex: 1 1 auto;
}

/* Positioning context for the custom scrollbar thumb, which overlays the list's right edge. It cannot
   live inside .select-game-list: that box's own overflow-y would clip a thumb pulled outside it and drag
   it along with the scroll. And without `position: relative` here the thumb would position off #app. */
.select-game-scroll {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
}

/* The game buttons scroll when the catalogue is long (they never shrink — see .text-button's flex:0 0
   auto), while Search and Close below stay fixed and reachable. The scroll area takes whatever height
   the popup column has left, so it adapts to the window instead of a magic cap. */
.select-game-list {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: calc(8 * var(--px));
  width: 100%;
  height: 100%;
  overflow-y: auto;
  /* Room so the last item isn't flush against the Search box below. */
  padding-bottom: calc(8 * var(--px));
  /* Hide the native scrollbar entirely — Chromium never animates ::-webkit-scrollbar, so a fade is
     impossible there. The visible one is .select-game-thumb, a real element we can transition. On a
     public page this beats the native bar for a second reason: a native scrollbar eats width INSIDE the
     list and shoves the right-aligned buttons over when it appears, and `scrollbar-gutter: stable`
     reserves that width permanently, breaking the mockups' flush right edge. */
  scrollbar-width: none;
}
.select-game-list::-webkit-scrollbar {
  display: none;
}

/* Custom scrollbar thumb: a hairline that barely reads against the frosted popup. Positioned from JS
   (translateY) and faded out when the focus leaves the game list or after a few idle seconds. It sits
   OUTSIDE the list's right edge (a negative `right` pulls it out of the box), so the buttons keep their
   full width — nothing shifts when it appears.
   PORT NOTE: the launcher also lets you DRAG this thumb (pointer capture, ~35 lines). Not ported: the
   list scrolls by wheel, gamepad and keyboard, and grabbing a 4px hairline is a marginal path. */
.select-game-thumb {
  --thumb-width: calc(4 * var(--px));
  --thumb-gap: calc(16 * var(--px)); /* clearance between the buttons and the scrollbar */
  position: absolute;
  top: 0;
  right: calc(-1 * (var(--thumb-width) + var(--thumb-gap)));
  width: var(--thumb-width);
  /* Fixed height: a proportional thumb (clientHeight/scrollHeight) grows to nearly the full track when
     the list only just overflows, which reads as a bug. JS only positions it (translateY). */
  height: calc(32 * var(--px));
  border-radius: calc(2 * var(--px));
  background: color-mix(in srgb, var(--fg) 10%, transparent);
  opacity: 0;
  transition: opacity 0.35s ease;
  /* Purely an indicator here (no drag), so it never needs to catch the pointer. */
  pointer-events: none;
}
.select-game-thumb.is-visible {
  opacity: 1;
}

/* ── Game buttons, marquee ──────────────────────────────────────────────────
   Game buttons size to their title (like every other .text-button, right-aligned in the stack), but never
   wider than the column's 424px cap — a long title is HARD-CLIPPED there (no ellipsis). Only while the
   button is focused does the title marquee-scroll back and forth so the full name is readable; the popup
   never widens. Non-overflowing titles never scroll.
   The cap is derived from the column rather than written as 424 so the two can't drift apart: the Figma
   frame puts 26px between the widest button and the column's left edge. */
.select-game-list .game-button {
  width: auto;
  max-width: calc(100% - 26 * var(--px));
  justify-content: flex-start;
  overflow: hidden;
}
.game-label {
  /* `0 1 auto`: sized by the text, but allowed to SHRINK once the button hits max-width — that shrink
     (with min-width:0) is what produces the clip. `1 …` would instead stretch it on short titles. */
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  --fade: calc(8 * var(--px)); /* soft-fade width at a clipped edge */
}
/* A clipped (overflowing) title fades out at the RIGHT edge so the cut looks soft, not chopped. */
.game-button.is-clipped .game-label {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent 100%);
  mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent 100%);
}
/* While the focused title marquee-scrolls, fade BOTH edges so the text softly enters/leaves on each side. */
.game-button.is-scrolling .game-label {
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--fade),
    #000 calc(100% - var(--fade)),
    transparent 100%
  );
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--fade),
    #000 calc(100% - var(--fade)),
    transparent 100%
  );
}
.game-label-inner {
  display: inline-block;
  white-space: nowrap;
  will-change: transform;
}
/* Ping-pong marquee: shift + per-title duration are set from JS (measured overflow); `alternate` scrolls
   to the end and back, `ease-in-out` dwells at each edge so the start/end are readable. */
.game-button.is-scrolling .game-label-inner {
  animation: game-marquee var(--marquee-duration, 6s) ease-in-out infinite alternate;
}
@keyframes game-marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(var(--marquee-shift, 0));
  }
}

/* ── List placeholder (loading / empty / error / no matches) ────────────────
   Deliberately NOT a button: it must stay out of the focus stack, or A would land on a line that does
   nothing and the whole list would look broken. */
.list-placeholder {
  flex: 0 0 auto;
  height: calc(92 * var(--px));
  padding: 0 calc(13 * var(--px));
  display: flex;
  align-items: center;
  font-weight: 500;
  font-size: calc(28 * var(--px));
  white-space: nowrap;
  color: color-mix(in srgb, var(--d2) 65%, transparent);
}

/* ── Input (Search) ─────────────────────────────────────────────────────────
   A TextButton you can type into. The width follows the content (115px empty, 250px with "Search: bloo"
   in the mockups), measured by .input-mirror — see controls.ts. */
.input-button {
  /* The label is left-aligned inside the box, unlike a plain TextButton's centered caption. */
  justify-content: flex-start;
  gap: 0;
  overflow: hidden;
  /* Containing block for the absolutely-positioned mirror, so it can't stretch anything. */
  position: relative;
  /* Same 424 cap as a game button, and for the same reason: the box grows LEFT from a fixed right edge,
     so without it a wide enough query walks straight out of the column and over the veil. The input's
     `maxlength` bounds the text but not its width — 24 capitals are twice as wide as 24 lowercase.
     Past the cap the field shrinks (min-width:0) and scrolls its own content, which keeps the caret
     in view — the one thing a plain `overflow: hidden` would not. */
  max-width: calc(100% - 26 * var(--px));
}

.input-prefix,
.input-colon {
  pointer-events: none;
}

/* The colon only exists once something has been typed: mockup 2 reads "Search", mockup 3 "Search: bloo". */
.input-colon {
  display: none;
  padding-right: calc(6 * var(--px));
}
.input-button.is-filled .input-colon {
  display: inline;
}

.input-field {
  /* Sized from JS (the mirror below); this is only the floor for an empty field's caret. */
  width: calc(1 * var(--px));
  min-width: 0;
  padding: 0;
  border: none;
  outline: none;
  background: transparent;
  color: inherit;
  font-family: inherit;
  font-weight: inherit;
  font-size: inherit;
  line-height: inherit;
  /* The global kiosk rules below make a text field unusable — a caret you cannot place and text you
     cannot select. Undo them here and nowhere else. */
  user-select: text;
  -webkit-user-select: text;
  cursor: text;
}
.input-button {
  cursor: text;
}

/* Hidden twin of the field, carrying the same text in the same font: its offsetWidth is the width the
   field needs. `size` is no substitute — it counts monospace ex units and misses on a proportional font. */
.input-mirror {
  position: absolute;
  visibility: hidden;
  white-space: pre;
  pointer-events: none;
  font-family: inherit;
  font-weight: inherit;
  font-size: inherit;
  letter-spacing: inherit;
}

/* ── TextButton — textual sibling of .rect-button ───────────────────────────
   Transparent with d2 text; inverts to a d2 fill with d1 text on hover/focus (the design's "Active"
   variant — the mockup's filled bottom button is just the default focus, not a persistent state).
   Also worn by an <a> (the Github link), hence the text-decoration reset.

   Horizontal padding is 13, the Figma component's value in every instance across all five mockups; the
   launcher writes 32. This brings the component to its own spec, so it is visibly narrower on the
   existing screens too (Collection goes ~205 → 184px, where min-width takes over).
   `min-width: 184` is NOT from Figma — the mockups author it inconsistently (184 in one, 98 in another).
   It is the launcher's value and stays the single source of the lower bound. */

.text-button {
  height: calc(92 * var(--px));
  /* Never squeeze: these are flex items in a vertical stack, and the default flex-shrink:1 would compress
     them instead of letting a long list scroll (see .select-game-list). */
  flex: 0 0 auto;
  min-width: calc(184 * var(--px));
  padding: 0 calc(13 * var(--px));
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: calc(10 * var(--px));
  background: transparent;
  color: var(--d2);
  font-family: inherit;
  font-weight: 500;
  font-size: calc(28 * var(--px));
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition:
    background 0.2s ease,
    color 0.2s ease,
    transform 0.12s ease;
}

/* Hover paints the same highlight as .is-focused, but ONLY while the cursor is live: once the gamepad
   takes over (html.cursor-hidden), the pointer still physically sits over whatever it last hovered, so a
   bare :hover would keep that button highlighted alongside the gamepad's .is-focused one — two
   highlights, and A (which acts on .is-focused) would fire a different button than the hovered one looks. */
html:not(.cursor-hidden) .text-button:hover,
.text-button.is-focused {
  background: var(--d2);
  color: var(--d1);
}

/* ── BROWSER: narrow / short viewports ─────────────────────────────────────
   Below this size the proportional pixel above collapses to something unreadable (≈0.2px on a phone,
   i.e. a 6px title), so the design pixel gets a floor. But a floor alone breaks the layout: at that
   scale the absolutely-positioned two-line block no longer fits beside the More button, and
   `body{overflow:hidden}` would hide the overflow rather than reveal it — the bug would show up as
   text sliding under the button. So the bar switches to flow layout here, and the status may wrap.
   The trigger is width OR height: a landscape phone (812×375) and a tablet (768×1024) both need it,
   and only one of the two dimensions is small in each. */
@media (max-width: 900px), (max-height: 600px) {
  :root {
    --px: max(min(0.0925926vh, 0.0520833vw), 0.55px);
  }

  #bottom-bar {
    height: auto;
    padding-top: calc(120 * var(--px));
  }

  .bar-content {
    position: relative;
    left: auto;
    right: auto;
    bottom: auto;
    height: auto;
    margin: 0 calc(50 * var(--px)) calc(50 * var(--px));
    /* Room for the More button, which stays absolutely positioned in this box's top-right corner — and,
       on a game screen, the mirror-image room for Play at the top-LEFT (both stay absolute here, so
       without the reserve the text would run under them). */
    padding-right: calc(124 * var(--px));
    display: flex;
    flex-direction: column;
    align-items: flex-start;
  }

  #app[data-route='game'] .bar-content {
    padding-left: calc(124 * var(--px));
  }

  /* Back into the flow, in reading order (the markup puts #status first so the launcher can lift the
     title over it). */
  .title,
  .status {
    position: static;
    height: auto;
    transform: none;
  }
  /* `left` is meaningless in the flow — the padding above is what makes room for Play now. */
  #app[data-route='home'] .title,
  #app[data-route='home'] .status {
    left: auto;
  }
  .title {
    order: 1;
  }
  .status {
    order: 2;
    /* Wrap instead of running off the edge — this line is the widest thing in the bar. */
    white-space: normal;
  }

  .popup-column {
    width: min(calc(450 * var(--px)), 88vw);
  }
}

/* ── BROWSER: reduced motion ───────────────────────────────────────────────
   The hero both pans (animation) and cross-fades (a 1s opacity transition). Killing only the animation
   would leave the fade running, so both go. The Play button's ring and the game-title marquee are the
   other two perpetual motions on the page — an endlessly pulsing ring at the screen edge is precisely
   what this query exists for; the ring itself stays, only its beat stops. */
@media (prefers-reduced-motion: reduce) {
  .hero-layer {
    animation: none;
    transition: none;
  }
  html:not(.cursor-hidden) .play-button:hover::after,
  .play-button.is-focused::after {
    animation: none;
  }
  .game-button.is-scrolling .game-label-inner {
    animation: none;
  }
}
