<script lang="ts">
  import { onMount } from 'svelte';
  import Screen from '../components/Screen.svelte';
  import Modal from '../components/Modal.svelte';
  import GameLimitModal from '../components/GameLimitModal.svelte';
  import PinPad, { type PinResult } from '../components/PinPad.svelte';
  import { getLobby } from '../lib/lobbycache';
  import { isKindLocked } from '../lib/gamelimits';
  import { GameKind } from '../lib/model';
  import { gateway } from '../lib/gateway';
  import { app, handleError, showToast } from '../lib/app.svelte';
  import { connection } from '../lib/connection.svelte';
  import { offlineMode } from '../lib/offline.svelte';
  import { localSource, isLocalGameId } from '../lib/gamesource';
  import { newLocalGameId, randomSeed } from '../lib/localgame/id';
  import { localGuestId } from '../lib/localguest';
  import { navigate } from '../lib/router.svelte';
  import { t, type MessageKey } from '../lib/i18n/index.svelte';
  import { validDisplayName } from '../lib/profileValidation';
  import { verifyPin, type PinLock } from '../lib/pin';
  import { commitName, rosterReady, buildSeats, shuffleSeeded, type RosterRow } from '../lib/roster';
  import type { AccountRef, GameView, Variant } from '../lib/model';
  import {
    availableVariants,
    VARIANT_FLAG,
    VARIANT_RULES,
    supportsMultipleWordsToggle,
    multipleWordsForRequest,
  } from '../lib/variants';

  // The auto-match move clock (mirrors backend game.DefaultTurnTimeout = 24h).
  const AUTO_MATCH_HOURS = 24;

  // The offered variants are gated by the player's profile preferences (the variants
  // they enabled in Settings); the auto-match list and the friend-invite picker both use this.
  const variants = $derived(availableVariants(app.profile?.variantPreferences));
  // "Multiple words per turn" off is the single-word rule; it is offered for Russian games
  // only (English is always standard and shows no toggle). Shared by both flows.
  let multipleWords = $state(false);
  // Auto-match: the variant is a select (highlight, no immediate enqueue) confirmed by the
  // Start button. A lone offered variant is pre-selected; with several the player must pick.
  let selectedAuto = $state<Variant | ''>('');
  $effect(() => {
    if (variants.length === 1 && !selectedAuto) selectedAuto = variants[0].id;
  });
  const timeouts = [
    { secs: 300, key: 'time.minutes' as MessageKey, n: 5 },
    { secs: 1800, key: 'time.minutes' as MessageKey, n: 30 },
    { secs: 3600, key: 'time.hours' as MessageKey, n: 1 },
    { secs: 86400, key: 'time.hours' as MessageKey, n: 24 },
  ];

  const guest = $derived(app.profile?.isGuest ?? true);
  let mode = $state<'auto' | 'friends'>('auto');
  // Within "with friends": online = a remote invite, offline = a pass-and-play (hotseat) game on this
  // device. Both exist regardless of connectivity, but with no network the online segment is disabled
  // and offline is forced — so the create flow always works.
  let friendsMode = $state<'online' | 'offline'>('online');
  $effect(() => {
    if (offlineMode.active) friendsMode = 'offline';
  });
  // The quick-game opponent: an honest AI (a robot that joins and moves at once, shown as 🤖)
  // or a random human via auto-match. AI is the default.
  let opponent = $state<'ai' | 'random'>('ai');

  // Active-game limit lock: the auto-start kind (vs_ai or random by the opponent choice) tested
  // against the caller's per-kind cap over the online lobby's active games. A capped start opens the
  // funnel modal instead of enqueuing (the server also refuses it); offline never locks (local
  // games are uncapped). The lock lifts when the profile is re-fetched after a guest→durable upgrade.
  const autoKind = $derived(opponent === 'ai' ? GameKind.vsAi : GameKind.random);
  // The player's current games for the per-kind lock: seeded from the lobby snapshot for an instant
  // render, then refreshed on mount (below) so a game created since the last lobby visit is counted.
  // Only server games count toward the per-kind server limit (device-local games are unlimited), so
  // the merged snapshot is filtered to the server ones here (the on-mount refresh below already reads
  // the server list directly).
  let lobbyGames = $state<GameView[]>(getLobby()?.games.filter((g) => !isLocalGameId(g.id)) ?? []);
  const autoLocked = $derived(!offlineMode.active && isKindLocked(lobbyGames, app.profile?.gameLimits, autoKind));
  let limitOpen = $state(false);

  // --- auto-match ---
  // Enqueue drops the player straight into a real game — a freshly opened one awaiting an
  // opponent, or another player's open game they just joined — so we navigate into it at once
  // and the player waits inside. The opponent (a human or, after the wait, a robot) takes the
  // empty seat later via the opponent_joined push; there is no separate "searching" screen.
  let starting = $state(false);

  async function find(v: Variant) {
    if (starting) return;
    starting = true;
    try {
      // Offline mode: create a device-local vs_ai game instead of enqueuing on the backend. The
      // dictionary version comes from the profile (advertised for exactly this) or, for a device-local
      // guest with no profile yet, the bundled __DICT_VERSION__; the bag is seeded locally and the same
      // game screen then drives it through the local source.
      if (offlineMode.active) {
        const version = app.profile?.dictVersions?.[v] ?? __DICT_VERSION__;
        if (!version) {
          handleError(new Error('dict_unavailable'));
          return;
        }
        const id = newLocalGameId();
        await localSource.create({
          id,
          variant: v,
          dictVersion: version,
          seed: randomSeed(),
          multipleWords: multipleWordsForRequest(v, multipleWords),
          seats: [
            // The human seat carries the account id — the server session's, or a device-local guest id
            // when there is no session yet — so the game screen and the lobby identify "you" and the
            // correct turn exactly as for an online game (the robot uses a synthetic id).
            { kind: 'human', name: app.profile?.displayName ?? t('common.guest'), accountId: app.session?.userId ?? localGuestId() },
            { kind: 'robot', name: 'Robot' },
          ],
        });
        navigate(`/game/${id}`);
        return;
      }
      const r = await gateway.lobbyEnqueue(v, multipleWordsForRequest(v, multipleWords), opponent === 'ai');
      if (r.game) navigate(`/game/${r.game.id}`);
    } catch (e) {
      handleError(e);
    } finally {
      starting = false;
    }
  }

  // --- friend game ---
  let friends = $state<AccountRef[]>([]);
  let selected = $state<string[]>([]);
  let friendFilter = $state('');
  // A lone offered variant is pre-selected and its picker disabled (nothing else to choose);
  // with several, the player picks. '' renders the disabled placeholder option.
  let inviteVariant = $state<Variant | ''>('');
  $effect(() => {
    if (variants.length === 1 && !inviteVariant) inviteVariant = variants[0].id;
  });

  // The offline dictionary guard: creating a device-local game (vs_ai or hotseat) needs the variant's
  // dawg to be loadable without the network — bundled (native, always) or IndexedDB-cached (web). A
  // native build bundles every variant, so this only ever bites a web install whose dawg was not
  // cached. dawgReady is null while the async check runs, true/false once resolved; online it stays
  // true (the guard is inert).
  let dawgReady = $state<boolean | null>(true);
  const guardVariant = $derived(mode === 'auto' ? selectedAuto : friendsMode === 'offline' ? inviteVariant : '');
  $effect(() => {
    const v = guardVariant;
    if (!offlineMode.active || !v) {
      dawgReady = true;
      return;
    }
    dawgReady = null; // checking
    const version = app.profile?.dictVersions?.[v] ?? __DICT_VERSION__;
    let cancelled = false;
    // getDawg resolves from IndexedDB / the native bundle offline (the network tier is refused by the
    // kill switch), so a null result means the dictionary is genuinely unavailable on this device.
    void import('../lib/dict')
      .then((m) => m.getDawg(v, version))
      .then((d) => {
        if (!cancelled) dawgReady = !!d;
      });
    return () => {
      cancelled = true;
    };
  });
  // dictBlocked gates the offline create on a genuinely-missing dictionary (never while still checking).
  const dictBlocked = $derived(offlineMode.active && dawgReady === false);

  let timeoutSecs = $state(86400);
  let hints = $state(1);

  const filteredFriends = $derived(
    friendFilter.trim()
      ? friends.filter((f) => f.displayName.toLowerCase().includes(friendFilter.trim().toLowerCase()))
      : friends,
  );

  onMount(async () => {
    // Refresh the lobby games so the per-kind lock counts the current set — the cached snapshot can
    // lag a game created since the last lobby visit. Online only; the lock never applies offline, and
    // the server enforces every cap regardless.
    if (!offlineMode.active && connection.online) {
      try {
        lobbyGames = (await gateway.gamesList()).games;
      } catch {
        // Keep the cached seed on a transient failure.
      }
    }
    // Offline mode offers only the local vs_ai flow (the friends section is hidden), and the network
    // is off, so skip the friend fetch — it would be refused by the transport and toast an error.
    if (guest || offlineMode.active) return;
    try {
      friends = await gateway.friendsList();
    } catch (e) {
      handleError(e);
    }
  });

  function toggle(id: string) {
    selected = selected.includes(id) ? selected.filter((x) => x !== id) : [...selected, id];
  }

  async function sendInvite() {
    if (selected.length === 0 || selected.length > 3 || !inviteVariant) return;
    try {
      await gateway.invitationCreate(selected, {
        variant: inviteVariant,
        turnTimeoutSecs: timeoutSecs,
        hintsAllowed: hints > 0,
        hintsPerPlayer: hints,
        dropoutTiles: 'remove',
        multipleWordsPerTurn: multipleWordsForRequest(inviteVariant, multipleWords),
      });
      showToast(t('new.invited'));
      navigate('/');
    } catch (e) {
      handleError(e);
    }
  }

  // --- offline hotseat (pass-and-play) ---
  // A device-local 2-4 human game. The host (referee) sets a mandatory master PIN first — it gates
  // the roster and, in-game, the host overrides; each player row optionally locks its seat with a PIN.
  let hostPin = $state<PinLock | undefined>(undefined);
  let rows = $state<RosterRow[]>([
    { name: '', committed: '' },
    { name: '', committed: '' },
  ]);
  let askHostPlays = $state(false); // the "do you take a seat?" prompt shown once the host PIN is set

  // The PIN-pad overlay target: what a completed PIN applies to.
  type PadTarget =
    | { kind: 'host-set' }
    | { kind: 'host-change' }
    | { kind: 'seat-set'; index: number }
    | { kind: 'seat-change'; index: number }
    | { kind: 'row-delete'; index: number };
  let pad = $state<PadTarget | null>(null);
  // The row whose delete cross is armed: tapping a row's kebab asks the host PIN, and a correct PIN
  // arms its ❌ (mirroring the lobby delete) rather than removing it silently. Tapping the kebab again
  // (or arming another row) clears the authorisation.
  let deleteRow = $state<number | null>(null);

  const padMode = (p: PadTarget): 'set' | 'verify' | 'change' =>
    p.kind === 'host-set' || p.kind === 'seat-set' ? 'set' : p.kind === 'row-delete' ? 'verify' : 'change';
  function padVerify(p: PadTarget): ((pin: string) => Promise<boolean>) | undefined {
    if (p.kind === 'host-change' || p.kind === 'row-delete') return (pin) => verifyPin(pin, hostPin!);
    if (p.kind === 'seat-change') return (pin) => verifyPin(pin, rows[p.index].pin!);
    return undefined; // set flows need no verifier
  }
  const padTitle = (p: PadTarget): string =>
    p.kind === 'host-set' || p.kind === 'host-change' || p.kind === 'row-delete'
      ? t('hotseat.hostPin')
      : t('hotseat.setPin');

  function onPad(r: PinResult): void {
    const target = pad;
    pad = null;
    if (!target) return;
    switch (target.kind) {
      case 'host-set':
        if (r.kind === 'set') {
          hostPin = r.lock;
          askHostPlays = true;
        }
        break;
      case 'host-change':
        if (r.kind === 'set') hostPin = r.lock; // the host PIN is mandatory — no remove option
        break;
      case 'seat-set':
        if (r.kind === 'set') rows[target.index].pin = r.lock;
        break;
      case 'seat-change':
        if (r.kind === 'set') rows[target.index].pin = r.lock;
        else if (r.kind === 'removed') rows[target.index].pin = undefined;
        break;
      case 'row-delete':
        if (r.kind === 'verified') deleteRow = target.index; // arm the ❌; the actual delete is a tap on it
        break;
    }
  }

  // onKebab toggles a row's delete authorisation: a second tap (already armed) closes it and resets
  // the host authorisation; otherwise it asks the host PIN (which, on success, arms the ❌).
  function onKebab(i: number): void {
    if (deleteRow === i) {
      deleteRow = null;
      return;
    }
    deleteRow = null; // only one row armed at a time
    pad = { kind: 'row-delete', index: i };
  }

  function removeRow(i: number): void {
    rows = rows.filter((_, j) => j !== i);
    deleteRow = null;
  }

  function addRow(): void {
    rows = [...rows, { name: '', committed: '' }];
    deleteRow = null;
  }

  function setHostPlays(yes: boolean): void {
    askHostPlays = false;
    if (!yes) return;
    // Seat the host at row 0, its name reused from the profile (a normal seat — its optional seat PIN
    // is separate from the master PIN).
    const committed = commitName(app.profile?.displayName ?? '', '');
    rows[0] = { name: committed, committed };
  }

  function openSeatPin(i: number): void {
    pad = rows[i].pin ? { kind: 'seat-change', index: i } : { kind: 'seat-set', index: i };
  }

  async function startHotseat(): Promise<void> {
    if (starting || !hostPin || !inviteVariant || !rosterReady(rows)) return;
    starting = true;
    try {
      const version = app.profile?.dictVersions?.[inviteVariant] ?? __DICT_VERSION__;
      if (!version) {
        handleError(new Error('dict_unavailable'));
        return;
      }
      const id = newLocalGameId();
      const seed = randomSeed();
      // Randomise the seating so the FIRST mover is random (the engine starts at seat 0, so shuffling
      // the seats picks a random starter and turn order). Seed-driven, so replay is consistent.
      // Snapshot the roster + PINs to plain objects: the rows/pins are $state proxies, and a proxy
      // fails structured-clone when the record is persisted to IndexedDB (silently, best-effort store)
      // — so the game would vanish on reload. See lib/localgame/store.
      await localSource.create({
        id,
        variant: inviteVariant,
        dictVersion: version,
        seed,
        multipleWords: multipleWordsForRequest(inviteVariant, multipleWords),
        seats: $state.snapshot(shuffleSeeded(buildSeats(rows), seed)),
        hotseat: true,
        hostPin: hostPin ? $state.snapshot(hostPin) : undefined,
      });
      navigate(`/game/${id}`);
    } catch (e) {
      handleError(e);
    } finally {
      starting = false;
    }
  }
</script>

<Screen title={t('new.title')} back="/">
  <div class="page">
      <!-- The auto/friends selector. Online it is hidden for guests (no friends to invite). Offline
           it is always shown: "quick" is the local vs_ai flow, "with friends" is the local
           pass-and-play (hotseat) flow. -->
      {#if !guest || offlineMode.active}
        <div class="seg modes">
          <button class="opt" class:active={mode === 'auto'} onclick={() => (mode = 'auto')}>{t('new.auto')}</button>
          <button class="opt" class:active={mode === 'friends'} onclick={() => (mode = 'friends')}>{t('new.withFriends')}</button>
        </div>
      {/if}

      {#if mode === 'auto'}
        {#if !offlineMode.active}
          <div class="seg modes">
            <button class="opt" class:active={opponent === 'ai'} onclick={() => (opponent = 'ai')}>🤖 {t('new.opponentAI')}</button>
            <button class="opt" class:active={opponent === 'random'} onclick={() => (opponent = 'random')}>👤 {t('new.opponentRandom')}</button>
          </div>
        {/if}
        <div class="variants">
          {#each variants as v (v.id)}
            <button
              class="variant"
              class:selected={selectedAuto === v.id}
              onclick={() => (selectedAuto = v.id)}
              disabled={!connection.online && !offlineMode.active}
            >
              <span class="vmain">
                <span class="vname">{t(v.label)}</span>
                {#if VARIANT_FLAG[v.id]}
                  <span class="vflag">{VARIANT_FLAG[v.id]}</span>
                {:else}
                  <img class="vflag-img" src="flag-ussr.svg" alt="" />
                {/if}
              </span>
              <span class="vrules">{t(VARIANT_RULES[v.id])}</span>
            </button>
          {/each}
        </div>
        {#if variants.some((v) => supportsMultipleWordsToggle(v.id))}
          <label class="toggle">
            <span>{t('new.multipleWordsPerTurn')}</span>
            <input type="checkbox" bind:checked={multipleWords} />
          </label>
        {/if}
        <p class="movelimit">{opponent === 'ai' ? t('new.aiInactiveLimit') : t('new.moveLimit', { n: AUTO_MATCH_HOURS })}</p>
        {#if opponent === 'random'}<p class="searchhint">{t('new.searchHint')}</p>{/if}
        {#if dictBlocked}<p class="dictnote" role="alert">{t('new.dictUnavailable')}</p>{/if}
        <button
          class="invite"
          class:locked={autoLocked}
          disabled={(autoLocked ? false : !selectedAuto) || (!connection.online && !offlineMode.active) || dictBlocked || starting}
          onclick={() => (autoLocked ? (limitOpen = true) : selectedAuto && find(selectedAuto))}
        >{#if autoLocked}🔒 {/if}{t('new.start')}</button>
        <GameLimitModal
          open={limitOpen}
          {guest}
          onclose={() => (limitOpen = false)}
          onlogin={() => { limitOpen = false; navigate('/profile'); }}
        />
      {:else}
        <!-- With friends: an online remote invite, or an offline pass-and-play (hotseat) on this
             device. Both exist regardless of connectivity, but with no network the online segment is
             disabled and offline is forced (friendsMode). -->
        <div class="seg modes">
          <button class="opt" class:active={friendsMode === 'online'} disabled={offlineMode.active} onclick={() => (friendsMode = 'online')}>{t('new.playRemote')}</button>
          <button class="opt" class:active={friendsMode === 'offline'} onclick={() => (friendsMode = 'offline')}>{t('new.playLocal')}</button>
        </div>
        {#if offlineMode.active}<p class="dictnote">{t('new.needsNetwork')}</p>{/if}
        {#if friendsMode === 'offline'}
        <!-- Offline pass-and-play (hotseat): a device-local 2-4 human game. The host sets a master
             PIN first (it gates the roster); each seat may add its own PIN. -->
        <div class="hostpin">
          <span class="ftitle">{t('hotseat.hostPin')}</span>
          <button class="plink" onclick={() => (pad = hostPin ? { kind: 'host-change' } : { kind: 'host-set' })}>
            {hostPin ? t('hotseat.changePin') : t('hotseat.setPin')}
          </button>
        </div>
        <!-- Variant plaques + the multiple-words toggle, identical to Quick Match. -->
        <div class="variants">
          {#each variants as v (v.id)}
            <button class="variant" class:selected={inviteVariant === v.id} onclick={() => (inviteVariant = v.id)}>
              <span class="vmain">
                <span class="vname">{t(v.label)}</span>
                {#if VARIANT_FLAG[v.id]}
                  <span class="vflag">{VARIANT_FLAG[v.id]}</span>
                {:else}
                  <img class="vflag-img" src="flag-ussr.svg" alt="" />
                {/if}
              </span>
              <span class="vrules">{t(VARIANT_RULES[v.id])}</span>
            </button>
          {/each}
        </div>
        {#if variants.some((v) => supportsMultipleWordsToggle(v.id))}
          <label class="toggle">
            <span>{t('new.multipleWordsPerTurn')}</span>
            <input type="checkbox" bind:checked={multipleWords} />
          </label>
        {/if}
        <div class="roster">
          {#each rows as row, i (i)}
            <div class="prow">
              <input
                class="pname"
                class:invalid={row.name.trim() !== '' && !validDisplayName(row.name)}
                bind:value={row.name}
                disabled={!hostPin}
                placeholder={t('hotseat.playerName')}
                maxlength="40"
                oninput={() => (row.committed = commitName(row.name, row.committed))}
                onblur={() => (row.name = row.committed)}
              />
              <button class="plink" disabled={!hostPin || row.committed === ''} onclick={() => openSeatPin(i)}>
                {row.pin ? t('hotseat.changePin') : t('hotseat.setPin')}
              </button>
              {#if rows.length > 2}
                {#if deleteRow === i}
                  <button class="prow-del" aria-label={t('hotseat.removePlayer')} onclick={() => removeRow(i)}>❌</button>
                {/if}
                <button
                  class="pkebab"
                  class:armed={deleteRow === i}
                  disabled={!hostPin}
                  aria-label={t('hotseat.removePlayer')}
                  onclick={() => onKebab(i)}
                >⋮</button>
              {/if}
            </div>
          {/each}
        </div>
        {#if rows.length < 4}
          <button class="addrow" disabled={!hostPin} onclick={addRow}>
            + {t('hotseat.addPlayer')}
          </button>
        {/if}
        {#if dictBlocked}<p class="dictnote" role="alert">{t('new.dictUnavailable')}</p>{/if}
        <button
          class="invite"
          disabled={!hostPin || !inviteVariant || !rosterReady(rows) || dictBlocked || starting}
          onclick={startHotseat}
        >{t('new.start')}</button>
      {:else if friends.length === 0}
        <p class="subtitle">{t('new.noFriends')}</p>
      {:else}
        <div class="fg">
          <div class="picked">
            <span class="ftitle">{t('new.pickFriends')} ({selected.length})</span>
            <input class="search" bind:value={friendFilter} placeholder={t('new.searchFriends')} />
          </div>
          <div class="friends-scroll">
            {#each filteredFriends as f (f.accountId)}
              <label class="friend">
                <input type="checkbox" checked={selected.includes(f.accountId)} onchange={() => toggle(f.accountId)} />
                <span>{f.displayName}</span>
              </label>
            {/each}
            {#if filteredFriends.length === 0}<p class="muted">—</p>{/if}
          </div>
          <div class="settings-row">
            <label class="field">
              <span>{t('new.gameType')}</span>
              <select bind:value={inviteVariant} class:placeholder={!inviteVariant} disabled={variants.length === 1}>
                <option value="" disabled>—</option>
                {#each variants as v (v.id)}<option value={v.id}>{t(v.label)}</option>{/each}
              </select>
            </label>
            <label class="field">
              <span>{t('new.moveTime')}</span>
              <select bind:value={timeoutSecs}>
                {#each timeouts as to (to.secs)}<option value={to.secs}>{t(to.key, { n: to.n })}</option>{/each}
              </select>
            </label>
            <label class="field">
              <span>{t('new.hintsPerPlayer')}</span>
              <select bind:value={hints}>
                {#each [0, 1, 2] as h (h)}<option value={h}>{h}</option>{/each}
              </select>
            </label>
          </div>
          {#if inviteVariant && supportsMultipleWordsToggle(inviteVariant)}
            <label class="toggle">
              <span>{t('new.multipleWordsPerTurn')}</span>
              <input type="checkbox" bind:checked={multipleWords} />
            </label>
          {/if}
          <button class="invite" disabled={selected.length === 0 || !inviteVariant || !connection.online} onclick={sendInvite}>{t('new.invite')}</button>
        </div>
        {/if}
      {/if}

      {#if pad}
        <PinPad
          mode={padMode(pad)}
          title={padTitle(pad)}
          verify={padVerify(pad)}
          allowRemove={pad.kind === 'seat-change'}
          onclose={() => (pad = null)}
          onresult={onPad}
        />
      {/if}
      {#if askHostPlays}
        <Modal title={t('hotseat.hostPlaysTitle')} onclose={() => (askHostPlays = false)}>
          <div class="hostplays">
            <button class="ghost" onclick={() => setHostPlays(false)}>{t('hotseat.hostPlaysNo')}</button>
            <button class="primary" onclick={() => setHostPlays(true)}>{t('hotseat.hostPlaysYes')}</button>
          </div>
        </Modal>
      {/if}
  </div>
</Screen>

<style>
  /* Natural-flow content (matching the Profile sub-view): no forced height and no pinned CTA, so the
     screen's own scroll (Screen .content + --vvh) handles overflow and a focused input scrolls into
     view above the soft keyboard with a single, clean scroll container. */
  .page {
    padding: var(--pad);
    display: flex;
    flex-direction: column;
    gap: 14px;
    box-sizing: border-box;
  }
  .subtitle {
    color: var(--text-muted);
    margin: 0;
  }
  .variants {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  /* A plaque per variant (like the lobby game cards): the name with its flag on the right,
     and a one-line rules summary below. */
  .variant {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 12px 14px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: var(--radius);
    text-align: left;
    user-select: none;
  }
  .vmain {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
  }
  .vname {
    font-size: 1.05rem;
    font-weight: 600;
  }
  .vflag {
    font-size: 1.3rem;
    line-height: 1;
  }
  .vflag-img {
    width: 1.6rem;
    height: auto;
    border-radius: 2px;
  }
  .vrules {
    font-size: 0.8rem;
    color: var(--text-muted);
  }
  /* Selected auto-match variant: an accent inset border (the button no longer enqueues on
     tap; the Start button confirms the choice). */
  .variant.selected {
    border-color: var(--accent);
    box-shadow: inset 0 0 0 2px var(--accent);
  }
  .movelimit {
    margin: 0;
    text-align: center;
    color: var(--text-muted);
    font-size: 0.85rem;
  }
  .seg {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
  }
  .modes {
    margin-bottom: 4px;
  }
  .opt {
    flex: 1;
    min-width: 64px;
    padding: 10px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: var(--radius-sm);
    user-select: none;
  }
  .opt.active {
    background: var(--accent);
    color: var(--accent-text);
    border-color: var(--accent);
  }
  .fg {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  .picked {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .ftitle {
    font-size: 0.9rem;
    color: var(--text-muted);
  }
  .search {
    min-width: 0;
    padding: 9px 11px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: var(--radius-sm);
  }
  .friends-scroll {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }
  .friend {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    background: var(--surface);
    border-radius: var(--radius-sm);
  }
  .settings-row {
    display: flex;
    gap: 8px;
  }
  .field {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .field span {
    font-size: 0.78rem;
    color: var(--text-muted);
  }
  .field select {
    width: 100%;
    box-sizing: border-box;
    min-width: 0;
    padding: 9px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: var(--radius-sm);
  }
  .field select.placeholder {
    color: var(--text-muted);
  }
  .toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 11px;
    border: 1px solid var(--border);
    background: var(--surface);
    border-radius: var(--radius-sm);
    user-select: none;
  }
  .toggle span {
    font-size: 0.85rem;
    color: var(--text);
  }
  .muted {
    color: var(--text-muted);
    margin: 0;
  }
  .invite {
    flex: 0 0 auto;
    padding: 14px;
    border: 1px solid var(--accent);
    background: var(--accent);
    color: var(--accent-text);
    border-radius: var(--radius);
    font-weight: 600;
  }
  .invite:disabled {
    opacity: 0.5;
  }
  /* A capped start (the active-game limit): an outline instead of the filled CTA, with a 🔒, so it
     reads as gated rather than ready. Tapping it opens the funnel modal, never a game. */
  .invite.locked {
    background: transparent;
    color: var(--accent);
  }
  .searchhint {
    margin: 0;
    text-align: center;
    color: var(--text-muted);
    font-size: 0.85rem;
    line-height: 1.4;
  }
  /* The offline-blocker reason (a missing dictionary, or the online segment needing a connection). */
  .dictnote {
    margin: 0;
    text-align: center;
    color: var(--warn);
    font-size: 0.85rem;
    line-height: 1.4;
  }
  /* --- offline hotseat roster --- */
  .hostpin {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 11px;
    border: 1px solid var(--border);
    background: var(--surface);
    border-radius: var(--radius-sm);
  }
  .roster {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .prow {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .pname {
    flex: 1;
    min-width: 0;
    padding: 10px 11px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: var(--radius-sm);
  }
  .pname:disabled {
    opacity: 0.5;
  }
  .pname.invalid {
    border-color: var(--danger);
  }
  .plink {
    flex: 0 0 auto;
    padding: 8px 10px;
    border: none;
    background: none;
    color: var(--accent);
    font-weight: 600;
    white-space: nowrap;
  }
  .plink:disabled {
    color: var(--text-muted);
    opacity: 0.6;
  }
  .pkebab {
    flex: 0 0 auto;
    padding: 6px 8px;
    border: none;
    background: none;
    color: var(--text-muted);
    font-size: 1.1rem;
    line-height: 1;
  }
  .pkebab.armed {
    color: var(--accent);
  }
  /* The delete cross a correct host PIN arms next to a player row (mirrors the lobby game delete). */
  .prow-del {
    flex: 0 0 auto;
    padding: 6px 8px;
    border: none;
    background: none;
    font-size: 1rem;
    line-height: 1;
  }
  .addrow {
    align-self: flex-start;
    padding: 8px 12px;
    border: 1px dashed var(--border);
    background: none;
    color: var(--accent);
    border-radius: var(--radius-sm);
    font-weight: 600;
  }
  .addrow:disabled {
    opacity: 0.5;
    color: var(--text-muted);
  }
  .hostplays {
    display: flex;
    gap: 10px;
    justify-content: flex-end;
  }
  .hostplays button {
    padding: 10px 16px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    font-weight: 600;
  }
  .hostplays .primary {
    background: var(--accent);
    color: var(--accent-text);
    border-color: var(--accent);
  }
</style>
