:root {
  --hjerte: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12 21s-8-5.1-8-10.4A4.6 4.6 0 0 1 12 7a4.6 4.6 0 0 1 8 3.6C20 15.9 12 21 12 21z'/%3E%3C/svg%3E");
  --bg: #171b20;
  --surface: #242c37;
  --surface-2: #2f3a47;
  --line: rgba(238, 247, 255, .13);
  --p1: #24dceb;
  --p1-deep: #087f9f;
  --p2: #ff5b82;
  --p2-deep: #b52e56;
  --violet: #a68cff;
  --gold: #ffd052;
  --good: #72e58d;
  --bad: #ff626c;
  --text: #f6f9fc;
  --muted: #929eab;
  /* The top edge of everything pinned to the top of the front page: the winning
     run on the left, the language switch on the right. One value, because two
     copies of it drifted three pixels apart the first time.

     The env() term is what keeps both clear of a notch in the installed app,
     where the overlay reaches the very top of the screen. The 3px is what a
     plain browser tab gets, since there the browser's own bar is above us and
     the inset is zero - which is why raising this number moved nothing on a
     phone browser and everything in the app. As high as it goes. */
  --top-row: max(3px, env(safe-area-inset-top));
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font-family: "Segoe UI", Roboto, system-ui, sans-serif;
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
}
body {
  background:
    linear-gradient(120deg, rgba(36, 220, 235, .055), transparent 36%),
    linear-gradient(240deg, rgba(255, 91, 130, .045), transparent 34%),
    linear-gradient(180deg, #28323d 0, #1b222a 48%, #151a1f 100%);
}
button, input { font: inherit; }
button { border: 0; color: inherit; background: none; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.hidden { display: none !important; }

#app {
  width: min(100%, 720px);
  height: 100%;
  height: 100dvh;
  margin: auto;
  position: relative;
  overflow: hidden;
  padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom);
  background: linear-gradient(180deg, rgba(36, 44, 54, .98), rgba(21, 26, 31, .99));
  border-inline: 1px solid rgba(255,255,255,.04);
}
#fxLayer { position: fixed; inset: 0; z-index: 300; pointer-events: none; overflow: hidden; }
.game-screen { height: 100%; display: flex; flex-direction: column; position: relative; overflow: hidden; }

/*
 * Everything around the board shares out the height the board did not need.
 *
 * The cell size comes from the width, so the board is as wide as the phone and
 * exactly as tall as it is wide. A tall phone therefore has height left over
 * that the board cannot use, and it has to go somewhere. It used to go into the
 * frame, which stretched the whole column and left the board sitting in the
 * middle of a much taller panel; trimming the frame alone only turned that into
 * two black voids instead. Neither is a screen that is filled.
 *
 * So the leftover is shared, and the flex-grow numbers below are the whole
 * decision, in one place, in proportion:
 *
 *   scorebar 1.6   the cards carry the most content, so they take the most
 *   bottom-bar 1.2 bigger buttons on a bigger phone are worth having
 *   header 0.5     a title strip that grows reads as wasted space
 *   resourcebar    0.25, because a row of pulses is a line, not a panel
 *   arena 1.6      the rest, which shows as arena around the board
 *
 * Each block keeps its old height as min-height, so a short phone loses nothing
 * and the growth only ever happens where there is genuinely room.
 */
.header {
  min-height: 35px;
  flex: 0.5 0 35px;
  padding: 8px 11px 4px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 7px;
  border-bottom: 1px solid rgba(238, 247, 255, .075);
  background: rgba(27, 32, 39, .72);
}
.logo {
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 1px;
  background: linear-gradient(105deg, #76f4fb, #bcaaff 44%, #ff89a5 78%, #ffe180);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  white-space: nowrap;
}
.room {
  font-size: 9px;
  font-weight: 900;
  letter-spacing: .7px;
  padding: 5px 9px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: linear-gradient(180deg, #303d4a, #222a33);
  white-space: nowrap;
  box-shadow: inset 0 1px rgba(255,255,255,.055);
}
.live { display: flex; align-items: center; gap: 5px; font-size: 9px; font-weight: 900; color: var(--good); white-space: nowrap; }
.live.off { color: var(--bad); }
.dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; box-shadow: 0 0 9px currentColor; animation: softPulse 1.5s ease-in-out infinite; }
@keyframes softPulse { 50% { opacity: .53; transform: scale(.9); } }

.scorebar {
  min-height: 72px;
  flex: 1.6 0 72px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 58px minmax(0, 1fr);
  gap: 7px;
  align-items: stretch;
  padding: 6px 10px 5px;
  position: relative;
  z-index: 3;
}
/*
 * ANSIKTET I POENGKORTET.
 *
 * Kortet var en kolonne: navn, liv, poeng. Ordet "LIVES" tok bredden til et
 * stort tall uten aa si noe tallet ikke sier - naa staar tallet alene med et
 * hjerte, og den frigjorte bredden gaar til spillerens eget ansikt.
 *
 * Ansiktet henger i kanten det hoerer til: venstre for spiller 1, hoyre for
 * spiller 2, slik resten av kortet allerede er speilet. Teksten beholder hele
 * hoyden si - ingenting av den er gjort mindre.
 */
.player-card {
  min-width: 0;
  min-height: 74px;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  padding: 8px 10px 6px;
  border-radius: 8px;
  border: 1px solid var(--line);
  box-shadow: inset 0 1px rgba(255,255,255,.09), 0 5px 15px rgba(0,0,0,.22);
  overflow: hidden;
  position: relative;
}
.player-card.p1 { color: var(--p1); background: linear-gradient(150deg, rgba(29, 166, 184, .29), rgba(31, 102, 123, .28) 48%, rgba(32, 39, 48, .97)); }
.player-card.p2 { color: var(--p2); background: linear-gradient(150deg, rgba(205, 56, 94, .30), rgba(138, 50, 81, .30) 48%, rgba(32, 39, 48, .97)); text-align: right; flex-direction: row-reverse; }
/*
 * ANSIKTET LIGGER I RAMMEN, IKKE VED SIDEN AV TEKSTEN.
 *
 * Forste forsok satte det i raden som et eget element. MAALT: navnet trengte
 * 126 px og hadde noyaktig 126 - et ansikt paa 46 px tok 54 av dem med
 * mellomrommet, og "Langtnavn · YOU" ble klippet til under det halve. Navnet,
 * livene og poengene skal kunne leses; det var betingelsen.
 *
 * Naa ligger ansiktet UTENFOR flyten, forankret i kortets indre kant og like
 * hoyt som kortet selv. Teksten beholder hele bredden sin og ligger oppaa.
 * Masken toner bildet ut mot teksten, saa den siden forblir moerk og lesbar -
 * bildet blir en del av kortets bakgrunn i stedet for en nabo som dytter.
 */
.card-text { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; position: relative; z-index: 1; }
.card-face { position: absolute; top: 0; bottom: 0; height: 100%; width: 62%; object-fit: cover;
  object-position: center 22%; z-index: 0; pointer-events: none; opacity: .92; }
.player-card.p1 .card-face { right: 0;
  -webkit-mask: linear-gradient(90deg, transparent 0, rgba(0,0,0,.55) 42%, #000 82%);
  mask: linear-gradient(90deg, transparent 0, rgba(0,0,0,.55) 42%, #000 82%); }
.player-card.p2 .card-face { left: 0; transform: scaleX(-1);
  -webkit-mask: linear-gradient(90deg, transparent 0, rgba(0,0,0,.55) 42%, #000 82%);
  mask: linear-gradient(90deg, transparent 0, rgba(0,0,0,.55) 42%, #000 82%); }
.pname { font-size: 10px; font-weight: 900; letter-spacing: .6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.life { font-size: 27px; font-weight: 900; line-height: 1; margin-top: 4px; letter-spacing: 0;
  display: flex; align-items: center; gap: 5px; }
.player-card.p2 .life { justify-content: flex-end; }
/* Maske og ikke skrifttegn: et hjerteglyf ser forskjellig ut paa hver telefon,
   og paa noen mangler det helt. currentColor gir det spillerens egen farge. */
.life::before { content: ''; flex: 0 0 auto; width: .62em; height: .62em; background: currentColor;
  -webkit-mask: var(--hjerte) center / contain no-repeat; mask: var(--hjerte) center / contain no-repeat; }
.life.p1 { color: var(--p1); text-shadow: 0 2px 8px rgba(36, 220, 235, .23); }
.life.p2 { color: var(--p2); text-shadow: 0 2px 8px rgba(255, 91, 130, .23); }
.player-score { font-size: 10px; font-weight: 900; letter-spacing: .4px; margin-top: 4px; color: var(--gold); }
.vs { font-size: 13px; font-weight: 900; color: var(--gold); text-align: center; line-height: 1.2; min-width: 54px; align-self: center; }
.ping { display: block; font-size: 8px; color: var(--muted); font-weight: 800; margin-top: 4px; }
/* Said out loud, above the board, when the opponent has gone. The player used
   to be left on a board that had simply stopped, with no way to finish that did
   not hand the match to the person who closed the app. */
.opponent-gone {
  /* Never shrinks. The game screen is a full height column, and the board below
     grows to fill it, so without this the one message the player most needs to
     see is the one that gets squeezed away on a short screen. */
  flex: 0 0 auto;
  margin: 0 10px 6px; padding: 8px 12px;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  border: 1px solid rgba(255, 98, 108, .34); border-radius: 8px;
  background: linear-gradient(105deg, rgba(255, 98, 108, .16), rgba(43, 52, 63, .96));
  font-size: 11px; font-weight: 900; letter-spacing: .5px; color: #ffd7da;
  box-shadow: inset 0 1px rgba(255,255,255,.05), 0 4px 14px rgba(0,0,0,.2);
}
.opponent-gone[hidden] { display: none; }
.opponent-gone small { font-size: 10px; font-weight: 800; color: #d3b6ba; letter-spacing: .3px; }
.claim-btn {
  min-height: 34px; padding: 0 14px; border-radius: 7px;
  font-size: 11px; font-weight: 1000; letter-spacing: .6px; color: #2f1318;
  background: linear-gradient(135deg, #ffd052, #ff9d5c);
  box-shadow: inset 0 1px rgba(255,255,255,.5), 0 4px 12px rgba(255, 157, 92, .28);
}
.claim-btn:active, .claim-btn.is-pressed { transform: scale(.97); }

.resourcebar {
  min-height: 44px;
  flex: 0.25 0 44px;
  margin: 2px 10px 6px;
  padding: 6px 10px;
  border: 1px solid rgba(255, 208, 82, .18);
  border-radius: 8px;
  background: linear-gradient(105deg, rgba(255, 208, 82, .10), rgba(43, 52, 63, .96) 34%, rgba(32, 40, 50, .98));
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 12px;
  align-items: center;
  position: relative;
  z-index: 2;
  box-shadow: inset 0 1px rgba(255,255,255,.055), 0 4px 14px rgba(0,0,0,.17);
}
.energy-side { min-width: 0; }
.energy-line { display: flex; align-items: center; justify-content: space-between; gap: 7px; }
.energy-title { font-size: 8px; font-weight: 900; letter-spacing: .65px; color: #d6dee7; }
.pulses { display: flex; gap: 4px; }
.epulse { width: 21px; height: 10px; border-radius: 5px; border: 1px solid rgba(231,240,248,.18); background: rgba(255,255,255,.045); overflow: hidden; position: relative; box-shadow: inset 0 1px rgba(255,255,255,.04); }
.epulse.on { background: linear-gradient(90deg, #ffcb42, #ff7860); border-color: rgba(255,232,151,.65); box-shadow: 0 0 8px rgba(255, 196, 70, .34), inset 0 1px rgba(255,255,255,.45); }
.epulse.charging::before { content: ""; position: absolute; inset: 0; transform-origin: left; transform: scaleX(var(--charge, .2)); background: linear-gradient(90deg, #ffe274, #ff8459); box-shadow: inset 0 1px rgba(255,255,255,.38); }
.tutorial-strip {
  min-height: 108px;
  flex: 0 0 108px;
  /* A little air down to the board. It sat almost against it. */
  margin: 0 7px 9px;
  padding: 10px 12px;
  display: grid;
  grid-template-columns: 54px minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  border-block: 1px solid rgba(166, 140, 255, .24);
  background: linear-gradient(90deg, rgba(166,140,255,.14), rgba(36,220,235,.07) 56%, rgba(255,208,82,.06));
  box-shadow: inset 0 1px rgba(255,255,255,.045);
  transition: background 180ms ease, border-color 180ms ease;
}
/* The lesson objective can run long on a small phone. Anchoring it to the top
   keeps the title and the opening lines on screen; centring the block pushed
   both ends off it. Its only rule used to live in the landscape layout, so it
   disappeared when landscape did. */
.tutorial-help-overlay { justify-content: flex-start; padding: 14px 18px; overflow-y: auto; overscroll-behavior: contain; }
.tutorial-strip.is-complete { border-color: rgba(91, 234, 178, .34); background: linear-gradient(90deg, rgba(91,234,178,.13), rgba(36,220,235,.07) 56%, rgba(255,208,82,.08)); }
.tutorial-step {
  width: 54px;
  height: 46px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  color: #2b243f;
  background: linear-gradient(145deg, #c8b8ff, #8ff2f6);
  font-size: 17px;
  font-weight: 900;
  box-shadow: 0 4px 10px rgba(0,0,0,.22), inset 0 1px rgba(255,255,255,.5);
}
.tutorial-step.complete { color: #152c24; background: linear-gradient(145deg, #8ff2d3, #4edca8); animation: valuePop 320ms ease; }
.tutorial-copy { min-width: 0; }
/* The cover fills the screen behind the menu rather than sitting in the flow as
   a picture. The gradient is what keeps the buttons legible: the artwork is
   deliberately calm at the bottom, and this darkens it further so text over it
   never has to fight the image. */
.start-cover { position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.start-cover img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 22%; }
.start-cover::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(20, 24, 31,.30) 0%, rgba(20, 24, 31,.55) 42%, rgba(20, 24, 31,.88) 74%, rgba(20, 24, 31,.96) 100%);
}
/*
 * Everything in the front page stacks in flow, except the pieces that are
 * pinned to a corner.
 *
 * This rule sets position: relative on every direct child, which silently beats
 * an absolute set on the element itself. It has now caught two things: the
 * language switch, and the streak badge, which sat in the middle of the page
 * looking like a styling accident. Anything pinned has to be named here.
 */
.start-overlay > *:not(.start-cover):not(.language-switch):not(.streak-badge) { position: relative; z-index: 1; }

/* Icons on the mode buttons, sized to the label beside them. */
/* The mode artwork, where an emoji used to be. Emoji are drawn differently on
   every platform and had nothing to do with the game's own visual language. */
.mode-icon { width: 108px; height: 108px; object-fit: contain; margin: 4px auto 10px; display: block;
  filter: drop-shadow(0 6px 20px rgba(0,0,0,.55)); }
.btn-icon { width: 26px; height: 26px; margin-right: 9px; object-fit: contain; vertical-align: -7px; flex: none; }
.win-trophy { width: 96px; height: 144px; object-fit: contain; margin: 0 auto 6px; display: block;
  filter: drop-shadow(0 6px 18px rgba(255,208,82,.30)); }
/* The broken shield after a defeat, and the two commanders facing each other
   while a room waits for its second player. Both replace an emoji that was
   standing in for artwork that did not exist yet.
   Sized against the screen, not against themselves: the waiting picture is
   492 px wide of its own accord and hung 86 px off both edges of a small
   Android before this. */
.lose-art { display: block; width: auto; max-width: 62%; height: 128px;
  object-fit: contain; margin: 0 auto 10px;
  filter: drop-shadow(0 8px 22px rgba(0,0,0,.55)); }
.wait-art { display: block; width: min(86%, 320px); height: auto; margin: 0 auto 12px;
  filter: drop-shadow(0 8px 26px rgba(0,0,0,.5)); }

/* DIVISJONSMERKET, i metallets egen farge, med et lys rundt seg.
 *
 * Fargene laa tidligere tre steder med hver sin kopi, og det ene av dem -
 * `.ready-division` paa klar-skjermen - var graa fordi ingen hadde oppdatert
 * kopien der. Naa staar de ETT sted og gjelder alle, saa et nytt sted der merket
 * vises arver fargen av seg selv.
 *
 * Lyset er en text-shadow i samme farge som teksten (`currentColor`), ikke en
 * fast verdi: da glocher bronse bronse og diamant diamant, uten fire regler til.
 */
.leader-division, .ready-division, .division {
  text-shadow: 0 0 9px currentColor, 0 0 2px rgba(0,0,0,.6);
}
.leader-division.bronze,  .ready-division.bronze,  .division.bronze  { color: #e2a06a; }
.leader-division.silver,  .ready-division.silver,  .division.silver  { color: #dbe5ef; }
.leader-division.gold,    .ready-division.gold,    .division.gold    { color: #ffd052; }
.leader-division.diamond, .ready-division.diamond, .division.diamond { color: #8ff2f6; }
.leader-division { font-size: 10px; font-weight: 900; letter-spacing: .5px; }
/* The division, and the rating it can no longer fall below. The floor is the
   point of the whole thing: it is why climbing towards the top of a band is
   worth doing rather than something to stop and protect. */
.division {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-top: 7px; padding: 7px 11px; border-radius: 8px;
  font-size: 12px; font-weight: 900; letter-spacing: .6px;
  border: 1px solid currentColor; background: rgba(255,255,255,.04);
}
.division-floor { color: #93a2b3; font-size: 10.5px; font-weight: 700; letter-spacing: .2px; text-shadow: none; }
/* The running score against the person you just played. It sits directly above
   the rematch button because that is the thing it is an argument for. */
.h2h {
  margin-bottom: 9px; padding: 8px 12px; border-radius: 8px;
  font-size: 14px; font-weight: 900; letter-spacing: .3px;
  border: 1px solid currentColor; background: rgba(255,255,255,.05);
}
.h2h.up { color: var(--good); }
.h2h.down { color: var(--bad); }
.h2h.even { color: var(--gold); }
/* Rolled up: the step badge and the fold arrow only, so the board gets the
   height back. The objective is on the board in gold anyway. */
.tutorial-strip.collapsed { min-height: 40px; flex-basis: 40px; padding-top: 4px; padding-bottom: 4px; }
.tutorial-strip.collapsed .tutorial-copy,
.tutorial-strip.collapsed .tutorial-help-btn,
.tutorial-strip.collapsed .tutorial-wait { display: none; }
.tutorial-strip.collapsed .tutorial-step { width: 44px; height: 30px; font-size: 13px; }
.tutorial-fold {
  flex: none; width: 34px; height: 34px; display: grid; place-items: center;
  border-radius: 8px; border: 1px solid rgba(231,240,248,.18);
  background: rgba(255,255,255,.06); color: #e7eef6;
  font-size: 15px; font-weight: 900; line-height: 1; cursor: pointer;
}
.tutorial-fold:hover { background: rgba(255,255,255,.13); }
.tutorial-fold:active, .tutorial-fold.is-pressed { transform: scale(.92); background: rgba(255,255,255,.18); }
.tutorial-copy strong { display: block; color: #eeeaff; font-size: 15px; letter-spacing: .4px; }
.tutorial-copy > span { display: -webkit-box; margin-top: 4px; color: #d6dee7; font-size: 13px; line-height: 1.35; white-space: normal; overflow: hidden; -webkit-line-clamp: 4; -webkit-box-orient: vertical; }
.tutorial-help-btn { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 50%; color: #e9edf2; border: 1px solid rgba(231,240,248,.24); background: rgba(28, 35, 43,.62); box-shadow: inset 0 1px rgba(255,255,255,.08); font-family: Georgia, serif; font-size: 15px; font-weight: 900; }
.tutorial-help-btn:active, .tutorial-help-btn.is-pressed { transform: scale(.93); background: rgba(166,140,255,.22); }
/* One grid cell, not two. The strip has three columns, and putting the help
   button and the fold chevron in as separate children made four items: the
   fourth wrapped onto an implicit second row and hung 22 px below the panel it
   belonged to. It looked wrong while working perfectly, which is the kind of
   thing that gets lived with. */
.tutorial-actions { display: flex; align-items: center; gap: 6px; }
.tutorial-continue { min-width: 74px; height: 31px; padding: 0 9px; border-radius: 6px; color: #182420; background: linear-gradient(180deg, #ffe578, #f7c447); border: 1px solid rgba(255,240,172,.72); box-shadow: 0 4px 12px rgba(0,0,0,.24), inset 0 1px rgba(255,255,255,.55); font-size: 8px; font-weight: 1000; letter-spacing: .45px; }
.tutorial-continue:active, .tutorial-continue.is-pressed { transform: translateY(1px) scale(.97); }
.tutorial-wait { max-width: 76px; color: #91eacb; font-size: 8px; font-weight: 900; line-height: 1.2; text-align: right; }
.tutorial-help-step { margin-bottom: 9px; color: var(--gold); font-size: 10px; font-weight: 1000; letter-spacing: 1.3px; }
.tutorial-help-instruction { max-width: 430px; color: #e4eaf0; font-size: 13px; line-height: 1.55; }
.tutorial-tip { width: min(430px, 92vw); margin: 18px 0 6px; padding: 13px 14px; display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 10px; align-items: start; border-block: 1px solid rgba(143,242,246,.24); background: rgba(36,220,235,.055); }
.tutorial-tip > span { color: #8ff2f6; font-size: 14px; text-align: center; }
.tutorial-tip p { margin: 0; color: #bbc8d4; font-size: 11px; line-height: 1.5; }

/* What you are meant to do, in gold. The container lets touches through so the
   board is still playable behind it; only the text itself catches one, and that
   dismisses it.
   Anchored low rather than centred: a lesson board is shorter than the canvas
   and sits in the middle of it, so a centred card landed straight on the piece
   the lesson was telling you to move. Lesson one asked you to drag a crystal
   and then covered it. */
.lesson-goal {
  position: absolute; inset: 0; z-index: 5;
  display: flex; align-items: flex-end; justify-content: center;
  padding: 0 22px 6px; pointer-events: none;
}
.lesson-goal[hidden] { display: none; }
.lesson-goal > span {
  pointer-events: auto; cursor: pointer;
  max-width: 320px; padding: 15px 20px; border-radius: 12px;
  text-align: center; font-size: 19px; font-weight: 800; line-height: 1.3;
  color: var(--gold); text-shadow: 0 2px 12px rgba(0,0,0,.85);
  background: rgba(24, 30, 36, .82);
  border: 1px solid color-mix(in srgb, var(--gold) 46%, transparent);
  box-shadow: 0 10px 30px rgba(0,0,0,.5);
  animation: goalIn 240ms ease-out;
}
@keyframes goalIn { from { opacity: 0; transform: translateY(10px) scale(.97) } to { opacity: 1; transform: none } }

.arena-shell {
  position: relative;
  /*
   * Tall enough for the board and not a pixel taller.
   *
   * The board is seven by seven and the cell size comes from the width, so the
   * height the frame actually needs follows from the width: the grid is as tall
   * as it is wide, plus the zone header the canvas draws above it (cap in
   * relayout(), 78px) and the padding around it. Left to stretch, the frame took
   * the whole column and the board sat in the middle of a much taller panel with
   * dead space above and below.
   *
   * The few px of slack matter: relayout() takes min(byW, byH), so a frame one
   * pixel short would make the height the binding constraint and shrink every
   * cell. Erring high costs a sliver of background; erring low costs the board.
   *
   * The auto margins hand the leftover height back to the column, split evenly
   * above and below, which keeps the bottom bar on the bottom edge instead of
   * letting the whole screen bunch at the top.
   */
  flex: 1.6 1 calc(100vw + 84px);
  max-height: calc(100vw + 190px);
  min-height: 0;
  /* Trimmed to the frame. Every pixel here is a pixel the cells do not get. */
  margin: auto 3px;
  overflow: hidden;
  border-radius: 8px;
  border: 1px solid rgba(231, 240, 248, .13);
  /* Lifted from near black. The board reads as a lit surface the pieces sit on
     rather than a hole they float in, which is what players expect. */
  background: linear-gradient(180deg, rgba(55, 67, 82, .98), rgba(36, 44, 55, .99));
  box-shadow: 0 12px 28px rgba(0,0,0,.32), inset 0 1px rgba(255,255,255,.045);
}
#phaserArena { position: absolute; inset: 0; touch-action: none; contain: layout paint style; transform: translateZ(0); will-change: transform; }
#phaserArena canvas { display: block; width: 100% !important; height: 100% !important; touch-action: none; outline: none; image-rendering: auto; }

.bottom-bar { min-height: 39px; flex: 1.2 0 39px; padding: 4px 10px 5px; display: grid; grid-template-columns: 66px 1fr auto 1fr 66px; gap: 9px; align-items: center; }
.bottom-spacer { min-width: 0; }
/*
 * Five children, five columns.
 *
 * The bar was a three column grid, and dropping the two audio switches into it
 * gave it seven children. Grid put the overflow on an implicit second row, and
 * since the bar is a fixed 39 pixels tall the INFO button simply disappeared
 * below the edge. The switches are wrapped in one element so they occupy one
 * column between them.
 */
.bottom-audio { display: flex; gap: 6px; justify-content: center; min-width: 0; }
.foot-btn { height: 29px; border-radius: 6px; background: linear-gradient(180deg, #374554, #242c37); border: 1px solid rgba(231,240,248,.14); font-size: 9px; font-weight: 900; letter-spacing: .65px; box-shadow: inset 0 1px rgba(255,255,255,.07), 0 4px 12px rgba(0,0,0,.20); transition: transform 80ms ease, filter 120ms ease; }
.foot-btn:active, .foot-btn.is-pressed { transform: translateY(1px) scale(.965); filter: brightness(1.14); }

/* `safe center`, ikke `center`. Den ene forskjellen er hele feilen.
 *
 * MAALT paa telefon: toppen av flere skjermer var klippet av selv etter aa ha
 * rullet helt opp - hodet paa figuren manglet, oeverste rad med ansikter laa
 * halvveis utenfor. `npm run test:topp` satte tall paa det: 73 piksler utenfor
 * paa 390x844 med ansiktsvelgeren aapen, 175 paa 360x640.
 *
 * Aarsaken ser riktig ut i koden, og det er nettopp derfor den overlevde saa
 * lenge: et sentrert flex-innhold som blir hoyere enn beholderen flyter over
 * LIKT i begge retninger, men rulling naar bare det som ligger under. Overskuddet
 * paa toppen er utenfor, og ingen rulling i verden henter det inn.
 *
 * `safe center` sier: sentrer naar det er plass, og fall tilbake til toppen naar
 * det ikke er det. Eldre nettlesere forstaar ikke verdien og beholder linja
 * over, saa dette kan ikke gjore noe verre noe sted. */
.overlay { position: absolute; inset: 0; z-index: 500; display: flex; flex-direction: column; align-items: center; justify-content: center; justify-content: safe center; padding: 26px 18px; overflow-y: auto; overscroll-behavior: contain; background: linear-gradient(180deg, rgba(39, 48, 59, .995), rgba(23, 27, 34, .998) 58%, rgba(19, 21, 26, 1)); }
.start-overlay { justify-content: flex-end; padding: max(48px, calc(env(safe-area-inset-top) + 40px)) 0 max(26px, calc(env(safe-area-inset-bottom) + 18px)); background: #171c24; }
/* Clear of the language switch, which is pinned to the top right. The title
   used to sit under it. */
.start-heading { position: relative; z-index: 2; margin-bottom: auto; padding: 24px 6px 10px; text-align: center; }
/* The name carries the page, and it is artwork rather than text: the metal, the
   blue to red gradient and the subtitle are one piece, which no web font can
   reproduce. Because it is a single image it also cannot rewrap or reflow, so
   it looks the same on every phone.
   The mark fills 90 % of the picture, the rest is its own glow, so a full width
   image puts the letters at about 87 % of the screen. Capped on a tablet, where
   full width would be absurd. */
.start-logo { display: block; width: 100%; max-width: 560px; height: auto; margin: 0 auto; }
.start-art { position: relative; width: 100%; aspect-ratio: 16 / 7.7; overflow: hidden; border-block: 1px solid rgba(255,255,255,.10); background: #151c2a; box-shadow: 0 14px 34px rgba(0,0,0,.36); }
.start-art::after { content: ""; position: absolute; inset: 0; pointer-events: none; box-shadow: inset 0 0 40px rgba(15, 20, 30,.45); }
.start-art img { width: 100%; height: 100%; display: block; object-fit: cover; object-position: center 56%; }
/* Over artwork, not over a flat panel. The shadow is what keeps it readable
   where the image is busiest. */
/* Storre enn resten av .ov-sub: dette er det eneste et forstegangsbesok
   faktisk leser, og sporsmaalet nederst skal treffe. Teksten ble kortere da den
   ble et sporsmaal, saa den taaler storrelsen uten aa ta mer plass. */
.ov-sub.start-copy { margin: 13px 18px 16px; color: #dfe8f2; font-size: 13px; line-height: 1.5;
  text-shadow: 0 2px 10px rgba(0,0,0,.9), 0 0 22px rgba(0,0,0,.7); }
.ov-sub.start-copy b { color: #fff; font-size: 14.5px; }
.start-menu { padding-inline: 18px; }
/* Lavere knapper BARE her. Forsiden er forankret nederst og overskriften har
   margin-bottom:auto, saa hver piksel spart nedenfor blir luft under logoen.
   Lyd- og musikkknappene og fotnotene er ikke .menu-btn og roeres ikke. */
.start-menu .menu-btn { min-height: 46px; padding-top: 10px; padding-bottom: 10px; font-size: 12.5px; }
.start-menu { gap: 8px; }
.ov-icon { font-size: 50px; margin-bottom: 8px; filter: drop-shadow(0 5px 9px rgba(0,0,0,.26)); }
/* The painted marks that replaced the emoji. Height is what is fixed, so a
   wide one like the speed streaks and a tall one like the flag read as the
   same weight on the screen instead of the wide one dwarfing the rest. */
.ov-mark { height: 92px; width: auto; max-width: 78vw; display: block; }
.ov-title { font-size: 25px; font-weight: 900; letter-spacing: 2px; text-align: center; }
.ov-sub { font-size: 12px; color: var(--muted); line-height: 1.58; text-align: center; max-width: 360px; margin: 10px 0 17px; }
/* Reglene for navnet star under advarselen om at det er permanent. Mindre, fordi
   det er opplysning og ikke noe du maa lese for aa komme videre. */
.name-rules { display: inline-block; margin-top: 4px; font-size: 10px; opacity: .82; }
/* En knapp som ser ut som en lenke. Aa hente tilbake en profil er sjelden, saa
   den skal vaere der uten aa konkurrere med det man vanligvis gjor. */
.link-btn { margin-top: 7px; padding: 4px 2px; border: 0; background: none; color: var(--gold);
  font: inherit; font-size: 11px; text-decoration: underline; cursor: pointer; }
/* Koden er lang og maa kunne leses tegn for tegn, saa den brytes og staar i
   fastbredde. Uten word-break gikk den ut av skjermen paa en telefon. */
.recovery { margin-bottom: 10px; text-align: center; }
.recovery-title { font-size: 10px; letter-spacing: .8px; color: var(--muted); margin-bottom: 5px; }
.recovery-code { padding: 9px 10px; border: 1px solid rgba(255,255,255,.14); border-radius: 7px;
  background: rgba(255,255,255,.05); color: #dce4eb; font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 11px; line-height: 1.5; word-break: break-all; user-select: all; margin-bottom: 8px; }
.recovery-why { margin-top: 7px; font-size: 10px; color: var(--muted); line-height: 1.5; }
.recovery-input { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; letter-spacing: 0; }
.ov-sub b { color: var(--text); }
/*
 * The winning run. Gold, top left, opposite the language switch.
 *
 * Absolute like the switch, so it sits in the corner rather than pushing the
 * logo down. It is only rendered when there is a run, so it never occupies the
 * corner for nothing.
 */
/* Top left of the front page, and as high as the phone allows. max() keeps it
   clear of a notch; the 3px is what a phone without one gets. It is the first
   thing worth being proud of, so it sits above everything else. */
.streak-badge { position: absolute; top: var(--top-row); left: 10px; z-index: 3;
  display: flex; align-items: center; gap: 6px; padding: 5px 12px 5px 9px; border-radius: 999px;
  background: linear-gradient(135deg, rgba(255,208,82,.20), rgba(255,166,50,.12));
  border: 1px solid rgba(255,208,82,.55); box-shadow: 0 3px 12px rgba(255,170,40,.16); }
.streak-count { font-size: 12px; font-weight: 1000; letter-spacing: .5px; color: #ffd97a; }
/* Ratingen rett under rekka, og flammen ved siden av BEGGE linjene.
 *
 * Forste utgave la flammen absolutt i hjornet, og da la den seg oppaa k-en i
 * STREAK: merket var ikke bredt nok til aa gi den plass, og absolutt plassering
 * tar ikke plass - den bare legger seg der. Naa er teksten en soyle og flammen
 * et eget ledd ved siden av, saa den sentrerer seg selv mot begge linjene og
 * skyver rammen ut akkurat saa mye som den trenger. */
.streak-badge { flex-direction: row; align-items: center; gap: 11px; padding: 6px 15px 6px 13px; }
.streak-tekst { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; min-width: 0; }
.streak-rating { font-size: 10px; font-weight: 900; letter-spacing: .45px; color: #ffe9b0; }
/* Uten rekke er merket bare ratingen, og da skal det ikke rope like hoyt. */
.streak-badge.bare-rating { background: rgba(255,255,255,.05); border-color: rgba(255,255,255,.14);
  box-shadow: none; padding: 5px 11px; }
.streak-badge.bare-rating .streak-rating { color: #cfd8e2; }
.streak-flame { width: 9px; height: 9px; border-radius: 50%; background: #ffd052;
  box-shadow: 0 0 7px rgba(255,208,82,.9); animation: streak-pulse 1.9s ease-in-out infinite; }
@keyframes streak-pulse { 0%,100% { opacity: .55; transform: scale(.85); } 50% { opacity: 1; transform: scale(1.15); } }
.opp-streak { margin-left: 6px; font-size: 8.5px; font-weight: 900; color: #ffd97a; }
.profile-streak { color: #ffd97a; font-weight: 900; }
/* Said instead of nothing when there is no rating change to report. */
.rating-note { font-size: 11px; font-weight: 800; color: #a8b5c2; letter-spacing: .3px; }

/* The match report. Label on the left, number on the right, one line each, so
   the eye runs down a single column of values instead of hunting through a
   sentence. The row that says how much was stopped is the one worth reading
   first, so it is the one that is bigger. */
.stat-rows { width: 100%; max-width: 320px; margin: 4px auto 2px; text-align: left; }
.stat-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 5px 2px; border-bottom: 1px solid rgba(238,247,255,.07); font-size: 11.5px; }
.stat-row:last-child { border-bottom: 0; }
.stat-row > span { color: var(--muted); }
.stat-row > b { color: var(--text); font-weight: 900; white-space: nowrap; }
.stat-row > b > small { display: inline-block; margin-left: 7px; font-size: 10px;
  font-weight: 800; color: var(--gold); }
.stat-row.wide { font-size: 14px; padding: 8px 2px; }
.stat-row.wide > b > small { font-size: 13px; }
.stat-section { grid-column: 1 / -1; margin-top: 5px; padding-top: 5px; border-top: 1px solid rgba(255,208,82,.22); }
.stat-section-title { padding: 3px 2px 5px; color: var(--gold); font-size: 10px; font-weight: 900; letter-spacing: .7px; }

/* BELONNINGSKORTENE LIGGER OPPAA BRETTET, ikke paa en egen skjerm.
 *
 * `.overlay` er fellesskjermen: `inset: 0` med en ugjennomsiktig gradient. Den
 * er riktig for meny, profil og sluttbilde, men her gjemte den nettopp det
 * spilleren hadde jobbet for aa se - vandreren staaende paa maalstreken, og en
 * kamp som fortsetter mens han velger.
 *
 * Brettet laa allerede bak i DOM-en; det ble bare malt over. Derfor er dette
 * bare maling: kortene skyves ned, og bakgrunnen gaar fra helt gjennomsiktig
 * oeverst til moerk nede, saa kortene fortsatt er lesbare mot et brett i
 * bevegelse.
 *
 * Overlegget tar fortsatt imot trykk. Det er med vilje: et bomtrykk ved siden
 * av et kort skal ikke flytte en krystall mens valget staar aapent.
 */
.walker-reward-overlay {
  gap: 14px;
  justify-content: flex-end;
  padding-bottom: max(18px, calc(env(safe-area-inset-bottom) + 12px));
  background: linear-gradient(180deg,
    rgba(14,18,24,0) 0%,
    rgba(14,18,24,.10) 34%,
    rgba(14,18,24,.62) 58%,
    rgba(14,18,24,.90) 78%);
}
.walker-reward-overlay .ov-title { text-shadow: 0 2px 10px rgba(0,0,0,.85); }
.walker-rewards { width: min(100%, 390px); display: grid; grid-template-columns: repeat(3, minmax(86px, 1fr)); gap: 9px; }
.walker-reward-card { min-width: 0; min-height: 142px; padding: 13px 7px 11px; display: grid; grid-template-rows: 82px auto; place-items: center; gap: 7px; border-radius: 8px; color: #effaff; background: linear-gradient(180deg, rgba(38,52,66,.98), rgba(20,27,35,.98)); border: 1px solid rgba(145,221,237,.36); box-shadow: 0 10px 25px rgba(0,0,0,.38), inset 0 1px rgba(255,255,255,.10); }
.walker-reward-card b { font-size: 10px; line-height: 1.25; letter-spacing: .35px; }
.walker-reward-card.pulse { border-color: rgba(255,208,82,.64); }
.walker-reward-card.stock { border-color: rgba(82,223,161,.55); }
.walker-reward-card.lives { border-color: rgba(255,91,130,.58); }
.walker-reward-card:active, .walker-reward-card.is-pressed { transform: translateY(2px) scale(.97); filter: brightness(1.16); }
.walker-reward-card:disabled { opacity: .42; cursor: wait; }
.walker-reward-card.is-pending { opacity: 1; border-color: #ffd052; box-shadow: 0 0 0 2px rgba(255,208,82,.28), 0 0 24px rgba(255,208,82,.30), inset 0 1px rgba(255,255,255,.13); animation: rewardPending 850ms ease-in-out infinite; }
@keyframes rewardPending { 50% { filter: brightness(1.18); transform: translateY(-1px); } }
.reward-visual { width: 82px; height: 82px; position: relative; display: grid; place-items: center; }
.reward-pulse { position: relative; width: 58px; height: 25px; border-radius: 13px; background: linear-gradient(90deg,#ffbd3e,#fff29b 48%,#ff8b55); border: 2px solid #fff1a2; box-shadow: 0 0 8px #ffd554, 0 0 23px rgba(255,190,50,.68), inset 0 3px rgba(255,255,255,.55); }
.reward-pulse::after { content: ''; position: absolute; left: 8px; top: 4px; width: 29px; height: 7px; border-radius: 5px; background: rgba(255,255,255,.58); }
.reward-crystals { position: relative; width: 76px; height: 76px; display: block; }
.reward-crystals img { position: absolute; width: 54px; height: 54px; object-fit: contain; filter: drop-shadow(0 5px 6px rgba(0,0,0,.42)); }
.reward-crystals img:nth-child(1) { left: 0; top: 17px; transform: rotate(-13deg); }
.reward-crystals img:nth-child(2) { right: 0; top: 17px; transform: rotate(13deg); }
.reward-crystals img:nth-child(3) { left: 11px; top: 0; }
.reward-lives { display: flex; align-items: center; justify-content: center; letter-spacing: 0; }
.reward-lives i { margin-left: -7px; color: #ff557f; font-size: 39px; font-style: normal; line-height: 1; text-shadow: 0 0 12px rgba(255,85,127,.8), 0 4px 5px rgba(0,0,0,.45); }
.reward-lives i:first-child { margin-left: 0; }
.walker-reward-cancel { width: min(100%, 390px); }

/* A row you can open. Both lists carry a profile behind them, so both say so
   the same way: a thumb sized target that lifts a little when pressed. */
.leader-row[data-player], .opp-row[data-player] { cursor: pointer; -webkit-tap-highlight-color: transparent; }
.leader-row[data-player]:active, .opp-row[data-player]:active { transform: scale(.988); }
.leader-row[data-player]::after, .opp-row[data-player]::after {
  content: '›'; margin-left: 6px; color: var(--muted); font-weight: 900; font-size: 15px; line-height: 1; }
.opp-row[data-player]::after { order: 9; align-self: center; }

/* The faces. Round, because a portrait in a circle reads as a person and a
   portrait in a square reads as a picture of one. The chosen one is ringed in
   gold rather than merely brighter: on a small screen a difference in weight is
   easier to see than a difference in shade. */
.avatar { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.avatar.big { width: 96px; height: 96px; margin: 0 auto 6px;
  border: 2px solid rgba(255,208,82,.5); box-shadow: 0 6px 18px rgba(0,0,0,.35); }
.avatar-button { display: block; margin: 0 auto 6px; padding: 0; border-radius: 50%;
  transition: transform 90ms ease; }
.avatar-button:active, .avatar-button.is-pressed { transform: scale(.95); }
.avatar-picker { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px;
  width: 100%; max-width: 320px; margin: 8px auto 12px; }
.avatar-choice { padding: 2px; border-radius: 50%; border: 2px solid transparent;
  background: rgba(255,255,255,.04); transition: transform 90ms ease, border-color 120ms ease; }
.avatar-choice > .avatar { width: 100%; height: auto; aspect-ratio: 1 / 1; }
.avatar-choice.is-picked { border-color: var(--gold); box-shadow: 0 0 12px rgba(255,208,82,.35); }
.avatar-choice:active { transform: scale(.94); }

/* Before the match: two players side by side, yours on the left. Whoever has
   said ready is ringed in gold, so the state of the room is readable at a
   glance rather than from a sentence. */
/* Samme `safe center` som fellesregelen: klar-skjermen viser to spillerkort og
 * kan bli hoyere enn en liten telefon. */
.ready-overlay { justify-content: center; justify-content: safe center; gap: 4px; }
.ready-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
  width: 100%; max-width: 380px; margin: 6px auto 4px; }
.ready-side { padding: 10px 8px 8px; border-radius: 12px; text-align: center;
  border: 1px solid var(--line); background: linear-gradient(180deg, rgba(43,52,63,.96), rgba(24,30,38,.98)); }
.ready-side.is-me { border-color: rgba(36,220,235,.45); }
.ready-side.is-ready { border-color: var(--gold); box-shadow: 0 0 14px rgba(255,208,82,.22); }
.ready-side .avatar.big { width: 76px; height: 76px; }
.ready-name { font-size: 13px; font-weight: 900; letter-spacing: .4px; margin-top: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Fargen kommer fra fellesregelen lenger opp. Her sto `color: var(--muted)`,
 * altsaa graatt - merket var fargelagt to andre steder og glemt her. */
.ready-division { font-size: 9px; font-weight: 900; letter-spacing: .5px; margin-bottom: 6px; }
.ready-lines { text-align: left; }
.ready-line { display: flex; justify-content: space-between; gap: 6px; padding: 3px 2px;
  font-size: 10.5px; border-bottom: 1px solid rgba(238,247,255,.06); }
.ready-line:last-child { border-bottom: 0; }
.ready-line > span { color: var(--muted); }
.ready-line > b { color: var(--text); font-weight: 900; }
.ready-flag { min-height: 14px; margin-top: 6px; font-size: 10px; font-weight: 1000;
  letter-spacing: .8px; color: var(--gold); }
.ready-waiting { padding: 12px; font-size: 12px; font-weight: 800; color: var(--muted); text-align: center; }

.language-switch { position: absolute; top: var(--top-row); right: 10px; display: grid; grid-template-columns: 1fr 1fr; padding: 2px; border: 1px solid rgba(255,255,255,.14); border-radius: 7px; background: rgba(21, 27, 34,.78); box-shadow: 0 5px 14px rgba(0,0,0,.24); }
.language-option { min-width: 46px; height: 24px; padding: 0 6px; border-radius: 4px; color: #8f9daa; background: transparent; font-size: 8.5px; font-weight: 900; letter-spacing: .4px; transition: color 120ms ease, background 120ms ease, transform 80ms ease; }
.language-option.selected { color: #17272f; background: linear-gradient(135deg, #7ef6ff, #dffcff); box-shadow: inset 0 1px rgba(255,255,255,.55), 0 3px 9px rgba(38,211,229,.18); }
.language-option:active, .language-option.is-pressed { transform: scale(.96); }
/* Small on purpose, but a thumb is not. The button keeps its size while an
   invisible pad around it reaches the 48 px target Android asks for. */
.language-option::after { content: ""; position: absolute; inset: -12px -6px; }
.language-option { position: relative; }
.menu { width: min(100%, 340px); display: flex; flex-direction: column; gap: 9px; }
/* The buttons take their language from the artwork: dark glass panels with a
   lit edge, the way the shield walls and consoles in the image are drawn. Flat
   saturated fills sat on top of the picture and looked like a different app. */
.menu-btn {
  min-height: 52px; padding: 12px 16px; border-radius: 10px;
  color: #d6f7ff; font-size: 13.5px; font-weight: 900; letter-spacing: .7px;
  background: linear-gradient(180deg, rgba(36, 55, 74,.86), rgba(24, 34, 44,.92));
  border: 1px solid rgba(84,214,240,.55);
  box-shadow: 0 8px 22px rgba(0,0,0,.45), inset 0 1px rgba(255,255,255,.10), 0 0 18px rgba(36,220,235,.16);
  text-shadow: 0 0 12px rgba(36,220,235,.45);
  transition: transform 90ms ease, filter 120ms ease, box-shadow 160ms ease;
}
/*
 * Butikkstripa paa hovedmenyen. EN linje, med vilje.
 *
 * Den ble forst bygget som en full menyknapp paa sluttskjermen. MAALT paa
 * 390x844 var innholdet da 922 piksler mot 844 skjerm - knappen laa under
 * kanten der ingen ser den. Den er derfor bade flyttet og gjort smal: den skal
 * vaere synlig uten aa konkurrere med SPILL, som er hele grunnen til at noen er
 * paa skjermen.
 *
 * En <a> og ikke en <button>, fordi en ekte lenke kan langtrykkes og aapnes i ny
 * fane - nettopp det folk gjor med en butikklenke de vil se paa senere.
 */
.butikk-stripe {
  display: flex; align-items: baseline; justify-content: center; flex-wrap: wrap; gap: 4px 8px;
  min-height: 38px; padding: 9px 14px; border-radius: 999px; text-decoration: none;
  background: rgba(255,255,255,.045); border: 1px solid rgba(231,240,248,.16); color: #cfe0f2;
}
.butikk-stripe b { font-size: 12px; font-weight: 900; letter-spacing: .5px; }
.butikk-stripe span { font-size: 11px; font-weight: 600; color: #8fa3b8; }
.menu-btn.orange {
  color: #ffdfe6;
  background: linear-gradient(180deg, rgba(77, 36, 50,.86), rgba(42, 24, 31,.92));
  border-color: rgba(255,91,130,.60);
  box-shadow: 0 8px 22px rgba(0,0,0,.45), inset 0 1px rgba(255,255,255,.10), 0 0 18px rgba(255,91,130,.20);
  text-shadow: 0 0 12px rgba(255,91,130,.50);
}
.menu-btn.gold {
  color: #ffe9ac;
  background: linear-gradient(180deg, rgba(74, 59, 26,.88), rgba(44, 35, 17,.94));
  border-color: rgba(255,208,82,.68);
  box-shadow: 0 8px 24px rgba(0,0,0,.5), inset 0 1px rgba(255,255,255,.12), 0 0 22px rgba(255,208,82,.24);
  text-shadow: 0 0 12px rgba(255,208,82,.55);
}
.menu-btn.dark { background: linear-gradient(180deg, rgba(44, 55, 69,.78), rgba(27, 34, 42,.88)); color: #c6d4e2; border-color: rgba(231,240,248,.20); box-shadow: 0 6px 18px rgba(0,0,0,.4), inset 0 1px rgba(255,255,255,.07); text-shadow: none; }
.menu-btn:active, .menu-btn.is-pressed { transform: translateY(2px) scale(.98); filter: brightness(1.08); }
.menu-btn:disabled { opacity: .45; }

/*
 * The people you have played, and what they are doing.
 *
 * The status word carries the meaning, not a coloured dot alone: a dot is
 * invisible to a colourblind player and meaningless to everyone on a first
 * look. The colour is there to make it quick once the word has been read once.
 */
/* Skiller lista fra handlingene over. Klassen ble laget da lista fikk sin egen
   overskrift, men aldri stilt - den satt klistret inntil kodefeltet. */
.opp-heading { width: min(100%, 340px); margin: 18px 0 8px; font-size: 10px; font-weight: 900;
  letter-spacing: 1.1px; color: var(--muted); text-align: center; }
.opp-list { display: flex; flex-direction: column; gap: 6px; width: 100%; max-height: 46vh; overflow-y: auto; padding: 2px 0; }
.opp-row { display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: center;
  padding: 7px 10px; border-radius: 8px; background: rgba(36, 46, 57,.72); border: 1px solid rgba(231,240,248,.10); }
.opp-who { min-width: 0; text-align: left; }
.opp-name { font-size: 12px; font-weight: 900; color: #eaf1f8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.opp-status { font-size: 8.5px; font-weight: 900; letter-spacing: .5px; margin-top: 2px; }
.opp-status.free { color: #6ee7a0; }
.opp-status.busy { color: #ffd052; }
.opp-status.away { color: #7c8794; }
.opp-btn { min-height: 30px; padding: 0 12px; font-size: 9px; }
.opp-btn:disabled { opacity: .32; }
.opp-waiting { font-size: 8.5px; font-weight: 900; color: #ffd052; letter-spacing: .4px; }

/*
 * An incoming challenge. It sits over the menu because the person who sent it
 * is waiting and it expires; a line further down the page would be missed.
 */
.challenge-card { position: fixed; left: 50%; transform: translateX(-50%); bottom: max(16px, env(safe-area-inset-bottom));
  width: min(92%, 420px); padding: 11px 13px; border-radius: 11px;
  /*
   * Above the menu overlay, which sits at 500.
   *
   * At 40 the card was painted and readable and could not be touched: the
   * overlay covered it for hit testing, so every tap on accept went into the
   * menu behind it and nothing happened. Measured with elementFromPoint, which
   * is the only way to tell the difference between visible and reachable.
   */
  z-index: 600;
  background: linear-gradient(180deg, rgba(50, 63, 80,.97), rgba(31, 39, 48,.98));
  border: 1px solid rgba(255,208,82,.42); box-shadow: 0 10px 30px rgba(0,0,0,.5);
  animation: challenge-in 220ms cubic-bezier(.2,.8,.3,1); }
@keyframes challenge-in { from { opacity: 0; transform: translateX(-50%) translateY(14px); } }
.challenge-who { font-size: 11px; font-weight: 900; color: #ffe9a8; text-align: center; margin-bottom: 8px; }
.challenge-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
.challenge-actions .menu-btn { min-height: 34px; font-size: 9.5px; }

/*
 * The two audio switches.
 *
 * Off is shown by dimming the word and striking it through, not by an icon. A
 * speaker glyph depends on a font that is not guaranteed, and a crossed out
 * speaker is genuinely ambiguous at this size: it reads as "press to mute" to
 * about as many people as it reads as "currently muted".
 */
.audio-row { display: flex; justify-content: center; gap: 8px; padding: 2px 0 4px; }
.audio-btn { min-height: 30px; padding: 0 13px; border-radius: 6px; background: linear-gradient(180deg, #374554, #242c37);
  border: 1px solid rgba(231,240,248,.16); color: #cdd8e4; font-size: 9px; font-weight: 900; letter-spacing: .7px;
  box-shadow: inset 0 1px rgba(255,255,255,.07), 0 4px 12px rgba(0,0,0,.20); }
.audio-toggle.off { opacity: .42; text-decoration: line-through; text-decoration-thickness: 1.5px; }
.bottom-bar .audio-toggle { padding: 0 9px; }

/*
 * One press feel for every button in the game.
 *
 * Down is instant, up is eased over 130 ms. Putting a transition on the way
 * down would insert a delay between the finger and the picture, and that is
 * precisely what a player reads as lag.
 *
 * The .is-pressed class is set from pointerdown rather than relying on :active
 * alone. On touch, :active is applied inconsistently and sometimes late, and a
 * press that only sometimes shows is worse than none. Both triggers are kept so
 * a mouse still works if the pointer events ever stop firing.
 */
.audio-btn:active, .audio-btn.is-pressed { transform: translateY(1px) scale(.965); filter: brightness(1.14); }
.menu-btn, .foot-btn, .claim-btn, .language-option, .side-option, .audio-btn,
.tutorial-help-btn, .tutorial-fold, .tutorial-continue, .profile-delete,
.utility-actions button, .utility-actions a {
  transition: transform 130ms cubic-bezier(.22,.68,.3,1), filter 130ms ease, background 130ms ease;
  -webkit-tap-highlight-color: transparent;
}
.menu-btn.is-pressed, .foot-btn.is-pressed, .claim-btn.is-pressed,
.language-option.is-pressed, .side-option.is-pressed, .tutorial-help-btn.is-pressed,
.tutorial-fold.is-pressed, .tutorial-continue.is-pressed, .profile-delete.is-pressed, .audio-btn.is-pressed,
.utility-actions button.is-pressed, .utility-actions a.is-pressed,
.menu-btn:active, .foot-btn:active, .claim-btn:active,
.language-option:active, .side-option:active, .tutorial-help-btn:active,
.tutorial-fold:active, .tutorial-continue:active, .profile-delete:active, .audio-btn:active,
.utility-actions button:active, .utility-actions a:active { transition-duration: 0s; }
.join-row { display: grid; grid-template-columns: 1fr 1.22fr; gap: 9px; }
.room-input { min-width: 0; border: 1px solid rgba(255,255,255,.17); border-radius: 8px; background: #222a33; color: #fff; text-align: center; text-transform: uppercase; font-size: 19px; font-weight: 900; letter-spacing: 3px; }
.join-label { margin: 3px 0 -2px; color: #aeb8c3; font-size: 9px; font-weight: 900; letter-spacing: .7px; text-align: center; }
.side-choice { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 10px; padding: 7px 8px 7px 11px; border: 1px solid rgba(255,255,255,.12); border-radius: 8px; background: rgba(26, 32, 40,.72); color: #aeb8c3; font-size: 9px; font-weight: 900; letter-spacing: .6px; }
.side-options { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
.side-option { min-height: 34px; border-radius: 5px; background: #303e4d; color: #aeb8c3; font-size: 10px; font-weight: 900; }
.side-option:active, .side-option.is-pressed { transform: translateY(1px) scale(.965); filter: brightness(1.12); }
.side-option.selected { background: linear-gradient(135deg, #66eaf2, #d5fbff); color: #15242b; box-shadow: inset 0 1px rgba(255,255,255,.55), 0 3px 12px rgba(50,216,230,.18); }
.match-spinner { width: 46px; height: 46px; margin-bottom: 20px; border: 4px solid rgba(255,255,255,.12); border-top-color: var(--gold); border-radius: 50%; animation: spin .85s linear infinite; }
.match-spinner.small { width: 32px; height: 32px; margin: 22px auto; border-width: 3px; }
.rematch-note { padding: 9px 11px; border: 1px solid rgba(255,255,255,.12); border-radius: 7px; background: rgba(255,255,255,.04); color: #d9e1e8; font-size: 10px; font-weight: 800; text-align: center; }
@keyframes spin { to { transform: rotate(360deg); } }
.wait-code { font-size: 38px; font-weight: 900; letter-spacing: 6px; color: var(--gold); margin: 4px 0 10px; }
.rules { max-width: 390px; max-height: 66vh; overflow: auto; padding: 15px 16px; border-radius: 8px; background: rgba(32, 39, 48,.98); font-size: 11px; line-height: 1.55; color: #cbd4dd; border: 1px solid rgba(255,255,255,.09); }
.rules h3 { font-size: 12px; color: var(--gold); margin: 11px 0 3px; }
.rules h3:first-child { margin-top: 0; }
.profile-mark { width: 54px; height: 54px; display: grid; place-items: center; border-radius: 50%; color: #183745; background: linear-gradient(145deg, #83f5fa, #58cad7); font-size: 29px; font-weight: 900; box-shadow: 0 8px 22px rgba(44,209,225,.20), inset 0 1px rgba(255,255,255,.55); }
.profile-form { width: min(100%, 340px); display: flex; flex-direction: column; gap: 9px; }
.player-name-input { width: 100%; height: 50px; padding: 0 14px; border: 1px solid rgba(126,246,255,.34); border-radius: 8px; outline: none; background: rgba(24, 32, 40,.94); color: #fff; text-align: center; font-size: 17px; font-weight: 900; letter-spacing: .7px; box-shadow: inset 0 1px rgba(255,255,255,.055), 0 6px 18px rgba(0,0,0,.22); }
.player-name-input:focus { border-color: #7ef6ff; box-shadow: 0 0 0 3px rgba(126,246,255,.11), inset 0 1px rgba(255,255,255,.07); }
.profile-setup-overlay { justify-content: flex-start; padding: max(51px, calc(env(safe-area-inset-top) + 45px)) 0 max(18px, env(safe-area-inset-bottom)); background: #171c24; }
.profile-setup-art { position: relative; width: 100%; max-width: 760px; aspect-ratio: 16 / 7.35; overflow: hidden; border-block: 1px solid rgba(255,255,255,.11); background: #152332; box-shadow: 0 16px 38px rgba(0,0,0,.42); animation: profileArtReveal 520ms cubic-bezier(.2,.8,.2,1) both; }
.profile-setup-art::after { content: ""; position: absolute; inset: 0; pointer-events: none; box-shadow: inset 0 -34px 34px rgba(17, 23, 31,.42), inset 0 0 34px rgba(15, 19, 26,.26); }
.profile-setup-art img { width: 100%; height: 100%; display: block; object-fit: cover; object-position: center 52%; }
.profile-art-sheen { position: absolute; inset: 0; pointer-events: none; background: linear-gradient(112deg, transparent 32%, rgba(255,255,255,.08) 49%, transparent 61%); transform: translateX(-100%); animation: profileSheen 4.6s 900ms ease-in-out infinite; }
.profile-setup-content { width: min(100%, 390px); display: flex; flex-direction: column; align-items: center; gap: 9px; padding: 17px 18px 0; text-align: center; animation: profileContentRise 420ms 130ms cubic-bezier(.2,.8,.2,1) both; }
.profile-setup-content .ov-title { font-size: clamp(22px, 7vw, 31px); }
.profile-setup-content .ov-sub { margin: 0 0 2px; font-size: 11px; line-height: 1.48; }
.profile-kicker { color: var(--gold); font-size: 9px; font-weight: 1000; letter-spacing: 1.8px; }
@keyframes profileArtReveal { from { opacity: 0; transform: scale(1.035); } to { opacity: 1; transform: scale(1); } }
@keyframes profileContentRise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes profileSheen { 0%,68% { transform: translateX(-110%); } 88%,100% { transform: translateX(110%); } }
.profile-delete:active, .profile-delete.is-pressed { transform: translateY(1px) scale(.97); filter: brightness(1.2); }
.profile-delete { min-height: 32px; color: #d58f96; font-size: 8px; font-weight: 900; text-decoration: underline; text-underline-offset: 3px; }
.profile-summary { position: absolute; top: max(14px, env(safe-area-inset-top)); left: 14px; max-width: 170px; min-height: 35px; display: flex; align-items: center; gap: 7px; }
.profile-summary > div { min-width: 0; display: flex; flex-direction: column; }
.profile-summary strong { max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 10px; letter-spacing: .45px; }
/* Ratingen rett UNDER navnet, ikke i en egen ramme ved siden av: den hoerer til
 * personen, og lest side om side sa den ut som noe annet. */
.summary-rating { font-size: 9px; font-weight: 900; letter-spacing: .4px; color: var(--gold); white-space: nowrap; }
/* Storre navn i tospiller. Det er der navnet betyr noe - det er dette
 * motstanderen ser. */
.profile-summary.stor { max-width: 215px; }
.profile-summary.stor strong { max-width: 155px; font-size: 13.5px; letter-spacing: .3px; }
.profile-summary.stor .summary-rating { font-size: 10.5px; }
.profile-summary > b { min-height: 29px; padding: 0 8px; display: grid; place-items: center; border: 1px solid rgba(255,255,255,.12); border-radius: 6px; background: rgba(255,255,255,.045); color: var(--gold); font-size: 8px; font-weight: 900; white-space: nowrap; }
.rating-delta { display: inline-block; margin-left: 3px; font-size: 13px; }
.rating-delta.up { color: var(--good); }
.rating-delta.down { color: var(--bad); }
.ov-sub.compact { margin-bottom: 10px; }
.leaderboard { width: min(100%, 390px); max-height: min(62vh, 560px); overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; margin-bottom: 11px; border-block: 1px solid rgba(255,255,255,.09); }
.leader-row { min-height: 48px; display: grid; grid-template-columns: 28px minmax(0,1fr) 48px 62px; align-items: center; gap: 7px; padding: 6px 7px; border-bottom: 1px solid rgba(255,255,255,.065); color: #dce4eb; }
.leader-row.is-you { background: linear-gradient(90deg, rgba(36,220,235,.13), rgba(36,220,235,.025)); }
.leader-rank { color: var(--gold); text-align: center; font-size: 13px; font-weight: 900; }
.leader-row div { min-width: 0; }
.leader-row strong, .leader-row small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Navnet er det eneste som star igjen under seg selv paa toplista, saa det tar
   plassen taggen hadde. 11px var satt for a gi rom til to linjer. */
.leader-row strong { font-size: 13px; }
.leader-row small { margin-top: 2px; color: var(--muted); font-size: 8px; }
.leader-row b { color: var(--gold); text-align: right; font-size: 11px; }
/* INGEN farge her. Denne regelen traff divisjonsmerket - det ER den siste
 * span-en i raden - og den er baade mer spesifikk og staar lenger ned i fila enn
 * fargene. Resultatet var at merkene i toplista sto graa mens de samme merkene
 * paa profilen hadde riktig metall. Fargen hoerer til merket, ikke til raden. */
.leader-row > span:last-child { text-align: right; font-size: 9px; font-weight: 800; }
.leader-empty { padding: 30px 12px; color: var(--muted); text-align: center; font-size: 10px; }
.utility-actions { display: flex; justify-content: center; gap: 14px; padding: 2px 0; }
.utility-actions button:active, .utility-actions a:active,
.utility-actions button.is-pressed, .utility-actions a.is-pressed { transform: translateY(1px) scale(.97); filter: brightness(1.25); }
.utility-actions button, .utility-actions a { color: #aeb9c4; font-size: 9px; font-weight: 900; text-decoration: underline; text-underline-offset: 3px; }

/* Never eats a touch. It sat over the bottom of the board with no
   pointer-events rule, so tapping a crystal there did nothing while a message
   was up. A message must not be able to block the game it is describing. */
.toast { pointer-events: none; position: fixed; z-index: 700; left: 50%; bottom: 62px; transform: translateX(-50%); padding: 10px 15px; border-radius: 7px; background: rgba(28, 34, 42,.98); border: 1px solid rgba(255,255,255,.17); font-size: 11px; font-weight: 900; white-space: nowrap; max-width: 92vw; overflow: hidden; text-overflow: ellipsis; box-shadow: 0 8px 22px rgba(0,0,0,.34); }
@keyframes bannerFloat { 0% { opacity: 0; transform: translate(-50%, -10px) scale(.92); } 15% { opacity: 1; transform: translate(-50%, 0) scale(1.01); } 76% { opacity: 1; transform: translate(-50%, 0) scale(1); } 100% { opacity: 0; transform: translate(-50%, -8px) scale(.98); } } 14% { opacity: 1; transform: translateY(0) scale(1); } 78% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-6px) scale(.99); } }
@keyframes valuePop { 0% { transform: scale(.9); filter: brightness(1.6); } 55% { transform: scale(1.09); } 100% { transform: scale(1); filter: brightness(1); } }
.value-pop { animation: valuePop 240ms cubic-bezier(.2,.8,.25,1); }
.loading-note { font-size: 10px; color: var(--muted); margin-top: 10px; }
.perf-badge { display: none; }

@media (max-height: 760px) {
  .start-overlay { padding-top: max(55px, calc(env(safe-area-inset-top) + 48px)); }
  .start-art { aspect-ratio: 16 / 6.7; }
  .start-copy { margin-top: 10px; margin-bottom: 11px; }
  .start-menu .menu-btn { min-height: 43px; padding-top: 9px; padding-bottom: 9px; }
  .header { min-height: 30px; flex-basis: 30px; padding-top: 5px; }
  .scorebar { min-height: 77px; flex-basis: 77px; padding-bottom: 5px; }
  .player-card { min-height: 68px; padding-top: 7px; }
  .life { font-size: 22px; }
  .resourcebar { min-height: 49px; flex-basis: 49px; margin-bottom: 5px; padding: 5px 9px; }
  /* Short screens are phones, which is where the instruction has least width
     and wraps to most lines. It used to shrink to 52 px here, so the panel got
     smaller exactly where it needed to be biggest and the text was cut off. It
     takes the height from the board, which simply gets a row fewer: in training
     the lesson matters more than board size. */
  .tutorial-strip { min-height: 100px; flex-basis: 100px; padding-top: 7px; padding-bottom: 7px; }
  .tutorial-strip.collapsed { min-height: 40px; flex-basis: 40px; padding-top: 4px; padding-bottom: 4px; }
  .tutorial-copy strong { font-size: 14px; }
  .tutorial-copy > span { font-size: 12.5px; -webkit-line-clamp: 4; }
  .tutorial-step { width: 48px; height: 40px; font-size: 15px; }
  .profile-setup-overlay { padding-top: max(47px, calc(env(safe-area-inset-top) + 41px)); }
  .profile-setup-art { aspect-ratio: 16 / 6.35; }
  .profile-setup-content { gap: 6px; padding-top: 10px; }
  .profile-setup-content .ov-sub { font-size: 9px; line-height: 1.34; }
  .profile-setup-content .player-name-input { height: 43px; }
  .profile-setup-content .menu-btn { min-height: 42px; padding-top: 9px; padding-bottom: 9px; }
  .bottom-bar { min-height: 36px; flex-basis: 36px; padding-top: 3px; padding-bottom: 4px; }
  .foot-btn { height: 27px; }
  .logo { font-size: 11px; }
}
@media (max-width: 360px) {
  .header { padding-left: 7px; padding-right: 7px; gap: 5px; }
  .logo { font-size: 10px; letter-spacing: .7px; }
  .room { padding-left: 7px; padding-right: 7px; }
  .live [data-connection] { display: none; }
  .scorebar { grid-template-columns: minmax(0, 1fr) 50px minmax(0, 1fr); gap: 5px; padding-left: 7px; padding-right: 7px; }
  .player-card { padding-left: 7px; padding-right: 7px; }
  .life { font-size: 21px; }
  .pname { font-size: 8px; }
  .resourcebar { margin-left: 7px; margin-right: 7px; }
  .epulse { width: 17px; }
  .profile-summary { max-width: 150px; }
  .profile-summary strong { max-width: 90px; }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}

/* The shared match start. Both players see the same number tick down on the
   server clock, so neither can act before the other has the board. */
.match-countdown{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  z-index:6;pointer-events:none;
  font-size:clamp(64px,22vw,150px);font-weight:800;line-height:1;
  color:#fff;text-shadow:0 0 28px rgba(0,0,0,.85),0 6px 18px rgba(0,0,0,.7);
  animation:countdown-beat .95s ease-out;
}
.match-countdown[hidden]{display:none}
@keyframes countdown-beat{
  from{opacity:0;transform:translate(-50%,-50%) scale(1.5)}
  35%{opacity:1}
  to{opacity:.85;transform:translate(-50%,-50%) scale(1)}
}

/* ------------------------------------------------------------------ VENTING
 * To plasser: du med ansikt og rating, motstanderen som ruller til noen kommer.
 * Ventetid uten noe aa se paa foles lengre enn den er, og koen er det ene
 * stedet en ny spiller bestemmer seg for aa bli eller gaa.
 */
.match-seats { display: flex; align-items: center; justify-content: center; gap: 10px; margin: 6px 0 14px; width: 100%; }
.match-seats .seat { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 12px 8px; border-radius: 14px; background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.09); }
.match-seats .seat b { font-size: 15px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.match-seats .seat span { font-size: 12px; color: var(--dim); }
.match-seats .seat-face { width: 76px; height: 76px; border-radius: 50%; overflow: hidden; position: relative;
  border: 2px solid rgba(255,255,255,.14); background: rgba(0,0,0,.35); }
.match-seats .seat-face .avatar { width: 100%; height: 100%; display: block; }

/* Motstanderplassen: en loype av ansikter som ruller forbi. To kopier av lista
 * etter hverandre, saa den kan gaa i ring uten et hopp der den begynner paa
 * nytt. */
.seat-rival { border-color: rgba(255,90,120,.28); }
/*
 * Vinduet hjulet gaar bak. Uten dette er et halvt ansikt i overkant bare et
 * bilde som er klippet av; med en mork kant leser det samme halve ansiktet som
 * noe som er PAA VEI FORBI. Det er den ene detaljen som skiller en rulle fra en
 * feil, og den koster ingenting: en maske, ingen ekstra tegning.
 */
.seat-rival .seat-face { border-color: rgba(255,120,140,.35);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 26%, #000 74%, transparent 100%);
  mask-image: linear-gradient(180deg, transparent 0, #000 26%, #000 74%, transparent 100%); }
/*
 * `steps()` og ikke jevn gliding. Med en kontinuerlig rulle staar det et halvt
 * ansikt over og et halvt under til enhver tid, og det leser som noe som er i
 * ustand. Med trinn vises ETT helt ansikt om gangen, slik et hjul stopper paa
 * ett symbol - og det er den bevegelsen oyet forventer.
 */
/*
 * `bottom: auto` er det som faktisk faar rullen til aa rulle.
 *
 * Med `inset: 0` ble hoyden laast til beholderen paa 76 px, og `-50%` ble da 38
 * piksler - et HALVT ansikt - for den hoppet tilbake. Den saa ut som et hjul som
 * sitter fast, fordi den aldri kom forbi det forste bildet. MAALT: hoyden er
 * 1520 px naa, og rullen vandrer 684 px i stedet for 38.
 *
 * Uten bunn blir hoyden innholdets egen: tjue ansikter a 76 px. `-50%` er da
 * noyaktig ti av dem, og fordi lista ligger der to ganger er bilde ti det samme
 * som bilde null - loopen har ingen skjot.
 *
 * KONTINUERLIG OG RASKT, ikke i trinn. Trinn ble prøvd mens hoyden var laast,
 * fordi et halvt ansikt saa ut som en feil. Naar hjulet faktisk gaar, er det
 * motsatt: en ekte spilleautomat GLIR, og et halvt ansikt i overkant og
 * underkant er nettopp det som gjor at det leser som fart. Trinn i denne
 * hastigheten blir et flimmer.
 */
.seat-reel { position: absolute; top: 0; left: 0; right: 0; bottom: auto;
  display: flex; flex-direction: column; animation: rulle 1.5s linear infinite;
  will-change: transform; }
.seat-reel .avatar { width: 76px; height: 76px; flex: 0 0 76px; }
@keyframes rulle { from { transform: translateY(0); } to { transform: translateY(-50%); } }

/* Tre prikker mellom plassene som gaar mot motstanderen: soket er paa vei ut. */
.seat-vs { display: flex; flex-direction: column; gap: 5px; flex: 0 0 auto; padding: 0 2px; }
.seat-vs i { width: 6px; height: 6px; border-radius: 50%; background: var(--gold); opacity: .25; animation: sok 1.4s ease-in-out infinite; }
.seat-vs i:nth-child(2) { animation-delay: .18s; }
.seat-vs i:nth-child(3) { animation-delay: .36s; }
@keyframes sok { 0%,100% { opacity: .2; transform: scale(.85); } 45% { opacity: 1; transform: scale(1.15); } }

/*
 * VENTEROMMET. Skjermen stod helt stille mens man ventet paa at den man hadde
 * utfordret skulle ta imot - kode, bilde og knapper, ingenting i bevegelse. En
 * stillestaaende skjerm leser som at ingenting skjer, og da tror man at
 * invitasjonen ikke kom fram.
 *
 * Samme `sok`-animasjon som prikkene mellom setene i koen, med vilje: spillet
 * skal ha EN maate aa si "vi venter paa noen" paa, ikke to som ligner.
 */
.venter { display: flex; align-items: center; justify-content: center; gap: 7px; margin: 2px 0 10px; }
.venter i { width: 6px; height: 6px; border-radius: 50%; background: var(--gold); opacity: .25;
  animation: sok 1.4s ease-in-out infinite; }
.venter i:nth-child(2) { animation-delay: .18s; }
.venter i:nth-child(3) { animation-delay: .36s; }
.venter span { font-size: 12.5px; font-weight: 700; color: var(--dim); letter-spacing: .3px; margin-left: 3px; }

/* LANDINGEN. Settes en gang naar klar-skjermen kommer rett fra koen: rullen
 * bremser opp og motstanderen faller paa plass, i stedet for aa staa der ferdig
 * i neste bilde. */
.ready-overlay.lander .ready-side:last-child .avatar { animation: land .78s cubic-bezier(.16,.9,.3,1) both; }
.ready-overlay.lander .ready-side:last-child { animation: land-kort .78s cubic-bezier(.16,.9,.3,1) both; }
@keyframes land {
  0% { transform: translateY(-46px) scale(.82); filter: blur(4px); opacity: .2; }
  62% { transform: translateY(5px) scale(1.04); filter: blur(0); opacity: 1; }
  100% { transform: translateY(0) scale(1); filter: blur(0); opacity: 1; }
}
@keyframes land-kort {
  0% { box-shadow: 0 0 0 0 rgba(255,190,80,0); }
  62% { box-shadow: 0 0 0 3px rgba(255,190,80,.55); }
  100% { box-shadow: 0 0 0 0 rgba(255,190,80,0); }
}

/* Ingen bevegelse for den som har bedt om aa slippe. Rullen staar da stille paa
 * ett ansikt, og landingen blir et rolig innslag i stedet for et fall. */
@media (prefers-reduced-motion: reduce) {
  .seat-reel, .seat-vs i { animation: none; }
  .ready-overlay.lander .ready-side:last-child,
  .ready-overlay.lander .ready-side:last-child .avatar { animation: none; }
}
