/* Public arcade hub stylesheet for the vanilla-TS player. A VERBATIM port of
   frontend/src/app/pages/hub/hub.component.scss - every value (px, color, gradient,
   transition, max-height/opacity collapse timing, grid gap, padding) is copied
   unchanged; nothing visual is altered.

   Wrapper strategy (per backend/docs/arcade/SPEC_CSS.md sections 5 and 1B): the Angular
   component was ViewEncapsulated, so its :host block and every .mf-* rule were auto-scoped.
   In vanilla there is no encapsulation, so:
     - the :host { ... } block (the --mf-* token overrides + display/width/height/font-family
       layout) moves onto the page-root wrapper class .arcade-hub, and
     - every hub-component-local selector (all .mf-hub-*, the .mf-board-overlay/.mf-board-sheet/
       .mf-board-mount overlay wrappers, all .mf-set-*, and the hub's own .mf-brand-mire/
       .mf-brand-games lockup spans) is prefixed with .arcade-hub so the rules cannot leak
       globally and win by specificity over the OLDER hub variant carried in shell.css
       (which has a per-card accent border + 12px gap - this live design uses 10px gap, no
       accent edge, a gear, and name-below-banner).
   The SHARED kit classes the mounted leaderboard panel reads (.mf-card, .mf-btn*, .mf-toggle*,
   .mf-list*, .mf-board-row/.mf-board-rank/.mf-board-avatar/.mf-board-name/.mf-board-score/
   .mf-board-level/.mf-board-me, etc.) are NOT in this file: they come globally from shell.css
   (a verbatim slice also in leaderboard-shared.css) and must stay UNWRAPPED so the mounted
   board reads them. Only the board OVERLAY positioning wrappers below (mf-board-overlay/sheet/
   mount), which live in hub.component.scss, are reproduced here - and the overlay uses
   position:fixed; inset:0 so it still covers the viewport even though it is .arcade-hub-scoped.

   The --mf-* tokens are declared on .arcade-hub (the former :host) so they inherit into both
   the hub DOM and the mounted board panel mounted inside it. (shell.css also declares the full
   --mf-* set on :root; re-asserting the subset here matches the source and keeps the hub true
   if it ever loads in isolation.) Loads fourth in the order base -> tokens -> shell -> hub -> play.
   The build-player.mjs step auto-copies styles/*.css into the dist root; the <link> is added
   by the composer, not here. */

/* :host -> .arcade-hub. The --mf-* token overrides + the box/type layout the host carried. */
.arcade-hub {
  --mf-bg: #10131c;
  --mf-bg-2: #1c2230;
  /* the page + header fill. NIGHT (dark navy) is now the DEFAULT; an optional DAY
     (warm parchment) theme is opted into via :root[data-theme="day"] .arcade-hub
     below. The theme is set by a data-theme attribute on <html> (index.ts applies
     and persists the choice); absence/unknown = night. */
  --mf-page-bg: #1c2230;
  --mf-surface: rgba(28, 34, 48, 0.86);
  /* the flatter row fill, the soft accent wash and the faint hairline the mounted
     kit leaderboard card reads; lifted straight from the kit shell :root so the
     card renders true inside the hub the same way it does in a game bundle */
  --mf-surface-flat: rgba(16, 19, 28, 0.4);
  --mf-accent-soft: rgba(184, 168, 216, 0.14);
  --mf-faint: rgba(255, 255, 255, 0.12);
  --mf-ink: #eef1f7;
  --mf-ink-dim: rgba(255, 255, 255, 0.55);
  --mf-accent: #b8a8d8;
  --mf-card-border: rgba(184, 168, 216, 0.18);
  --mf-radius: 14px;

  display: block;
  width: 100%;
  /* a fixed viewport height makes the hub its own scroll container, which is
     what lets the toolbar position:sticky actually stick. --tg-viewport-height
     tracks the Telegram fullscreen height (100vh stayed pinned to the static
     pre-fullscreen box on iOS and letterboxed); 100dvh is the web fallback. */
  height: var(--tg-viewport-height, 100dvh);
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
}

/* ***************************************************************
 * DAY theme (optional, opt-in)
 * *************************************************************** */

/* OPTIONAL warm-parchment DAY theme, opted into by data-theme="day" on <html>.
   hub.css re-declares the --mf-* tokens ON .arcade-hub, so those beat any
   :root[data-theme] tokens shell.css sets — the day token overrides MUST therefore
   be re-declared at this .arcade-hub-scoped selector to win inside the hub. NIGHT
   (no attribute / data-theme="night") keeps the dark default block above untouched. */
:root[data-theme="day"] .arcade-hub {
  --mf-page-bg: #faf9f5;
  --mf-bg: #faf9f5;
  --mf-bg-2: #efe9d9;
  --mf-surface: #ffffff;
  --mf-surface-flat: #f1eee7;
  --mf-ink: #2c2a26;
  --mf-ink-dim: #5c5850;
  --mf-faint: #e6e2d9;
  --mf-accent: #880e4f;
  --mf-accent-soft: rgba(136, 14, 79, 0.10);
  --mf-card-border: #e6e2d9;
  /* Paint the parchment fill on the wrapper itself in day mode. The scroller
     (.mf-hub) is made transparent below so the fixed doodle pseudo-element shows
     through between this fill and the cards instead of being hidden by an opaque
     scroller background. */
  background: var(--mf-page-bg);
}

/* DAY-only doodle wallpaper: a faded, repeating Telegram/WhatsApp-style line-art
   tile sat BEHIND every hub element. position:fixed pins it to the viewport so it
   reads as a calm backdrop without scroll jank; pointer-events:none keeps the cards
   and controls fully tappable; z-index:0 (with the scroller lifted to z-index:1
   below) keeps it strictly behind content so it never reduces readability. NIGHT
   never emits this pseudo-element, so the dark theme is unchanged. */
:root[data-theme="day"] .arcade-hub::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: url('/assets/shared/doodle-bg.webp');
  background-repeat: repeat;
  background-size: 460px;
  opacity: 0.2;
}

/* Lift the hub scroller above the day doodle so cards, grid and the sticky header
   all paint over the pattern. Scoped to day so night layering is untouched; the
   sticky header (.mf-hub-tools z-index:5) keeps its own higher stacking inside. */
:root[data-theme="day"] .arcade-hub .mf-hub {
  position: relative;
  z-index: 1;
  /* transparent in day so the wrapper parchment fill + the fixed doodle behind
     show through; the opaque navy fill is kept for night via the base rule */
  background: transparent;
}

.arcade-hub .mf-hub {
  position: relative;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  overflow-y: auto;
  /* Hide the scrollbar entirely (still scrollable by wheel / touch) so it never
     takes layout width. That keeps the tool row's left and right edge gaps equal
     (the scrollbar would otherwise eat the right side), and toggling Favorites
     never widens the content box / zooms the cards. */
  scrollbar-width: none;
  /* Ride the brand lockup at the very top: only the device safe-area inset plus a
     small 8px breathing gap (no extra static clearance). On web that inset is 0 so
     the lockup pins right to the top; a notch / status bar is still respected. */
  padding: calc(env(safe-area-inset-top) + 8px) 16px max(24px, env(safe-area-inset-bottom));
  background: var(--mf-page-bg);
  color: var(--mf-ink);
}

/* The settings gear, sitting at the right end of the tool row just past the
   favorites chip — a 42×42 icon button matching the chip / search height. */
.arcade-hub .mf-hub-gear {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  padding: 0;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 12px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.arcade-hub .mf-hub-gear:active {
  background: var(--mf-bg-2);
}

/* The brand lockup, shown in the sticky header top row */
.arcade-hub .mf-hub-lockup {
  display: flex;
  align-items: center;
  gap: 9px;
}

.arcade-hub .mf-hub-mark {
  display: block;
  height: 30px;
  width: 30px;
  /* the square brand monogram, the dark-background variant for the night theme */
  background: url('/assets/brand/trademire-icon-dark.svg') center / contain no-repeat;
}

/* day theme: swap the faded dark-background icon for the exact-brand-color day icon
   (#841B3C bordeaux circles + #746A82 grey figure) so the mark matches the day
   wordmark colours, never the washed-out dark variant on the light wallpaper */
:root[data-theme="day"] .arcade-hub .mf-hub-mark {
  /* ONE logo regardless of theme: day uses the SAME pinkish dark-variant icon as
     night (owner: apply the night look to day, single logo both modes) */
  background-image: url('/assets/brand/trademire-icon-dark.svg');
}

.arcade-hub .mf-hub-word {
  display: flex;
  align-items: baseline;
  gap: 5px;
  font-size: 20px;
  font-weight: 800;
  letter-spacing: 0.3px;
}

.arcade-hub .mf-brand-mire {
  color: #d96f8a;
}

.arcade-hub .mf-brand-games {
  color: #b8b0c2;
}

/* the stats row under the arcade title: the crown tally and the coins pill side by
   side, so the two totals read together as one compact stat bar */
.arcade-hub .mf-hub-stats {
  display: inline-flex;
  align-items: center;
  gap: 14px;
}

/* The arcade-wide crown tally, shown on the right of the sticky header top row */
.arcade-hub .mf-hub-total-crowns {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-weight: 800;
  color: var(--mf-gold);
}

/* the total-coins pill, stacked right under the crown tally (the subrow is a centered
   column): the gold jeton mark + the player's shared-economy balance, in the branded
   jeton gold so it reads as the account wallet shared across every game */
.arcade-hub .mf-hub-total-coins {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-weight: 800;
  color: #f7c948;
}

/* The invite tally. It is NOT in this stats row any more - it sits in the title line's
   leading flank (see .mf-hub-leftstack far below) - but the
   rule lives here beside the two pills it is a sibling in kind to, so the three stay in
   one visual family: same 800 weight, same 4px mark-to-text gap, same gold.

   The extra rules over the crowns/coins pills are all consequences of it being a BUTTON
   parked on its own line rather than a div in a centred strip: the button reset, a
   nowrap so a long localized label ("{count} uitgenodigd") breaks the grid column
   instead of folding onto a second line, and a slightly smaller type so the label rides
   in the flank without out-measuring the heading in most languages. */
.arcade-hub .mf-hub-total-referrals {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 0;
  font-family: inherit;
  font-size: 13px;
  font-weight: 800;
  line-height: 1.2;
  white-space: nowrap;
  color: var(--mf-gold);
  background: transparent;
  border: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.arcade-hub .mf-hub-total-referrals:active {
  opacity: 0.7;
}

/* The mark is a person with an arrow LEAVING them, and an arrow is one of the few
   glyphs whose meaning is carried by its direction, so in a right-to-left page it has to
   point the other way or it reads as somebody arriving. The label text beside it is
   reordered by the bidi algorithm on its own; only the drawing needs this. */
:root[dir="rtl"] .arcade-hub .mf-hub-total-referrals > svg {
  transform: scaleX(-1);
}

/* The invite banner — the growth CTA that rides the scroll content directly under the
   sticky header. A full-width tappable card: the gift mark, the headline + live
   progress subline, and a gold CTA chip. Gold-bordered so it reads as the reward /
   invite surface without shouting over the catalog. Hidden outside Telegram. */
.arcade-hub .mf-hub-invite {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  box-sizing: border-box;
  margin: 0 0 14px;
  padding: 12px 14px;
  font-family: inherit;
  text-align: left;
  background: var(--mf-surface);
  border: 1px solid var(--mf-gold);
  border-radius: 14px;
  cursor: pointer;
}

.arcade-hub .mf-hub-invite-icon {
  flex: 0 0 auto;
  font-size: 24px;
  line-height: 1;
}

.arcade-hub .mf-hub-invite-text {
  flex: 1 1 auto;
  min-width: 0;
}

.arcade-hub .mf-hub-invite-title {
  font-size: 14px;
  font-weight: 800;
  color: var(--mf-ink);
}

.arcade-hub .mf-hub-invite-sub {
  margin-top: 2px;
  font-size: 12px;
  color: var(--mf-ink-dim);
}

.arcade-hub .mf-hub-invite-cta {
  flex: 0 0 auto;
  padding: 8px 14px;
  font-size: 13px;
  font-weight: 800;
  /* brand bordeaux on the gold chip — the same readable pairing the coin mark uses */
  color: #841b3c;
  background: var(--mf-gold);
  border-radius: 10px;
  white-space: nowrap;
}

/* The community footer — a quiet bottom strip of social-channel links, separated from
   the grid by a hairline. Secondary by design (the invite banner is the primary growth
   lever), so it is low-emphasis and centered. */
.arcade-hub .mf-hub-community {
  max-width: 720px;
  margin: 8px auto 20px;
  padding: 16px 12px 0;
  text-align: center;
  border-top: 1px solid var(--mf-card-border);
}

.arcade-hub .mf-hub-community-caption {
  margin-bottom: 10px;
  font-size: 12px;
  color: var(--mf-ink-dim);
}

.arcade-hub .mf-hub-community-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
}

/* the footer channel buttons are round icon-buttons (the brand glyph only); the icon
   is currentColor so it tints with the theme and the active state */
.arcade-hub .mf-hub-community-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  color: var(--mf-ink-dim);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 50%;
  cursor: pointer;
}

.arcade-hub .mf-hub-community-link:active {
  color: var(--mf-ink);
  border-color: var(--mf-gold);
}

/* the Community channel buttons in the settings sheet share the language-pill shape
   (.mf-set-lang) but center a brand icon instead of text. The compound selector keeps
   the icon centering + tint winning over the base pill rule regardless of source
   order, since the button carries both classes. */
.arcade-hub .mf-set-lang.mf-set-social {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 52px;
  padding: 0 14px;
  color: var(--mf-ink-dim);
}

.arcade-hub .mf-set-lang.mf-set-social:active {
  color: var(--mf-ink);
}

/* The branded header + search toolbar, pinned to the top. Its fill is the page's
   own base background colour (the same --mf-bg the .mf-hub gradient starts from)
   and it carries NO drop shadow, so the header reads as part of the page rather
   than a separate bar — there is no seam line under it. It stays opaque so the
   grid never bleeds through the pinned header while the long list scrolls under it.
   Flush at the bottom (no bottom padding); the tool row bleeds to the page edges. */
.arcade-hub .mf-hub-tools {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  /* explicit stretch: the kit shell.css `.mf-hub-tools` sets align-items:center,
     which (since we only override flex-direction) would otherwise leak in and
     center+shrink each row to its content width — that is what stopped the search
     from filling the row and the tool row from bleeding to the edges. Stretch makes
     every row span the full header width so the search grows and the bleed works. */
  align-items: stretch;
  /* flexible full width: no fixed cap, so the header band spans the whole content
     column edge to edge on any device (was capped at 720px which left big doodle
     margins beside it on wide screens). */
  max-width: none;
  /* bleed the parchment band fully to the screen edges: the -16px side margins
     cancel the .mf-hub 16px side padding so the background touches both edges with
     no gap, and the matching 16px side padding re-insets the content so the brand,
     subtitle and tool row stay exactly where they were (the toolrow -13px bleed
     still resolves to the same 3px edge gap). */
  margin: 0 -16px 12px;
  padding: 12px 16px 2px;
  background: var(--mf-page-bg);
  /* explicitly none: the kit shell.css ships a `.mf-hub-tools` rule with a drop
     shadow, and without this override that shadow leaks in and draws a seam line
     under the header. The header must read as part of the page, so: no shadow. */
  box-shadow: none;
}

/* Extend the opaque bar up over the Telegram-clearance zone, so once the header
   pins there is never a transparent gap above it where the scrolling grid shows
   through. Sits behind the brand row and below the gear (which is z-index 6). */
.arcade-hub .mf-hub-tools::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: calc(max(24px, env(safe-area-inset-top)) + 64px);
  background: var(--mf-page-bg);
}

/* brand row: the Mire Games mark, centered and always pinned at the top */
.arcade-hub .mf-hub-brandrow {
  display: flex;
  justify-content: center;
}

/* subtitle row: the arcade label over the crown tally, centered under the brand.
   Collapses smoothly when the list is scrolled, leaving just the brand and the
   search pinned. A premium ease, not a hard cut.

   THE PREMIUM SCROLL FIX (the cause of the web.telegram search-bar / screen shake):
   the OLD collapse was a bare `max-height: 64px -> 0` transition. That reflowed every
   animation frame AND shrank the sticky header's flow height mid-scroll, which shrank
   the total scroll content; the browser then re-seated scrollTop, the re-seat fired
   another scroll event, the collapse flipped again => it oscillated and the whole
   screen jittered. The 18px-on / 6px-off band sat right where Telegram's momentum /
   rubber-band scroll lives, so the oscillation was constant.

   Two changes kill it: (1) the visible fold is now carried by a GPU-composited
   `transform: scaleY()` + `opacity` (run on the compositor, zero reflow), so the motion
   the eye follows never touches layout; the max-height/margin still release the box so
   the search rides up with no gap and no clip, but they only SETTLE the box, they no
   longer ARE the animation. (2) the index.ts side flips `.scrolled` at most once per
   animation frame (rAF-throttled) with a WIDE 64px-on / 24px-off dead-band kept far
   from the very top, so a momentum-scroll burst near 0 can never straddle a threshold
   and oscillate the box-release. transform-origin: top folds it up into the brand row;
   will-change keeps the transform/opacity on their own layer so a phone GPU composites
   the fold cheaply. */
.arcade-hub .mf-hub-subrow {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  overflow: hidden;
  /* max-height caps (does not stretch) the natural content, so expanded there is
     never a gap and never a clip. The tallest content is now the TITLE LINE plus the
     stats strip: the leading flank of the title line carries the ~15px invite tally
     (~20px with its padding), then this box's own 4px gap and the
     ~20px crowns/coins strip, so ~78px of real content — and the cap sits well clear of
     it so a locale whose invite label wraps taller is not clipped either. It was 96px,
     which the tally would have run right up against. The box still releases its space on
     collapse (so the search rides up cleanly), but the index.ts side flips it AT MOST
     once per animation frame (rAF) with a WIDE dead-band kept far from the very top,
     so the box-release transition settles once per crossing instead of thrashing
     against momentum scroll near 0. */
  max-height: 124px;
  margin-top: 7px;
  opacity: 1;
  transform-origin: top center;
  /* the transform + opacity are the GPU-composited motion the eye follows (no reflow);
     max-height + margin-top settle the box once underneath them. Premium ease. */
  transition:
    transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1),
    opacity 0.22s ease,
    max-height 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
    margin-top 0.34s cubic-bezier(0.22, 0.61, 0.36, 1);
  will-change: transform, opacity;
}

.arcade-hub .mf-hub-tools.scrolled .mf-hub-subrow {
  max-height: 0;
  margin-top: 0;
  opacity: 0;
  transform: scaleY(0) translateY(-10px);
}

/* the Oyun Salonu arcade label inside the subtitle row */
.arcade-hub .mf-hub-arcade {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
  color: var(--mf-ink);
  text-align: center;
}

/* search row: the search input and the settings gear. It bleeds out past the
   .mf-hub 16px side padding (negative side margins) so the search sticks flush to
   the left edge and the gear flush to the right edge. The favorites chip used to sit
   between them; it moved down into .mf-hub-chiprow with the two play-shape chips. */
.arcade-hub .mf-hub-toolrow {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
  /* 3px gap each side: .mf-hub pads 16px, so -13px leaves a 3px edge gap */
  margin-left: -13px;
  margin-right: -13px;
}

.arcade-hub .mf-hub-search {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 42px;
  box-sizing: border-box;
  padding: 10px 14px;
  font-family: inherit;
  font-size: 15px;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 12px;
  outline: none;
}

.arcade-hub .mf-hub-search::placeholder {
  color: var(--mf-ink-dim);
}

.arcade-hub .mf-hub-search:focus {
  border-color: var(--mf-accent);
}

/* the filter chip row, directly under the search input: favorites plus the two
   play-shape chips (level-based / endless). It bleeds to the same 3px edge gap as the
   tool row above so the two rows share one left edge. `flex-wrap` is the safety net,
   not the plan: three one-or-two-word chips fit one line down to a 320px phone in
   every shipped locale, and a locale that ever outgrows that wraps cleanly instead of
   pushing the gear off screen. `overflow-x` is deliberately NOT used - a horizontally
   scrolling strip would hide the third chip behind a gesture nobody is told about. */
.arcade-hub .mf-hub-chiprow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  margin-left: -13px;
  margin-right: -13px;
}

/* one filter chip: leading glyph, then the label. All three share this block, so the
   row reads as one control set; only the active tint differs (see the two .on rules
   below). The pill radius separates them from the 12px-radius search box and gear they
   sit under, which is the point - those two act, these three FILTER. */
.arcade-hub .mf-hub-chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 34px;
  padding: 0 12px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  color: var(--mf-ink-dim);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 999px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.14s ease, border-color 0.14s ease;
}

/* the glyph never shrinks when a long label competes for the row */
.arcade-hub .mf-hub-chip svg {
  flex: 0 0 auto;
}

/* the two play-shape chips light in the app accent. Favorites keeps the GOLD it has
   always had (the rule below wins on source order), which is doing real work: gold is
   the favorites colour everywhere in this hub - the card star, the crown tally - so the
   colour alone says the favorites chip is a different KIND of filter from the two
   accent shape chips it sits beside, and that it combines with either. */
.arcade-hub .mf-hub-modechip.on {
  color: var(--mf-accent);
  border-color: var(--mf-accent);
}

.arcade-hub .mf-hub-favchip.on {
  color: var(--mf-gold);
  border-color: var(--mf-gold);
}

/* Loading, error and empty state lines */
.arcade-hub .mf-hub-state,
.arcade-hub .mf-hub-empty {
  max-width: 720px;
  margin: 24px auto;
  text-align: center;
  color: var(--mf-ink-dim);
  font-size: 14px;
}

.arcade-hub .mf-hub-empty {
  grid-column: 1 / -1;
}

.arcade-hub .mf-hub-retry {
  margin-left: 10px;
  padding: 8px 16px;
  font-family: inherit;
  font-weight: 700;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 10px;
  cursor: pointer;
}

/* The two-column game grid: minmax keeps two cards per row on a phone */
.arcade-hub .mf-hub-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 10px;
  max-width: 720px;
  margin: 0 auto;
}

.arcade-hub .mf-hub-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 16px;
  text-decoration: none;
  background: var(--mf-surface);
  /* one uniform, calm border on every card. No per-game accent edge, so the
     grid reads clean and the colorful hero art is the only color, not a chaos
     of clashing card outlines. */
  border: 1px solid var(--mf-card-border);
  border-radius: var(--mf-radius);
  transition: transform 0.1s ease, background 0.15s ease, box-shadow 0.2s ease;
}

/* A filtered-out card must actually leave the layout. The card sets display:flex,
   which is an author rule that would otherwise beat the plain [hidden] attribute,
   so the favorites and search filters would visually do nothing. This author rule
   at attribute specificity restores the hide. */
.arcade-hub .mf-hub-card[hidden] {
  display: none;
}

.arcade-hub .mf-hub-card:hover {
  background: rgba(28, 34, 48, 0.96);
  transform: translateY(-2px);
}

.arcade-hub .mf-hub-card strong {
  font-size: 15px;
  font-weight: 700;
  color: var(--mf-ink);
}

.arcade-hub .mf-hub-card span {
  font-size: 12px;
  line-height: 1.4;
  color: var(--mf-ink-dim);
}

/* Art cards carry the hero banner edge to edge above the text block */
.arcade-hub .mf-hub-card-art {
  padding: 0;
  gap: 0;
  overflow: hidden;
}

/* the banner wrapper holds the hero art and the floated favorite star */
.arcade-hub .mf-hub-card-banner {
  position: relative;
}

.arcade-hub .mf-hub-card-banner img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-bottom: 1px solid var(--mf-card-border);
}

/* the game name on its own line below the banner, for every card (Miro Flight
   keeps its baked-in art name too, by design) */
.arcade-hub .mf-hub-card-name {
  /* 5px, not 9px, above the name: the banner already ends in its own 1px rule and a
     15px/1.2 line box carries ~3px of leading above the caps, so a 9px margin read as
     a ~12px optical gap and floated the title midway between the art and the footer
     instead of labelling the picture. At 5px the name sits under its banner the way a
     caption sits under a photo, and the slack it gives up lands below it (the footer
     is pinned by margin-top:auto, so the card height is unchanged).
     The side margin drops 12px -> 10px to match the footer's tightened gutter. */
  margin: 5px 10px 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--mf-ink);
  /* centered under the banner: with the tagline gone the card is art plus one
     label, and a label under a picture reads centered, like an app tile. The
     footer below keeps its left and right pinned icons, which is fine because
     the eye reads art, then label, then footer as three separate bands. */
  text-align: center;
}

/* The tagline rule is gone with the tagline itself: the card now carries art,
   name and the footer only, and the footer's margin-top:auto still pins it to
   the bottom edge, so no replacement spacing is needed. */

/* Favorite: a round frosted-glass star floated on the banner top-right, going
   gold when the card is favorited - the premium little control from the mockup. */
.arcade-hub .mf-hub-fav {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  color: #fff;
  background: rgba(8, 11, 18, 0.42);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 999px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.14s ease, background 0.14s ease, transform 0.12s ease;
}

.arcade-hub .mf-hub-fav.on {
  color: var(--mf-gold);
  background: rgba(8, 11, 18, 0.55);
  border-color: rgba(255, 215, 94, 0.7);
}

@media (hover: hover) {
  .arcade-hub .mf-hub-fav:hover {
    background: rgba(8, 11, 18, 0.62);
    transform: scale(1.06);
  }

  .arcade-hub .mf-hub-fav.on:hover {
    color: var(--mf-gold);
  }
}

/* Level badge: the favorite star's MIRROR on the banner top-left, for the level-
   ladder games only (cards.ts paints it from the save blob's extra.levelLadder and
   only above level 1). Every value that defines the chip is copied from
   `.mf-hub-fav` above — 8px inset, 32px tall, 999px radius, the same frosted glass
   fill/border/blur, the same z-index over the hero art — so the two corner marks
   read as one pair rather than two borrowed controls. The star's offset is PHYSICAL
   (`right: 8px`), so this one is physical too (`left: 8px`): under RTL the whole hub
   flips `dir`, and a logical inset here would swap the badge onto the star's corner
   and stack the two on top of each other.

   THE ONLY DIFFERENCE IS WIDTH, and it is deliberate: the star is a fixed 32x32
   square because a glyph never grows, while this chip holds 1-4 glyphs. `min-width:
   32px` keeps the common 1-2 digit case an exact circle matching the star, and
   `padding: 0 8px` lets 3 and 4 glyphs stretch it into a pill on the SAME 999px
   radius and the SAME 32px height, so it grows sideways into empty banner instead of
   shrinking its type. Measured at 14px/800 with tabular figures: "2" -> 32px (circle),
   "12" -> 32px, "100" -> 41px, "999+" -> 51px. The narrowest card the grid ever
   produces is 150px (`repeat(auto-fill, minmax(150px, 1fr))`), i.e. a 148px banner,
   and the star takes 40px of the right edge, so even the 51px worst case leaves 57px
   of untouched hero art between the two marks.

   `pointer-events: none` because it reports, it does not act: without it the badge
   would swallow taps on the top-left of the banner, which belong to the card body
   (launch the game). The star and the trophy are buttons and stop their own clicks;
   this one must not. */
.arcade-hub .mf-hub-level {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 2;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  height: 32px;
  padding: 0 8px;
  font-size: 14px;
  font-weight: 800;
  line-height: 1;
  /* tabular figures so a "1" and a "9" occupy the same advance: without it the chip
     width jitters between levels of equal digit count */
  font-variant-numeric: tabular-nums;
  /* the badge is a NUMBER, not a sentence, so it renders identically in every locale.
     Without this the RTL locales (ar/fa/ur/bn-adjacent) inherit dir=rtl from the
     document and the bidi algorithm moves the neutral "+" of the capped "999+" to the
     other side, painting "+999" - a different claim ("over 999" read as a prefix).
     Isolating the chip to ltr keeps the cap suffix where it belongs; the chip's own
     position stays physically top-left (see the offset above), so this changes the
     glyph order only, never the corner. */
  direction: ltr;
  unicode-bidi: isolate;
  color: #fff;
  background: rgba(8, 11, 18, 0.42);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 999px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  pointer-events: none;
}

/* Play-shape badge: the third corner mark on the banner, bottom-left, carrying the
   staircase (level-based) or the lemniscate (endless) glyph the filter chips use. It
   is the same frosted glass chip as the level badge and the favorite star, so the
   banner's marks read as one family, and it takes the one corner still free: the star
   owns top-right, the level badge top-left, and bottom-right is left empty so nothing
   crowds the widest edge of a 16/9 hero.

   26px, not 32px: this badge is on EVERY classified card (all 76 of them) while the
   level badge appears on ~32 and only past level 1, so at equal size the two would
   fight and the pair of top corners would stop reading as the primary marks. A 14px
   glyph inside 26px keeps the same 6px optical padding the 16px-in-32px marks have.

   The offset is PHYSICAL (`left: 8px`) for the same reason the level badge's is: an
   RTL page flips the hub's `dir`, and a logical inset would swing this onto the star's
   side and stack the two.

   `pointer-events: none` because it reports, it does not act - the card is an anchor
   and a tap on the banner belongs to the card body (launch the game). The star and the
   trophy are buttons that stop their own clicks; this one must never swallow one. */
.arcade-hub .mf-hub-mode {
  position: absolute;
  bottom: 8px;
  left: 8px;
  z-index: 2;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  color: #fff;
  background: rgba(8, 11, 18, 0.42);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 999px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  pointer-events: none;
}

/* The footer meta row: the leaderboard trophy on the left, the skin mark dead
   centre, the crown tally on the right, each holding its own corner.

   A THREE-COLUMN GRID, not a space-between flex row. Two of the three marks are
   conditional (the crowns pill only once the game reports a tally, the skin button
   only for a game the catalog says sells skins), and space-between distributes
   whatever children are actually present: a card with the trophy and the skin but no
   crowns would push the skin to the RIGHT edge, and a card with trophy + crowns and
   no skin reads centre-less. With 1fr auto 1fr and each child pinned to its own
   column, the trophy is at the left edge, the crowns at the right edge and the skin
   at the true midpoint of the row on EVERY card, however many marks it has.

   THE WIDTH BUDGET (this is what every number below is solved against). The grid is
   `repeat(auto-fill, minmax(150px, 1fr))` with a 10px gap, so the NARROWEST a card
   ever gets is exactly 150px — the instant a new column fits, each card is back at
   its 150px floor (this happens around a 342px viewport for the 2nd column and a
   502px one for the 3rd, so it is a real layout, not a corner case). At 150px the
   card's content box is 148px (two 1px borders), the row is 148 - 2*8 = 132px, two
   4px gaps leave 124px for the three marks, and the widest triplet in this catalog
   measures 36 + 36 + 44 = 116px. 8px of headroom, i.e. the crown count could grow
   another whole character before anything touched an edge. The widest real count is
   merge-manor's `15/15` (15 progressList goals, the catalog maximum); the skin count
   tops out at `0/3`.

   The side margin drops 12px -> 8px and the gap 6px -> 4px: that +12px is exactly
   what pays for the marks growing 32px -> 36px and their counts 9/11px -> 12px. The
   bottom margin drops 9px -> 8px so the footer's gutter is square on three sides. The
   26px floor becomes 36px, matching the marks, so a row is never shorter than what it
   holds. margin-top:auto still pins the footer to the card's bottom edge whatever
   height the title took, so a 2-line title never moves a mark. */
.arcade-hub .mf-hub-meta {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 4px;
  min-height: 36px;
  margin: auto 8px 8px;
}

/* legacy spacer, no longer emitted now the trophy is the first (left) child */
.arcade-hub .mf-hub-meta-spacer {
  flex: 1 1 auto;
}

/* ===============================================================
   THE FOOTER MARK STANDARD — trophy, costume, crowns.

   ONE block sizes all three, because they are one control family and they used to
   read as three: a 32x32 neutral square (trophy), a 32x32 gold square holding a 14px
   tee over a 9px count (costume), and a 23px-tall 999px-radius gold pill holding a
   13px crown beside an 11px count (crowns). Three heights, two radii, three icon
   sizes, two count sizes. The shape difference carried no meaning — all three are
   "one mark, optionally with a progress number" — so it was pure noise, and the two
   counts were below the size a child can read at arm's length.

   The standard, solved against the 150px-card budget documented on .mf-hub-meta:

     height     36px   (was 32 / 32 / 23.4) — also the touch target, above the 32px
                       floor, so the marks got EASIER to hit, not harder
     min-width  36px   so a count-less mark (the trophy) is a true square and the
                       narrow counts (`0/1`, `0/3`) do not shrink into slivers
     radius     11px   (was 10 / 10 / 999) — one squircle, no stray pill
     padding    0 5px  the only thing that varies the width: a mark is as wide as its
                       count needs and no wider (`15/15` -> 44px, `0/3` -> 36px)
     icon       16px   (was 16 / 14 / 13) — set on the svg here AND in the markup
     count      12px/1 weight 800 (was --- / 9 / 11)

   Layout is STACKED (icon over count) for all three, not icon-beside-count: laid out
   side by side, a 16px icon + a 12px `15/15` + padding measures ~68px, and 36 + 68 +
   a costume mark blows through the 124px budget on a 150px card. Stacked, the widest
   mark in the catalog is 44px and the triplet fits with 8px to spare.

   Everything below this block only PAINTS (which column, which colour); nothing else
   re-declares a size, so the three can never drift apart again. */
.arcade-hub .mf-hub-board,
.arcade-hub .mf-hub-skin,
.arcade-hub .mf-hub-crowns {
  box-sizing: border-box;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 36px;
  height: 36px;
  padding: 0 5px;
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
  border: 1px solid transparent;
  border-radius: 11px;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.14s ease, background 0.14s ease, border-color 0.14s ease;
}

/* the one icon size, re-asserted on the element so a stale width/height attribute in
   the markup can never reintroduce the 13/14/16 spread this block exists to end */
.arcade-hub .mf-hub-board svg,
.arcade-hub .mf-hub-skin svg,
.arcade-hub .mf-hub-crowns svg {
  display: block;
  flex: none;
  width: 16px;
  height: 16px;
}

/* the one count size. `color: inherit` lets the -empty variants below dim the whole
   mark from the parent, and this rule sits AFTER the generic `.mf-hub-card span`
   (12px/1.4, dim ink) it has to beat at equal specificity. */
.arcade-hub .mf-hub-board span,
.arcade-hub .mf-hub-skin span,
.arcade-hub .mf-hub-crowns span {
  margin: 0;
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
  color: inherit;
}

/* ---- paint: the leaderboard trophy, bottom-LEFT (bottom-RIGHT under RTL, where
   grid column 1 and justify-self:start both flip with the writing direction). The
   neutral one: it opens a board rather than reporting progress, so it stays in card
   ink instead of gold and the two gold marks read as the pair they are. */
.arcade-hub .mf-hub-board {
  grid-column: 1;
  justify-self: start;
  color: var(--mf-ink-dim);
  background: var(--mf-bg-2);
  border-color: var(--mf-card-border);
  cursor: pointer;
}

@media (hover: hover) {
  .arcade-hub .mf-hub-board:hover {
    color: var(--mf-gold);
    border-color: rgba(255, 215, 94, 0.5);
  }
}

/* ---- paint: the costume shop mark, dead CENTRE. Gold like the crowns because it
   carries the same kind of owned/total progress; a real <button> because it opens
   the game's shop. */
.arcade-hub .mf-hub-skin {
  grid-column: 2;
  justify-self: center;
  color: var(--mf-gold);
  background: rgba(255, 215, 94, 0.1);
  border-color: rgba(255, 215, 94, 0.4);
  cursor: pointer;
}

@media (hover: hover) {
  .arcade-hub .mf-hub-skin:hover {
    color: var(--mf-gold);
    border-color: rgba(255, 215, 94, 0.65);
  }
}

/* nothing owned yet reads muted, exactly like the zero-earned crowns mark, so the
   shop is still visible (and openable) without shouting like an earned one */
.arcade-hub .mf-hub-skin-empty {
  color: rgba(255, 255, 255, 0.42);
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.16);
}

/* ---- paint: the earned/total crowns mark, bottom-RIGHT (bottom-LEFT under RTL).
   Pinned to the third column so it stays on its edge whether or not the middle
   costume mark is present. It is a real <button> now that it opens the game's quest
   list, so it takes the same pointer + hover treatment as the costume mark beside it;
   nothing else about it changes, since the shared footer-mark block above already
   sized and shaped all three the same. */
.arcade-hub .mf-hub-crowns {
  grid-column: 3;
  justify-self: end;
  color: var(--mf-gold);
  background: rgba(255, 215, 94, 0.1);
  border-color: rgba(255, 215, 94, 0.4);
  cursor: pointer;
}

@media (hover: hover) {
  .arcade-hub .mf-hub-crowns:hover {
    color: var(--mf-gold);
    border-color: rgba(255, 215, 94, 0.65);
  }
}

/* the zero-earned crowns mark reads muted/dim until the first crown is won, so a
   game's crown progress is always visible (0/8) without shouting like a complete one */
.arcade-hub .mf-hub-crowns-empty {
  color: rgba(255, 255, 255, 0.42);
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.16);
}

/* The FEATURED cards lead the grid - the owner's seven, in his sequence, painted by
   player/app/pages/hub/order.ts (FEATURED_IDS), which is also the list the ordering
   itself reads, so the marked cards are exactly the leading cards. A calm brighter
   border marks them without a colored glow that would clash with the hero art; that
   restraint matters more now the band is seven cards rather than one. */
.arcade-hub .mf-hub-card-pinned {
  border-color: rgba(255, 255, 255, 0.28);
}

/* A fully-crowned card glows gold so a completed game stands out in the grid.
   overflow:hidden clips the premium gold sheen sweep to the card's rounded edge. */
.arcade-hub .mf-hub-card-crowned {
  border-color: var(--mf-gold);
  box-shadow: 0 0 18px rgba(255, 215, 94, 0.45);
  overflow: hidden;
}

/* a slow, subtle gold sheen sweeps diagonally across a fully-crowned card so it
   reads premium/alive without being busy. pointer-events:none keeps the card
   tappable; it sweeps in ~2.6s then rests ~2.4s before repeating. */
.arcade-hub .mf-hub-card-crowned::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(
    115deg,
    transparent 40%,
    rgba(255, 235, 170, 0.0) 46%,
    rgba(255, 240, 190, 0.4) 50%,
    rgba(255, 235, 170, 0.0) 54%,
    transparent 60%
  );
  transform: translateX(-130%);
  animation: mf-hub-crown-sheen 5s ease-in-out infinite;
}

@keyframes mf-hub-crown-sheen {
  0% {
    transform: translateX(-130%);
  }
  52% {
    transform: translateX(130%);
  }
  100% {
    transform: translateX(130%);
  }
}

/* respect reduced-motion (and keep phones cool): a completed card keeps its gold
   glow + border but drops the moving sheen entirely */
@media (prefers-reduced-motion: reduce) {
  .arcade-hub .mf-hub-card-crowned::after {
    animation: none;
    opacity: 0;
  }
}

/* ***************************************************************
 * Leaderboard overlay
 * *************************************************************** */

/* Full-screen dim backdrop; a tap outside the sheet closes the board. The inner
   sheet only positions and bounds the mounted kit card. The card itself (its
   surface, border, title, tabs, rows and Back button) is the SAME kit leaderboard
   panel the games render in their menu, styled by the global leaderboard-shared
   stylesheet, so the popup reads identically here and in game. Only the kit board
   classes are styled globally; the few extra tokens that panel reads, beyond the
   ones the host block above declares, are added there so the card renders true.
   These overlay wrappers are .arcade-hub-scoped (hub-component-local in the source),
   but position:fixed; inset:0 still makes the backdrop cover the full viewport. */
.arcade-hub .mf-board-overlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(24px, env(safe-area-inset-top)) 12px max(16px, env(safe-area-inset-bottom));
  background: rgba(4, 6, 11, 0.66);
  backdrop-filter: blur(2px);
}

/* The sheet is a transparent centered bound for the kit card. It carries no
   surface of its own, since the kit card paints its own frame, glass and shadow;
   the sheet only caps the width and height so a long board scrolls inside the
   card rather than off the screen. */
.arcade-hub .mf-board-sheet {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 440px;
  max-height: 100%;
  min-height: 0;
}

/* The mount container the kit panel root is appended into. It fills the sheet so
   the card can use its own auto margins to center and its own overflow to scroll. */
.arcade-hub .mf-board-mount {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-height: 100%;
  min-height: 0;
}

/* The board title's inline trophy. These three rules live in the kit's
   leaderboard-shared stylesheet (so the in-game board reads them), but the player
   does not link that sheet, so without them the kit's stroke="currentColor" trophy
   falls back to the panel's white ink and sits unaligned at the top-left. Restated
   here, .arcade-hub-scoped like the overlay wrappers above, so the header shows a
   gold trophy inline right before the centered game-name title. */
.arcade-hub .mf-board-title {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.arcade-hub .mf-board-title-icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  color: var(--mf-gold);
}

.arcade-hub .mf-board-title-icon svg {
  display: block;
  width: 22px;
  height: 22px;
}

/* ***************************************************************
 * Wardrobe (costume) overlay
 * *************************************************************** */

/* The wardrobe overlay reuses the board overlay's three wrappers verbatim
   (.mf-board-overlay/.mf-board-sheet/.mf-board-mount above, plus the day-mode
   backdrop override further down) and the kit card + list classes shell.css already
   ships, so the two hub overlays are one design rather than two. The ONLY thing it
   adds is the line shown before the panel exists: the wardrobe, unlike the board,
   has to fetch the game's own string slices and preview art from that game's bundle
   before the first row can be drawn, and a sheet showing nothing at all in that
   window reads as broken. It is replaced by the panel the moment it is built, and is
   also where the "this game has no wardrobe" message lands. .mf-skins-overlay marks
   the overlay for this scope; it carries no styling of its own on purpose, so the
   backdrop, sheet and day-mode treatment stay exactly the board's. */
.arcade-hub .mf-skins-overlay .mf-skins-state,
.arcade-hub .mf-quests-overlay .mf-quests-state {
  margin: auto;
  padding: 18px 22px;
  border-radius: var(--mf-radius);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  font-size: 14px;
  line-height: 1.5;
  color: var(--mf-ink-dim);
  text-align: center;
}

/* ***************************************************************
 * Quest (goals) overlay
 * *************************************************************** */

/* The per-game quest overlay is the wardrobe's twin: it reuses the same three
   wrappers, the same waiting line (shared selector above) and the kit card + list
   classes shell.css already ships, so a third hub sheet adds no third design. The
   ONE thing of its own is the summary line at the top of the card, which restates
   the earned/total fraction the crown mark on the card printed - the sheet opens
   with the same number that opened it, before the eye has to count ticks down a
   list of ten rows. Gold like the mark, centered under the game name, and quiet
   enough that the rows below stay the content. */
.arcade-hub .mf-quests-tally {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 800;
  line-height: 1;
  color: var(--mf-gold);
  text-align: center;
}

/* ***************************************************************
 * Settings overlay
 * *************************************************************** */

/* The full-bleed backdrop behind the settings sheet, dimming the catalog. A tap
   on the backdrop closes the sheet; the sheet itself stops the click. */
.arcade-hub .mf-set-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(6, 8, 14, 0.62);
  /* keep the sheet clear of the device chrome on a notched phone */
  padding: 0 0 env(safe-area-inset-bottom);
}

/* The settings sheet: a mobile-portrait bottom sheet a thumb can reach. It caps
   its height and scrolls inside so a long body never pushes off-screen. */
.arcade-hub .mf-set-sheet {
  box-sizing: border-box;
  width: 100%;
  max-width: 520px;
  max-height: 86vh;
  overflow-y: auto;
  padding: 18px 18px max(20px, env(safe-area-inset-bottom));
  background: linear-gradient(160deg, var(--mf-bg-2) 0%, var(--mf-bg) 100%);
  border: 1px solid var(--mf-card-border);
  border-radius: 18px 18px 0 0;
  box-shadow: 0 -10px 30px -12px rgba(0, 0, 0, 0.8);
}

/* The sheet header carries the title and the close control */
.arcade-hub .mf-set-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 6px;
}

.arcade-hub .mf-set-head h2 {
  margin: 0;
  font-size: 17px;
  font-weight: 800;
  color: var(--mf-ink);
}

.arcade-hub .mf-set-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 11px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.arcade-hub .mf-set-close:active {
  background: var(--mf-bg-2);
}

/* Each labeled settings group, separated by a hairline so the sections read apart */
.arcade-hub .mf-set-section {
  padding: 14px 0;
  border-top: 1px solid var(--mf-card-border);
}

.arcade-hub .mf-set-section:first-of-type {
  border-top: none;
}

.arcade-hub .mf-set-label {
  margin-bottom: 10px;
  font-size: 13px;
  font-weight: 700;
  color: var(--mf-ink-dim);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.arcade-hub .mf-set-hint {
  margin-bottom: 10px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--mf-ink-dim);
}

/* The language picker: a wrap of pill buttons, each in its own script. The active
   language pill lights with the accent so the current choice reads at a glance. */
.arcade-hub .mf-set-langs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.arcade-hub .mf-set-lang {
  flex: 0 0 auto;
  min-height: 40px;
  padding: 0 16px;
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 11px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.arcade-hub .mf-set-lang.on {
  color: #fff;
  background: rgba(184, 168, 216, 0.22);
  border-color: var(--mf-accent);
}

/* The invite platform pills are the ONE pill row whose buttons carry two lines: the
   platform name, and under it where that platform's invite link actually lands the friend
   ("App Store", "Google Play", the bot handle). The second line exists because the iOS and
   web links are the opaque '<site>/i/<code>' landing route, which a player cannot read a
   destination out of. Everything else about the row - the wrap, the gap, the border, the
   `.on` highlight - is still inherited from .mf-set-langs / .mf-set-lang above, so a picked
   invite pill reads exactly like a picked language. */
.arcade-hub .mf-set-inviteplatforms .mf-set-lang {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 1px;
  padding: 6px 13px;
  line-height: 1.2;
  text-align: left;
}

.arcade-hub .mf-set-invitedest {
  font-size: 11px;
  font-weight: 600;
  color: var(--mf-ink-dim);
}

/* On the picked pill the whole label goes white-on-accent, so the dim token would drop
   the destination out of the highlight it sits inside. */
.arcade-hub .mf-set-lang.on .mf-set-invitedest {
  color: rgba(255, 255, 255, 0.82);
}

/* Avatar: the same pill row as the language / invite-platform pickers, with the pill
   holding a round mascot picture instead of a word. Only the shape of the pill changes -
   the wrap, the gap, the border and the `.on` highlight are all inherited from
   .mf-set-langs / .mf-set-lang above, so a picked avatar reads exactly like a picked
   language. Square and padding-free because the content is the art itself. */
.arcade-hub .mf-set-avatar {
  width: 52px;
  height: 52px;
  padding: 0;
  overflow: hidden;
  border-radius: 50%;
}

.arcade-hub .mf-set-avatar-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A tile whose portrait could not be drawn keeps the pill and shows the mascot's initials
   instead, so an art set that is still landing reads as lettered tiles and never as a grid
   of broken-image glyphs. Smaller than a language pill's word because two letters have to
   sit inside a 52px circle. */
.arcade-hub .mf-set-avatar-miss {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.5px;
  color: var(--mf-ink-dim);
}

/* THE MASCOT GRID SCROLLS INSIDE ITSELF. One tile per game is seventy-seven of them, which
   flat would be a fourteen-row wall the player has to scroll past to reach Sound. Capped at
   a fraction of the viewport and scrolled internally, the section stays the same height as
   the language picker whatever the catalog grows to.
   overflow-x is hidden (never a sideways scroll on the sheet - the tiles are fixed width and
   simply wrap), overscroll-behavior keeps a flick at the end of the grid from scrolling the
   sheet behind it, and position: relative makes the tiles' offsetTop measure against this
   box, which is what the "scroll the worn mascot into view" pass reads. */
.arcade-hub .mf-set-avatars {
  position: relative;
  max-height: 44vh;
  padding: 4px 2px;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* The current pick, sitting in the collapsed row as its value: the face the player is
   wearing, at a size where the mascot is actually legible, with the initials underneath as
   the fallback for a portrait that will not load. */
.arcade-hub .mf-set-avatarcur {
  position: relative;
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  overflow: hidden;
  background: var(--mf-surface);
  border: 2px solid var(--mf-accent);
  border-radius: 50%;
}

.arcade-hub .mf-set-avatarcur-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The initials layer, absolutely placed so the picture covers it exactly when it loads. */
.arcade-hub .mf-set-avatarcur-fb {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  font-weight: 800;
  color: var(--mf-ink-dim);
}

/* The avatar row is the language row with a face for its value, so it inherits .mf-set-row
   and .mf-set-langrow whole; only the taller value needs its own breathing room. */
.arcade-hub .mf-set-avatarrow {
  padding: 6px 0;
}

/* Language: a collapsed row (label + current native name + chevron) that opens a
   search box over the pill grid, so the 30-language wall no longer fills the sheet. */
.arcade-hub .mf-set-langcur {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 700;
  color: var(--mf-ink-dim);
}

.arcade-hub .mf-set-chevron {
  font-size: 11px;
  line-height: 1;
  transition: transform 0.18s ease;
}

.arcade-hub .mf-set-langrow.open .mf-set-chevron {
  transform: rotate(180deg);
}

.arcade-hub .mf-set-langpicker {
  margin-top: 10px;
}

.arcade-hub .mf-set-langsearch {
  width: 100%;
  box-sizing: border-box;
  margin-bottom: 10px;
}

/* Theme: a sun / moon icon segment (language-independent) instead of localized text.
   The track uses the surface + border tokens so it reads on both day and night. */
.arcade-hub .mf-set-themeseg {
  display: inline-flex;
  gap: 4px;
  padding: 3px;
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 999px;
}

.arcade-hub .mf-set-themebtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 30px;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--mf-ink-dim);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.arcade-hub .mf-set-themebtn.on {
  background: var(--mf-accent);
  color: #fff;
}

/* The sound row: a full-width tappable row with a label and a slide switch */
.arcade-hub .mf-set-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 4px 0;
  font-family: inherit;
  background: transparent;
  border: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.arcade-hub .mf-set-row-label {
  font-size: 15px;
  font-weight: 600;
  color: var(--mf-ink);
}

/* The slide switch: an off track that turns accent when sound is on */
.arcade-hub .mf-set-switch {
  position: relative;
  flex: 0 0 auto;
  width: 46px;
  height: 28px;
  background: rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  transition: background 0.15s ease;
}

.arcade-hub .mf-set-switch.on {
  background: var(--mf-accent);
}

.arcade-hub .mf-set-knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 22px;
  height: 22px;
  background: #fff;
  border-radius: 50%;
  transition: transform 0.15s ease;
}

.arcade-hub .mf-set-switch.on .mf-set-knob {
  transform: translateX(18px);
}

/* The music volume slider row: a label on the left, the range filling the rest */
.arcade-hub .mf-set-sliderrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  width: 100%;
  padding: 2px 0 4px;
}

.arcade-hub .mf-set-sliderrow .mf-set-row-label {
  flex: 0 0 auto;
}

/* A flat range input themed to the accent track + white knob like the switch */
.arcade-hub .mf-set-slider {
  flex: 1 1 auto;
  min-width: 0;
  height: 28px;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  cursor: pointer;
}

.arcade-hub .mf-set-slider::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.14);
}

.arcade-hub .mf-set-slider::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.14);
}

.arcade-hub .mf-set-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  margin-top: -8px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--mf-accent);
  border: 3px solid #fff;
  box-sizing: border-box;
}

.arcade-hub .mf-set-slider::-moz-range-thumb {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--mf-accent);
  border: 3px solid #fff;
  box-sizing: border-box;
}

/* A dimmed, non-interactive look for the music controls while sound (or music) is
   off, so the group reads as subordinate to the sound toggle */
.arcade-hub .mf-set-dim {
  opacity: 0.4;
}

.arcade-hub .mf-set-slider:disabled {
  cursor: default;
}

/* The invite and promo rows: an input filling the row with a button beside it */
.arcade-hub .mf-set-inviterow,
.arcade-hub .mf-set-promorow {
  display: flex;
  gap: 8px;
  align-items: stretch;
}

.arcade-hub .mf-set-input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  box-sizing: border-box;
  padding: 10px 12px;
  font-family: inherit;
  font-size: 14px;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 11px;
  outline: none;
}

.arcade-hub .mf-set-input:focus {
  border-color: var(--mf-accent);
}

.arcade-hub .mf-set-input::placeholder {
  color: var(--mf-ink-dim);
}

.arcade-hub .mf-set-btn {
  flex: 0 0 auto;
  min-height: 44px;
  padding: 0 16px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 800;
  color: #fff;
  background: rgba(184, 168, 216, 0.22);
  border: 1px solid var(--mf-accent);
  border-radius: 11px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.arcade-hub .mf-set-btn:active {
  background: rgba(184, 168, 216, 0.34);
}

.arcade-hub .mf-set-btn:disabled {
  opacity: 0.55;
  cursor: default;
}

/* The promo result line under the apply row */
.arcade-hub .mf-set-status {
  margin-top: 10px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--mf-ink);
}

/* feedback section: a full-width multiline textarea, a ghost attach button + photo
   name, and a send button spaced under the attach row */
.arcade-hub .mf-set-textarea {
  width: 100%;
  display: block;
  margin-top: 8px;
  resize: vertical;
  min-height: 76px;
  line-height: 1.45;
}

.arcade-hub .mf-set-btn-ghost {
  color: var(--mf-ink);
  background: transparent;
  border-color: var(--mf-card-border);
  font-weight: 700;
}

.arcade-hub .mf-set-photoname {
  align-self: center;
  font-size: 13px;
  color: var(--mf-ink-dim);
}

/* the feedback send button is the only direct-child button of a section (the promo
   apply button lives inside a .mf-set-promorow), so this spaces only the send button */
.arcade-hub .mf-set-section > button.mf-set-btn {
  margin-top: 10px;
}

/* ***************************************************************
 * Legal section (privacy / terms links + delete-my-data)
 * *************************************************************** */

/* The little open-affordance chevron on the right of each legal link row (Privacy /
   Terms). The row itself is a .mf-set-row (label left, this glyph right); dim so it
   reads as a quiet forward affordance, not a control. */
.arcade-hub .mf-set-legal-arrow {
  flex: 0 0 auto;
  font-size: 18px;
  line-height: 1;
  color: var(--mf-ink-dim);
}

/* The delete-my-data wrapper, separated from the Terms row above it. Holds the danger
   button, its inline confirm line and the result status, which swap in place. */
.arcade-hub .mf-set-legal-delete {
  margin-top: 12px;
}

/* The inline confirm actions row: the Delete + Cancel buttons side by side, each
   filling half the row so neither dominates. */
.arcade-hub .mf-set-legal-actions {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}

.arcade-hub .mf-set-legal-actions .mf-set-btn {
  flex: 1 1 0;
}

/* The DANGER button variant, layered over .mf-set-btn for the destructive delete
   controls (the "Delete my data" button and the confirm "Delete"). A red-tinted
   outline rather than a loud solid fill, so it reads as destructive without shouting
   over the sheet. Declared AFTER the base .mf-set-btn rules so its colors win on the
   button that carries both classes. */
.arcade-hub .mf-set-btn-danger {
  color: #ff6b6b;
  background: rgba(224, 74, 74, 0.12);
  border-color: rgba(224, 74, 74, 0.5);
}

.arcade-hub .mf-set-btn-danger:active {
  background: rgba(224, 74, 74, 0.22);
}

/* ***************************************************************
 * Legal document viewer overlay
 * *************************************************************** */

/* The full-screen legal viewer, opened from the settings LEGAL section OVER the
   settings sheet, so its z-index (60) sits above the settings overlay (50). It shows a
   bundled privacy/terms page in an iframe. Unlike the settings/board overlays a tap on
   the backdrop does NOT close it (there is no backdrop click handler); only the header
   X dismisses it, since a legal read is deliberate. Top padding clears the device /
   Telegram top inset so the header never hides under the notch or the client chrome. */
.arcade-hub .mf-legal-overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: max(24px, env(safe-area-inset-top));
  background: rgba(6, 8, 14, 0.86);
}

/* The panel framing the document: a centered full-height column (phone = full width,
   desktop = a 520px reading column matching the settings sheet cap) carrying the
   header bar over the frame. Its own opaque page fill covers the backdrop behind it. */
.arcade-hub .mf-legal-panel {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  max-width: 520px;
  background: var(--mf-page-bg);
}

/* The header bar: the document title on the left and the X close on the right, split
   apart over a hairline like the sheet section dividers. */
.arcade-hub .mf-legal-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--mf-card-border);
}

.arcade-hub .mf-legal-title {
  margin: 0;
  font-size: 17px;
  font-weight: 800;
  color: var(--mf-ink);
}

/* The X close, the exact shape + tokens as the settings sheet's .mf-set-close so the
   two headers read identically. */
.arcade-hub .mf-legal-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 11px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.arcade-hub .mf-legal-close:active {
  background: var(--mf-bg-2);
}

/* The document frame fills the rest of the panel edge to edge, no border. */
.arcade-hub .mf-legal-frame {
  flex: 1 1 auto;
  width: 100%;
  min-height: 0;
  border: 0;
}

/* ***************************************************************
 * Parental gate (native Android only — Families-policy link check)
 * *************************************************************** */

/* The full-bleed backdrop behind the parental gate, dimming the catalog / settings
   sheet beneath it. A tap on the backdrop aborts (the link does NOT open); the card
   stops the click. Its z-index (70) sits above the settings sheet (50), the first-launch
   notice (55) and the legal viewer (60) so the gate is always the topmost surface when
   an external link is tapped. Native-only: nothing builds this on Telegram / web. */
.arcade-hub .mf-gate-overlay {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(6, 8, 14, 0.72);
}

/* The centered gate card: a compact modal a thumb can reach, capped so a small phone
   never overflows. Matches the settings sheet fill + border so the two read as one UI. */
.arcade-hub .mf-gate-card {
  box-sizing: border-box;
  width: 100%;
  max-width: 360px;
  padding: 20px;
  background: linear-gradient(160deg, var(--mf-bg-2) 0%, var(--mf-bg) 100%);
  border: 1px solid var(--mf-card-border);
  border-radius: 16px;
  box-shadow: 0 18px 40px -16px rgba(0, 0, 0, 0.85);
  text-align: center;
}

/* The gate title, matching the settings header weight/size */
.arcade-hub .mf-gate-title {
  margin: 0 0 6px;
  font-size: 17px;
  font-weight: 800;
  color: var(--mf-ink);
}

/* The gate instruction line, the dimmer supporting copy like a settings hint */
.arcade-hub .mf-gate-body {
  margin: 0 0 12px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--mf-ink-dim);
}

/* The question line ("A x B = ?"), the loud focal prompt the grown-up answers */
.arcade-hub .mf-gate-question {
  margin: 0 0 12px;
  font-size: 26px;
  font-weight: 800;
  letter-spacing: 1px;
  color: var(--mf-ink);
}

/* The numeric answer field, centered under the question. Reuses the settings input look
   (surface + hairline + accent focus) at a larger centered type for the digits. */
.arcade-hub .mf-gate-input {
  width: 100%;
  box-sizing: border-box;
  min-height: 48px;
  margin: 0 0 14px;
  padding: 10px 12px;
  font-family: inherit;
  font-size: 20px;
  font-weight: 700;
  text-align: center;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 11px;
  outline: none;
}

.arcade-hub .mf-gate-input:focus {
  border-color: var(--mf-accent);
}

/* The actions row: OK + Cancel side by side, each filling half the row */
.arcade-hub .mf-gate-actions {
  display: flex;
  gap: 8px;
}

.arcade-hub .mf-gate-actions .mf-set-btn {
  flex: 1 1 0;
}

/* The Cancel button's neutral variant: a transparent, hairline-outlined button so the
   abort action reads quieter than the primary OK. Its own class (not the shared
   `.mf-set-btn-ghost`) because the day `.mf-set-btn` override would otherwise flatten a
   ghost button to the same accent fill as OK. Declared AFTER the base `.mf-set-btn` so
   it wins at equal specificity in the night theme; the day override is re-asserted in
   the DAY theme block below. */
.arcade-hub .mf-gate-cancel {
  color: var(--mf-ink);
  background: transparent;
  border-color: var(--mf-card-border);
  font-weight: 700;
}

/* The wrong-answer shake: a short horizontal wobble applied to the card on a miss. The
   builder removes then re-adds the class (forcing a reflow between) so a repeat miss
   restarts the animation. */
@keyframes mfGateShake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-7px); }
  40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(3px); }
}

.arcade-hub .mf-gate-card.mf-gate-shake {
  animation: mfGateShake 0.32s ease;
}

/* ***************************************************************
 * First-launch notice (native Android only — one-time device-id disclosure)
 * *************************************************************** */

/* The backdrop behind the one-time first-launch notice, dimming the catalog on the very
   first native boot. It carries NO click-to-close (acknowledged via OK). Its z-index
   (55) sits above the settings sheet (50) and below the legal viewer (60) so the Privacy
   link opens the viewer OVER it. Native-only: nothing builds this on Telegram / web. */
.arcade-hub .mf-notice-overlay {
  position: fixed;
  inset: 0;
  z-index: 55;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(6, 8, 14, 0.62);
  /* keep the card clear of the device chrome on a notched phone */
  padding: 0 0 env(safe-area-inset-bottom);
}

/* The notice card: a bottom sheet matching the settings sheet fill / radius / border so
   the one-time disclosure reads as part of the same UI. */
.arcade-hub .mf-notice-card {
  box-sizing: border-box;
  width: 100%;
  max-width: 520px;
  padding: 20px 18px max(20px, env(safe-area-inset-bottom));
  background: linear-gradient(160deg, var(--mf-bg-2) 0%, var(--mf-bg) 100%);
  border: 1px solid var(--mf-card-border);
  border-radius: 18px 18px 0 0;
  box-shadow: 0 -10px 30px -12px rgba(0, 0, 0, 0.8);
}

/* The welcome title */
.arcade-hub .mf-notice-title {
  margin: 0 0 8px;
  font-size: 18px;
  font-weight: 800;
  color: var(--mf-ink);
}

/* The disclosure body copy */
.arcade-hub .mf-notice-body {
  margin: 0 0 12px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--mf-ink-dim);
}

/* The Privacy Policy text-link: an inline accent link (not a filled button) so it reads
   as a secondary read-more beside the primary OK button below it. */
.arcade-hub .mf-notice-link {
  display: inline-block;
  margin: 0 0 16px;
  padding: 0;
  font-family: inherit;
  font-size: 14px;
  font-weight: 700;
  color: var(--mf-accent);
  background: transparent;
  border: none;
  text-decoration: underline;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* The primary OK button fills the sheet width so the acknowledge action is unmissable.
   Layout only here; the fill/ink come from the shared `.mf-set-btn` (and its day override). */
.arcade-hub .mf-notice-ok {
  display: block;
  width: 100%;
}

/* The daily game-tip soft-ask reuses the notice shell but offers TWO choices, so its
   actions sit in a split row like the parental gate: Yes (primary accent) beside No (quiet
   outline), each filling half the row. Native-only surface; no web effect. */
.arcade-hub .mf-notice-actions {
  display: flex;
  gap: 8px;
}

.arcade-hub .mf-notice-actions .mf-set-btn {
  flex: 1 1 0;
}

/* The "No thanks" decline: a transparent, hairline-outlined button so it reads quieter than
   the accent Yes. Its own class (not the shared `.mf-set-btn-ghost`) because the day
   `.mf-set-btn` override would otherwise flatten a ghost button to the same accent fill as
   Yes. Declared AFTER the base `.mf-set-btn` so it wins at equal specificity in the night
   theme; the day override is re-asserted in the DAY theme block below. */
.arcade-hub .mf-notice-no {
  color: var(--mf-ink);
  background: transparent;
  border-color: var(--mf-card-border);
  font-weight: 700;
}

/* ***************************************************************
 * DAY theme — hardcoded-value overrides
 * *************************************************************** */

/* Every rule below is additive and day-scoped (selector begins with
   :root[data-theme="day"]); it repoints the few hardcoded colors that would break
   on the warm parchment to the day palette. Gold (#ffd75e) is left alone — crown /
   trophy semantics stay gold in both themes. Night visuals are untouched. */

/* Brand lockup: the night pink/grey marks become the bordeaux + grey day marks. */
:root[data-theme="day"] .arcade-hub .mf-brand-mire {
  color: #d96f8a;
}

:root[data-theme="day"] .arcade-hub .mf-brand-games {
  color: #b8b0c2;
}

/* Make the card body transparent in day so the fixed doodle wallpaper shows
   THROUGH the text footer beneath each hero banner (the banner img stays opaque
   up top). On phones the cards fill the screen, so this is where the doodle
   actually reads. The light border still frames each card. */
:root[data-theme="day"] .arcade-hub .mf-hub-card {
  /* a light 0.3 parchment veil (not fully transparent) so the fixed doodle reads
     softly behind the card text footer while keeping the title/tagline crisp */
  background: rgba(255, 255, 255, 0.3);
}

/* Card hover fill (desktop only): a cleaner, more opaque card on hover. */
:root[data-theme="day"] .arcade-hub .mf-hub-card:hover {
  background: rgba(255, 255, 255, 0.6);
}

/* Favorite star: the dark frosted chip becomes a light warm chip so the star
   reads on a light banner/card. */
:root[data-theme="day"] .arcade-hub .mf-hub-fav {
  color: #2c2a26;
  background: rgba(255, 255, 255, 0.7);
  border-color: rgba(0, 0, 0, 0.12);
}

:root[data-theme="day"] .arcade-hub .mf-hub-fav.on {
  color: var(--mf-gold);
  background: rgba(255, 255, 255, 0.85);
  border-color: rgba(255, 215, 94, 0.7);
}

@media (hover: hover) {
  :root[data-theme="day"] .arcade-hub .mf-hub-fav:hover {
    background: rgba(255, 255, 255, 0.92);
  }
}

/* Level badge: the same dark-chip -> light-chip swap the star takes one block above,
   because the two are one pair and a day theme that lightened only the right corner
   would break the symmetry the badge exists to keep. Same values as the day star, so
   the digits read on a light banner exactly as the star does. */
:root[data-theme="day"] .arcade-hub .mf-hub-level {
  color: #2c2a26;
  background: rgba(255, 255, 255, 0.7);
  border-color: rgba(0, 0, 0, 0.12);
}

/* Board + settings backdrops: the heavy near-black scrims become a lighter warm
   scrim so the dimmed catalog still reads light behind the sheet. */
:root[data-theme="day"] .arcade-hub .mf-board-overlay {
  background: rgba(44, 42, 38, 0.34);
}

:root[data-theme="day"] .arcade-hub .mf-set-overlay {
  background: rgba(44, 42, 38, 0.34);
}

/* Active language pill + primary settings button: the night white-on-wash look
   would wash out on parchment, so use day ink/accent so the active/primary state
   reads. */
:root[data-theme="day"] .arcade-hub .mf-set-lang.on {
  color: #ffffff;
  background: var(--mf-accent);
}

:root[data-theme="day"] .arcade-hub .mf-set-btn {
  color: #ffffff;
  background: var(--mf-accent);
}

:root[data-theme="day"] .arcade-hub .mf-set-btn:active {
  background: #6e0b3f;
}

/* Danger button on parchment: the day .mf-set-btn override above repaints EVERY
   .mf-set-btn solid accent, so the destructive variant must be re-asserted here (same
   specificity, declared later) with a darker red that reads on the light surface. */
:root[data-theme="day"] .arcade-hub .mf-set-btn-danger {
  color: #c0392b;
  background: rgba(192, 57, 43, 0.08);
  border-color: rgba(192, 57, 43, 0.42);
}

:root[data-theme="day"] .arcade-hub .mf-set-btn-danger:active {
  background: rgba(192, 57, 43, 0.16);
}

/* Legal viewer scrim: lighten the near-black backdrop to the warm day scrim like the
   board + settings overlays, so the parchment reads light on the panel's side gutters. */
:root[data-theme="day"] .arcade-hub .mf-legal-overlay {
  background: rgba(44, 42, 38, 0.5);
}

/* Switch + slider tracks/knobs: the white-on-dark translucent tracks vanish on a
   light surface, so day uses a dark-on-light track and a tinted knob. */
:root[data-theme="day"] .arcade-hub .mf-set-switch {
  background: rgba(0, 0, 0, 0.14);
}

/* The day track rule above carries four selector components, so it outranks the
   base `.arcade-hub .mf-set-switch.on` accent rule and was painting an ON switch
   in the OFF track colour: in day mode every switch read identically on and off,
   and the only remaining cue was the knob sliding 18px. The accent has to be
   restated at the day block's own weight to win it back. Night is untouched. */
:root[data-theme="day"] .arcade-hub .mf-set-switch.on {
  background: var(--mf-accent);
}

:root[data-theme="day"] .arcade-hub .mf-set-slider::-webkit-slider-runnable-track {
  background: rgba(0, 0, 0, 0.14);
}

:root[data-theme="day"] .arcade-hub .mf-set-slider::-moz-range-track {
  background: rgba(0, 0, 0, 0.14);
}

/* Empty-crowns mark: the white-on-dark muted chip becomes a dark-on-light muted chip
   so a 0/N crown count stays legible on parchment. The count needs no rule of its
   own: the shared footer-mark block gives every count `color: inherit`, so dimming
   the parent here dims the number with it. */
:root[data-theme="day"] .arcade-hub .mf-hub-crowns-empty {
  color: rgba(44, 42, 38, 0.5);
  background: rgba(0, 0, 0, 0.04);
  border-color: rgba(0, 0, 0, 0.12);
}

/* Nothing-owned skin button: same swap as the empty crowns pill above, so a 0/N skin
   mark stays legible on parchment instead of vanishing into it. The owned (gold)
   state needs no override — it reads on both surfaces like the earned crowns pill. */
:root[data-theme="day"] .arcade-hub .mf-hub-skin-empty {
  color: rgba(44, 42, 38, 0.5);
  background: rgba(0, 0, 0, 0.04);
  border-color: rgba(0, 0, 0, 0.12);
}

/* Pinned-card edge: the bright white border that marked the featured cards on dark
   would disappear on parchment, so day uses the accent at low alpha to keep the lead
   cards subtly distinguished. Same set as the dark rule above - the featured seven
   from player/app/pages/hub/order.ts, never a catalog flag. */
:root[data-theme="day"] .arcade-hub .mf-hub-card-pinned {
  border-color: rgba(136, 14, 79, 0.35);
}

/* Native parental gate + first-launch notice scrims: lighten the near-black backdrops
   to the warm day scrim like the board / settings / legal overlays, so the parchment
   reads light on the gutters behind the card. (Native-only surfaces; no web effect.) */
:root[data-theme="day"] .arcade-hub .mf-gate-overlay {
  background: rgba(44, 42, 38, 0.5);
}

:root[data-theme="day"] .arcade-hub .mf-notice-overlay {
  background: rgba(44, 42, 38, 0.34);
}

/* Gate Cancel on parchment: the day `.mf-set-btn` override repaints EVERY `.mf-set-btn`
   solid accent, so the neutral Cancel must be re-asserted here (same specificity as that
   override, declared later) to stay a quiet outlined button distinct from the accent OK. */
:root[data-theme="day"] .arcade-hub .mf-gate-cancel {
  color: var(--mf-ink);
  background: transparent;
  border-color: var(--mf-card-border);
}

/* Daily-tip decline on parchment: same reasoning as the gate Cancel above - the day
   `.mf-set-btn` override repaints EVERY `.mf-set-btn` solid accent, so the neutral No must be
   re-asserted here (same specificity, declared later) to stay a quiet outlined button
   distinct from the accent Yes. */
:root[data-theme="day"] .arcade-hub .mf-notice-no {
  color: var(--mf-ink);
  background: transparent;
  border-color: var(--mf-card-border);
}

/* ***************************************************************
 * Social surfaces: friends button, friends overlay, quest board,
 * the one-line name ask
 * *************************************************************** */

/* The friends button in the tool row, one step left of the gear. It is an EXACT copy of
   the `.mf-hub-gear` frame (42x42, same surface, border, radius and press state) rather
   than a new look, because the two sit side by side and any difference between them
   would read as one of them being broken. Only the glyph inside differs. */
.arcade-hub .mf-hub-friends {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  padding: 0;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 12px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.arcade-hub .mf-hub-friends:active {
  background: var(--mf-bg-2);
}

/* The crown tally became a <button> so it can open the quest board. These are the button
   RESET only: it must keep looking exactly like the inline text tally it was (the
   `.mf-hub-total-crowns` rule above still supplies the layout, weight and gold), because
   nothing about the header's appearance was meant to change - only its behaviour. */
.arcade-hub button.mf-hub-total-crowns {
  padding: 0;
  font: inherit;
  font-weight: 800;
  background: none;
  border: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.arcade-hub button.mf-hub-total-crowns:active {
  opacity: 0.7;
}

/* ---- shared card surface for the two hub-owned overlays ----------------------------
   The friends panel and the quest board mount into the SAME .mf-board-overlay /
   .mf-board-sheet / .mf-board-mount wrappers the leaderboard and wardrobe use, but their
   content is the hub's own rather than a kit card, so the card surface is declared here.
   It copies the kit dialog card's frame values so all four overlays read as one family. */
.arcade-hub .mf-friends-card,
.arcade-hub .mf-quest-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
  max-height: 100%;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  box-sizing: border-box;
  padding: 18px 16px;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 18px;
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.42);
}

.arcade-hub .mf-friends-title,
.arcade-hub .mf-quest-title {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 18px;
  font-weight: 800;
}

.arcade-hub .mf-friends-title-icon,
.arcade-hub .mf-quest-title-icon {
  font-size: 20px;
  line-height: 1;
}

/* ---- friends: the primary share action ---- */

/* Deliberately the largest, loudest control on the card: handing a link to somebody the
   player already talks to is the path that actually results in a friend, so it gets the
   accent and the full width while the code paste below stays a quiet secondary row. */
.arcade-hub .mf-friends-share {
  min-height: 46px;
  padding: 12px 16px;
  font-family: inherit;
  font-size: 15px;
  font-weight: 800;
  color: var(--mf-bg);
  background: var(--mf-accent);
  border: none;
  border-radius: 12px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.arcade-hub .mf-friends-share:disabled {
  opacity: 0.45;
  cursor: default;
}

/* ---- friends: the player's own code ---- */

.arcade-hub .mf-friends-coderow {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  background: var(--mf-bg-2);
  border: 1px solid var(--mf-card-border);
  border-radius: 12px;
}

.arcade-hub .mf-friends-codelabel {
  flex: 0 0 auto;
  font-size: 13px;
  color: var(--mf-ink-dim);
}

/* The code itself is monospace and allowed to break anywhere: it is a token to be read
   out or copied, not a word, and `min-width: 0` is what stops a long one from pushing the
   copy button off the row on a narrow phone. */
.arcade-hub .mf-friends-code {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  font-weight: 700;
}

.arcade-hub .mf-friends-copy {
  flex: 0 0 auto;
  padding: 7px 12px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 9px;
  cursor: pointer;
}

/* ---- friends: the paste-a-code row ---- */

.arcade-hub .mf-friends-hint {
  font-size: 13px;
  color: var(--mf-ink-dim);
}

.arcade-hub .mf-friends-addrow {
  display: flex;
  align-items: center;
  gap: 8px;
}

.arcade-hub .mf-friends-input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 42px;
  box-sizing: border-box;
  padding: 10px 12px;
  font-family: inherit;
  font-size: 15px;
  color: var(--mf-ink);
  background: var(--mf-bg-2);
  border: 1px solid var(--mf-card-border);
  border-radius: 11px;
  outline: none;
}

.arcade-hub .mf-friends-input:focus {
  border-color: var(--mf-accent);
}

.arcade-hub .mf-friends-add {
  flex: 0 0 auto;
  min-height: 42px;
  padding: 10px 16px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 700;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border: 1px solid var(--mf-card-border);
  border-radius: 11px;
  cursor: pointer;
}

.arcade-hub .mf-friends-add:disabled {
  opacity: 0.45;
  cursor: default;
}

/* The single status line for the add action. It is empty (and therefore invisible)
   until an add has actually been attempted, so the card does not carry a blank row. */
.arcade-hub .mf-friends-status:empty {
  display: none;
}

.arcade-hub .mf-friends-status {
  font-size: 13px;
  color: var(--mf-ink-dim);
}

/* Success and failure are DIFFERENT COLOURS, not different words in the same dim grey.
   The add action used to paint every answer in the line colour above, so a refusal and a
   confirmation were the same pixel until you read them - which is how a failed add came to
   look like a working one. Weight goes up with the colour so the line still reads on a
   screen where the hue is hard to see. */
.arcade-hub .mf-friends-status-ok {
  color: #45c47a;
  font-weight: 700;
}

.arcade-hub .mf-friends-status-bad {
  color: #ff6b6b;
  font-weight: 700;
}

/* Day mode paints this card on parchment (--mf-surface goes light), where both of the
   night inks above wash out. Darker shades of the same two hues, so the meaning of the
   colour does not change between themes. Higher specificity, so order does not matter. */
:root[data-theme="day"] .arcade-hub .mf-friends-status-ok {
  color: #1f7a45;
}

:root[data-theme="day"] .arcade-hub .mf-friends-status-bad {
  color: #b3261e;
}

/* ---- friends: the request + friend lists ---- */

.arcade-hub .mf-friends-block {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.arcade-hub .mf-friends-subtitle {
  font-size: 13px;
  font-weight: 800;
  color: var(--mf-ink-dim);
  text-transform: uppercase;
  letter-spacing: 0.4px;
}

.arcade-hub .mf-friends-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.arcade-hub .mf-friends-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  background: var(--mf-bg-2);
  border-radius: 11px;
}

/* The avatar cell: a colored initial with the photo layered over it, so a photoless or
   blocked-image friend still reads as a person. `position: relative` plus the absolutely
   filled img is what lets the initial show through when the image removes itself. */
.arcade-hub .mf-friends-avatar,
.arcade-hub .mf-quest-avatar {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  overflow: hidden;
  font-size: 14px;
  font-weight: 800;
  color: #fff;
  border-radius: 50%;
}

.arcade-hub .mf-friends-avatar img,
.arcade-hub .mf-quest-avatar img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Player-supplied text: it must never push the action buttons off the row, so it is the
   one flexible cell and it truncates rather than wraps. */
.arcade-hub .mf-friends-name,
.arcade-hub .mf-quest-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  font-size: 14px;
  font-weight: 700;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.arcade-hub .mf-friends-act {
  flex: 0 0 auto;
  padding: 6px 10px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  color: var(--mf-ink-dim);
  background: transparent;
  border: 1px solid var(--mf-card-border);
  border-radius: 9px;
  cursor: pointer;
}

/* Accept is the affirmative action of the pair, so it carries the accent while Decline
   and Remove stay quiet outlines - the destructive choice must never be the easy one. */
.arcade-hub .mf-friends-act-on {
  color: var(--mf-bg);
  background: var(--mf-accent);
  border-color: transparent;
}

.arcade-hub .mf-friends-empty {
  padding: 10px 2px;
  font-size: 13px;
  color: var(--mf-ink-dim);
  text-align: center;
}

.arcade-hub .mf-friends-close,
.arcade-hub .mf-quest-close {
  min-height: 42px;
  padding: 11px 16px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 700;
  color: var(--mf-ink);
  background: var(--mf-bg-2);
  border: 1px solid var(--mf-card-border);
  border-radius: 12px;
  cursor: pointer;
}

/* ---- quest board ---- */

.arcade-hub .mf-quest-sub {
  font-size: 12.5px;
  color: var(--mf-ink-dim);
  text-align: center;
}

/* The player's own standing, pinned above the global list so the board says something
   true about the reader even when the list itself is empty. */
.arcade-hub .mf-quest-self {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 12px;
  background: rgba(247, 201, 72, 0.12);
  border: 1px solid rgba(247, 201, 72, 0.4);
  border-radius: 12px;
}

.arcade-hub .mf-quest-self-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  font-size: 14px;
  font-weight: 800;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.arcade-hub .mf-quest-self-pct {
  flex: 0 0 auto;
  font-size: 15px;
  font-weight: 800;
  color: var(--mf-gold);
}

.arcade-hub .mf-quest-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 46vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.arcade-hub .mf-quest-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  background: var(--mf-bg-2);
  border-radius: 11px;
}

.arcade-hub .mf-quest-me {
  background: rgba(247, 201, 72, 0.12);
}

.arcade-hub .mf-quest-rank {
  flex: 0 0 auto;
  min-width: 22px;
  font-size: 13px;
  font-weight: 800;
  color: var(--mf-ink-dim);
  text-align: right;
}

.arcade-hub .mf-quest-pct {
  flex: 0 0 auto;
  font-size: 14px;
  font-weight: 800;
  color: var(--mf-gold);
}

.arcade-hub .mf-quest-status:empty {
  display: none;
}

.arcade-hub .mf-quest-status {
  font-size: 13px;
  color: var(--mf-ink-dim);
  text-align: center;
}

/* ---- the one-line name ask -------------------------------------------------------
   A strip, NOT a modal, riding one step above the board overlay (z-index 45 over the
   board's 40). That layering is the whole point: it captions a board the player can still
   see and read behind it, rather than blacking the board out to demand a decision before
   the board can be read.

   It docks to the TOP, not the bottom, and that is a correctness choice rather than a
   taste one: the board card's Back button sits at its BOTTOM edge, so a bottom strip
   would cover the one control the player uses to leave. The card's own title area can
   afford to be overlapped for the few seconds this bar lives; the way out cannot. */
.arcade-hub .mf-nameask {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  z-index: 45;
  display: flex;
  align-items: center;
  gap: 10px;
  box-sizing: border-box;
  padding: max(12px, env(safe-area-inset-top)) 14px 12px;
  color: var(--mf-ink);
  background: var(--mf-surface);
  border-bottom: 1px solid var(--mf-card-border);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}

.arcade-hub .mf-nameask-line,
.arcade-hub .mf-nameask-form {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 14px;
}

/* The name is styled inside the sentence rather than after it, so the player reads WHO
   they are instead of a label followed by a value. */
.arcade-hub .mf-nameask-name {
  overflow: hidden;
  font-weight: 800;
  color: var(--mf-gold);
  white-space: nowrap;
  text-overflow: ellipsis;
}

.arcade-hub .mf-nameask-edit {
  flex: 0 0 auto;
  padding: 2px 6px;
  font-family: inherit;
  font-size: 15px;
  color: var(--mf-ink-dim);
  background: none;
  border: none;
  cursor: pointer;
}

.arcade-hub .mf-nameask-input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 38px;
  box-sizing: border-box;
  padding: 8px 10px;
  font-family: inherit;
  font-size: 15px;
  color: var(--mf-ink);
  background: var(--mf-bg-2);
  border: 1px solid var(--mf-card-border);
  border-radius: 10px;
  outline: none;
}

.arcade-hub .mf-nameask-input:focus {
  border-color: var(--mf-accent);
}

.arcade-hub .mf-nameask-save {
  flex: 0 0 auto;
  padding: 9px 14px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 800;
  color: var(--mf-bg);
  background: var(--mf-accent);
  border: none;
  border-radius: 10px;
  cursor: pointer;
}

/* The edit form wraps so the refusal line below can take a full row of its own. The name
   is now saved through the server, which can refuse it (moderation, shape, cooldown), and
   a refusal has to be readable next to the field it is about - the bar stays open until
   the player picks something the server accepts. */
.arcade-hub .mf-nameask-form {
  flex-wrap: wrap;
}

.arcade-hub .mf-nameask-status:empty {
  display: none;
}

.arcade-hub .mf-nameask-status {
  flex: 1 1 100%;
  font-size: 12px;
  color: var(--mf-ink-dim);
}

/* Skip is a REAL choice, not an escape hatch, because the name arrives pre-filled and
   correct - so it is a visible, comfortably tappable control rather than a faint link. */
.arcade-hub .mf-nameask-skip {
  flex: 0 0 auto;
  padding: 9px 14px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  color: var(--mf-ink-dim);
  background: transparent;
  border: 1px solid var(--mf-card-border);
  border-radius: 10px;
  cursor: pointer;
}

/* ---- four-tab leaderboard strip --------------------------------------------------
   These rules also live in kit/leaderboard-shared.css (the in-game board reads that
   sheet), restated here because the player app does NOT link it - the same reason the
   board title's trophy rules are restated above. Keep the two copies in step.

   Below 400px the four segments wrap into two rows of two rather than sharing one line:
   at the shared 14px tab size, "Tum zamanlar" and "Arkadaslar" beside two more tabs do
   not fit a 320px sheet, and a clipped tab label reads as a broken board. Above that the
   strip stays a single row, where all four fit comfortably. */
.arcade-hub .mf-toggle-group.mf-toggle-4 .mf-toggle {
  min-width: 0;
  padding-right: 2px;
  padding-left: 2px;
}

@media (max-width: 400px) {
  .arcade-hub .mf-toggle-group.mf-toggle-4 {
    flex-wrap: wrap;
  }

  .arcade-hub .mf-toggle-group.mf-toggle-4 .mf-toggle {
    flex: 1 1 calc(50% - 4px);
  }
}

/* The Friends tab's empty-state action, appended inside the board list. It is a kit
   `.mf-btn`, so it already carries the kit button look; this only stops it from
   stretching edge to edge inside the list column. */
.arcade-hub .mf-board-addfriends {
  align-self: center;
  margin-top: 10px;
  padding-right: 18px;
  padding-left: 18px;
}

/* ***************************************************************
 * The hub title line
 * *************************************************************** */

/* The title line: the invite tally pinned to the leading edge, the arcade heading
   centred beside it. A three-column grid rather than a flex row with auto margins,
   because the heading has to stay centred on the CARD - not centred in whatever space
   the flank leaves - and the empty third column is what buys that. */
/* Two equal flanks with the heading between them: the tally sits at the leading edge of
   the first flank and the second stays empty, so the heading lands on the bar's true
   centre whatever width it turns out to be in a given locale. A fixed spacer would have
   to be re-guessed every time the label changed. */
.arcade-hub .mf-hub-titlerow {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 8px;

  /* the subrow is a centred column, so a child sizes to its content and floats in the
     middle; the title line has to span the whole bar instead or the mark cannot reach
     the leading edge the search box below it starts from */
  width: 100%;
}

/* the leading flank's contents stay pinned to the leading edge rather than centring in
   it. Both selectors are kept: the stack is what ships, the bare pill is the shape this
   row had before the invite tally joined it. */
.arcade-hub .mf-hub-titlerow > .mf-hub-leftstack {
  justify-self: start;
}

/* The leading flank: the invite tally, which is the corner the owner asked for the
   invite count in.

   A COLUMN INSIDE ONE GRID CELL, not two children of the title grid. The row is
   `1fr auto 1fr` precisely so the heading lands on the bar's true centre; two loose
   children would have consumed the heading's own column and pushed it off centre. This
   way the grid still measures exactly three things and the heading centres against the
   whole stack.

   align-items: flex-start so the tally hangs on the leading edge of the flank instead
   of centring in it, which is what keeps it reading as a left-aligned column rather
   than a floating badge.

   NO min-width: 0 HERE, and that is load-bearing rather than an omission. It was tried,
   and it cost the flank its min-content floor in the grid: on a 360px phone the Russian
   label at four digits ("9999 приглашено", 138px) was handed a 124px track, and since
   the label is nowrap it simply drew straight out of its column and over the arcade
   heading. Without it the track keeps its floor, the heading is PUSHED instead of
   overlapped, and it still lands within a few pixels of centre at any real count -
   measured at 360px: a 12-invite Russian pill leaves the heading centred at 177px
   against a screen centre of 180px. The whole title line's minimum is ~210px against
   328px of content width even in the longest locale, so nothing can force a sideways
   scroll either. */
.arcade-hub .mf-hub-leftstack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 5px;
}

/* the empty trailing flank that balances the mark */
.arcade-hub .mf-hub-titlerow::after {
  content: '';
  display: block;
}


/* The update notice — the quiet one-line strip that appears at the very top of the scroll
   content, above the invite banner, ONLY in the store apps and ONLY when the installed
   build is behind the version the owner published. It is deliberately slimmer and cooler
   than the gold invite banner: it is information, not a call to spend or invite, so it
   uses the neutral card border rather than the gold reward border and sets its sentence at
   the subline size. Hidden by default (the kit `mf-hidden` class) and revealed only by the
   version check, so on Telegram / web it is never even built. */
.arcade-hub .mf-hub-update {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  box-sizing: border-box;
  margin: 0 0 10px;
  padding: 8px 10px;
  background: var(--mf-bg-2);
  border: 1px solid var(--mf-card-border);
  border-radius: 12px;
}

/* the tap target: the mark plus the sentence, filling the strip so the whole line reads
   as one thing to press. Transparent + borderless so the strip looks like one surface. */
.arcade-hub .mf-hub-update-main {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
  padding: 0;
  font-family: inherit;
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
}

.arcade-hub .mf-hub-update-icon {
  flex: 0 0 auto;
  font-size: 15px;
  line-height: 1;
}

/* one short sentence; it truncates rather than wrapping to a second line, because the
   whole point of this surface is that it stays a single quiet line */
.arcade-hub .mf-hub-update-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--mf-ink-dim);
}

/* the dismiss ×. A comfortable 32px tap target that still reads as small on the strip. */
.arcade-hub .mf-hub-update-close {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  padding: 0;
  font-family: inherit;
  font-size: 17px;
  line-height: 1;
  color: var(--mf-ink-dim);
  background: none;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
}
