/*
 * The arcade's two chromeless pages: the screen (/play/<id>) and the phone
 * (/join/<code>). Same night, same cream, same accent as the site and the
 * same card colours as the tabletop runtime (#0b0a0e, #17151d, #f4f2f7), so
 * a card landing in the frame belongs to the page around it.
 */
:root {
  --void: #0b0a0e; --card: #17151d; --edge: #2a2732; --ink: #f4f2f7; --dim: #9d97ab; --lamp: #3ee0c8; --cream: #f4efe6;
  --font: "Bricolage Grotesque", ui-rounded, "SF Pro Display", system-ui, sans-serif;
}
@font-face { font-family: "Bricolage Grotesque"; src: url("/fonts/bricolage-grotesque.woff2") format("woff2"); font-weight: 200 800; font-display: swap; }
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { margin: 0; min-height: 100%; background: var(--void); color: var(--ink); font: 400 17px/1.45 var(--font); overscroll-behavior: none; }
h1 { margin: 0 0 .5rem; font-weight: 650; letter-spacing: -.025em; line-height: 1.05; text-wrap: balance; }
p { margin: 0 0 .6rem; }
.kicker { margin: 0 0 .5rem; font-size: .74rem; letter-spacing: .14em; text-transform: uppercase; color: var(--dim); }
.keys { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: 1rem; }
.key, .key-ghost { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: .55rem 1.2rem; border-radius: 999px; font-weight: 700; text-decoration: none; }
.key { background: var(--cream); color: #14121a; border: 0; }
.key-ghost { color: var(--ink); border: 1px solid rgba(244, 239, 230, .28); }
.key:active, .key-ghost:active { transform: scale(.975); }

/* ---------------------------------------------------------------- screen */
.screen { position: fixed; inset: 0; display: grid; }
.screen .game, .screen .mirror-picture { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: var(--void); }
.screen .mirror-picture { object-fit: cover; }
.screen .game[hidden], .screen .mirror-picture[hidden] { display: none; }
.mirror-note { position: fixed; z-index: 6; top: max(64px, calc(env(safe-area-inset-top) + 54px)); left: 50%; transform: translateX(-50%);
  margin: 0; max-width: min(88vw, 34rem); padding: .55rem .9rem; border-radius: 999px; text-align: center; font-size: .82rem;
  color: var(--cream); background: rgba(11, 10, 14, .82); border: 1px solid rgba(255,255,255,.15); pointer-events: none; }
.mirror-note[hidden] { display: none; }
/* Before the table opens: the art and the title, the state under it. Stays until the game paints. */
.opening { position: absolute; inset: 0; display: grid; place-items: center; padding: 2rem; text-align: center; }
.opening .cover { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: .38; filter: saturate(.8); }
.opening-words { position: relative; max-width: 40rem; }
.opening h1 { font-size: clamp(2.2rem, 6vw, 4.2rem); }
.opening .line { color: var(--dim); font-size: 1.1rem; }
.opening .state { color: var(--lamp); }
.screen[data-state="playing"] .opening { display: none; }
/*
 * THE DOOR, IN TWO SIZES AND ONE ELEMENT.
 *
 * It is fixed, it is above the game, and it never folds into a wrapping row
 * — the television lost this exact element that way when the room filled up,
 * and the stylesheet's comment at the time claimed the wrap protected it.
 * The tabletop stage draws its people along the bottom and its title
 * top-left, so this corner is the one a game uses least.
 *
 * `big` is the state a table opens in: nobody has joined, there is nothing
 * on the screen worth covering, and the one thing a room of strangers needs
 * is a code they can read from a sofa and a symbol a camera can find across
 * it. `corner` is every moment after the first phone sits down. The change
 * is a class on <main>, not a second element, so there is exactly one door
 * in this document and it cannot be lost by one state forgetting the other.
 */
.door { position: fixed; z-index: 10; display: flex; align-items: center; gap: 14px; padding: 10px 14px 10px 10px; border-radius: 18px;
  background: rgba(11, 10, 14, .82); border: 1px solid rgba(255, 255, 255, .12); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
.door[hidden] { display: none; }
.door-qr-box { display: grid; place-items: center; padding: 6px; border-radius: 12px; background: #fff; }
.door-qr { display: block; }
.door-words { display: grid; gap: 2px; min-width: 0; }
.door-line { margin: 0; font-size: .78rem; color: var(--dim); }
.door-what { display: none; }
.door-url { margin: 0; font-size: .92rem; }
.door-url b { font-weight: 650; }
.door-code { margin: 2px 0 0; font: 700 2rem/1 ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: .22em; color: var(--lamp); }
.door-seats { margin: 2px 0 0; font-size: .8rem; color: var(--dim); max-width: 16rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Who is in: a chip in that phone's own colour, so somebody on the far side
   of the room can see themselves arrive. One press is the whole of "am I
   in?" and a name in a list answers it worse than this does. */
.door-people { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 0; padding: 0; max-width: 22rem; }
.door-people:empty { display: none; }
.door-person { display: inline-flex; align-items: center; gap: 7px; padding: 5px 12px 5px 9px; border-radius: 999px; font-size: .9rem; font-weight: 650;
  background: rgba(255, 255, 255, .07); border: 1px solid rgba(255, 255, 255, .14); max-width: 11rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  animation: sat-down .42s cubic-bezier(.2, 1.3, .5, 1) both; }
.door-person::before { content: ''; flex: none; width: 11px; height: 11px; border-radius: 50%; background: var(--c, var(--dim)); box-shadow: 0 0 0 3px color-mix(in oklab, var(--c, #888) 28%, transparent); }
@keyframes sat-down { from { opacity: 0; transform: translateY(10px) scale(.86); } to { opacity: 1; transform: none; } }

/* ---- big: the way in IS the screen */
.screen[data-door="big"] .door { inset: 0; display: grid; place-content: center; justify-items: center; gap: 20px; grid-auto-flow: row;
  padding: 6vmin; border: 0; border-radius: 0; background: rgba(6, 6, 10, .78); }
.screen[data-door="big"] .door-words { justify-items: center; text-align: center; gap: 4px; }
.screen[data-door="big"] .door-what { display: block; margin: 0 0 .4rem; font-size: clamp(1.4rem, 4vmin, 2.4rem); font-weight: 700; letter-spacing: -.03em; line-height: 1.05; text-wrap: balance; max-width: 22ch; }
.screen[data-door="big"] .door-line { font-size: clamp(1rem, 2.4vmin, 1.3rem); color: var(--ink); }
.screen[data-door="big"] .door-url { font-size: clamp(1.1rem, 2.8vmin, 1.5rem); color: var(--dim); }
.screen[data-door="big"] .door-code { margin-top: 8px; font-size: clamp(3.4rem, 13vmin, 8rem); letter-spacing: .16em; text-indent: .16em; }
.screen[data-door="big"] .door-seats { margin-top: 10px; font-size: clamp(.95rem, 2.2vmin, 1.2rem); max-width: none; }
.screen[data-door="big"] .door-people { justify-content: center; max-width: min(90vw, 44rem); }

/* ---- corner: the game has the screen, the door keeps one fixed size */
.screen[data-door="corner"] .door { top: max(12px, env(safe-area-inset-top)); right: max(12px, env(safe-area-inset-right)); left: auto; bottom: auto; }

/* A public match has already recruited the room. Its door is an honest
   full-screen wait for player two, never a QR invitation; play.js removes the
   whole panel when the table opens so the controller owns every touch. */
body[data-public="room"] .door-qr-box,
body[data-public="room"] .door-url,
body[data-public="room"] .door-code,
body[data-public="room"] .door-seats,
body[data-public="room"] .door-people,
body[data-public="room"] .door-me,
body[data-public="room"] .door-back { display: none !important; }
/* Once matchmaking opens, this phone is a controller. The persistent install
   and studio offers otherwise sit over the controller's own player heading;
   keep the exit, but give the live controls the rest of the glass. */
body[data-public="room"] .screen[data-door="none"] .made,
body[data-public="room"] .screen[data-door="none"] .pass { display: none; }
/* A completed run has released the controls and is the first honest moment to
   offer the studio's pass. Stopping early stays a controller, not a sale. */
body[data-public="room"] .screen[data-door="none"][data-outcome="over"] .pass { display: inline-flex; }
@media (max-width: 40em) {
  .screen[data-door="corner"] .door { gap: 10px; padding: 8px 10px 8px 8px; }
  .screen[data-door="corner"] .door-qr-box { padding: 4px; }
  .screen[data-door="corner"] .door-code { font-size: 1.5rem; }
  .screen[data-door="corner"] .door-seats { max-width: 10rem; }
  .screen[data-door="corner"] .door-people { display: none; }
}

/*
 * ---- the door that was never true: a game whose manifest says phones and
 * whose build never knocked on the bridge (/arcade/web-pad.js). The code is
 * taken down and this takes its place, in the corner, quiet, out of the
 * game's way — it is a fact about the build, not an apology, and the game is
 * playing underneath it.
 */
.keyboard { position: fixed; z-index: 10; top: max(12px, env(safe-area-inset-top)); right: max(12px, env(safe-area-inset-right));
  display: grid; gap: 3px; justify-items: end; padding: 10px 14px; border-radius: 16px; text-align: right;
  background: rgba(11, 10, 14, .82); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border: 1px solid rgba(255, 255, 255, .12); }
.keyboard[hidden] { display: none; }
.keyboard-line { margin: 0; font-size: .92rem; font-weight: 650; }
.keyboard-sub { margin: 0; font-size: .78rem; color: var(--dim); }
.keyboard .key-ghost { margin-top: 6px; font-size: .8rem; padding: 6px 12px; }
/* A phone playing a game that never knocked (play.js `data-silent`): the game
   has the whole glass. The card says its line for a few seconds and goes; the
   way out becomes one small tab on the left edge (the netplay road's phone
   shell), and "Made with Homie" steps off the game's corner.
   AT THE TOP, AND TOUCH GOES THROUGH IT. It sat low in the middle, and on a
   phone that is where a game's own stick lives: Orbit Smash's "Drag here to
   move" zone was under it for its seven seconds (critic r3, legacyprobe). The
   top edge is a game's score line, which a sentence can borrow briefly; a
   thumb's zone it cannot. Only its one link takes a touch. */
.screen[data-silent="phone"] .keyboard { top: max(8px, env(safe-area-inset-top)); right: auto; bottom: auto; left: 50%;
  transform: translateX(-50%); grid-auto-flow: column; align-items: center; justify-items: center; text-align: left; width: max-content; max-width: calc(100vw - 88px);
  padding: 5px 5px 5px 13px; gap: 10px; border-radius: 999px; pointer-events: none; }
.screen[data-silent="phone"] .keyboard-line { font-size: .8rem; white-space: nowrap; }
.screen[data-silent="phone"] .keyboard-sub { display: none; }
.screen[data-silent="phone"] .keyboard .key-ghost { pointer-events: auto; margin: 0; min-height: 32px; padding: 0 12px; font-size: .74rem; white-space: nowrap; }
.screen[data-silent="phone"] .made, .screen[data-silent="phone"] .pass { display: none; }
/* ONE PILL ON A PHONE FROM THE FIRST PAINT (critic r4, legacy-and-unknown-strip.jpg).
   The old road on a phone opened with two chips over the game — "Play with
   friends" at the top and "Made with Homie" under the way out — and swapped
   them for a third at ten seconds. On a phone the made-by chip goes (it led
   to a Mac installer, from a phone); the top pill is the only thing the page
   puts on the game, first "Play with friends", then, if the game turns out
   to play on its own, the one line that says so, in the same place. */
.screen:not(.net)[data-hand="phone"] .made { display: none; }
.screen[data-silent="phone"] .way-out { top: 50%; left: 0; width: 26px; height: 58px; transform: translateY(-50%);
  border-radius: 0 14px 14px 0; border-left: 0; padding-left: env(safe-area-inset-left); }
.screen[data-silent="phone"] .way-out svg { width: 15px; height: 15px; }

/* /play/<id> for a game that is not here (arcade.mjs `missingPlayPage`): the play shell's own dark page, never plain text. */
.missing { position: fixed; inset: 0; display: grid; place-items: center; overflow: hidden; background: #0d0b14;
  padding: max(24px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right)) max(28px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left)); }
.missing-glow { position: absolute; inset: -10%; background:
  radial-gradient(45% 50% at 25% 30%, rgba(62, 224, 200, .22), transparent 70%),
  radial-gradient(50% 55% at 78% 70%, rgba(199, 146, 234, .24), transparent 70%),
  radial-gradient(35% 40% at 62% 18%, rgba(255, 209, 102, .12), transparent 70%);
  filter: blur(24px); animation: net-drift 26s ease-in-out infinite alternate; }
.missing-words { position: relative; max-width: 34rem; text-align: center; }
.missing-words h1 { font-size: clamp(2.2rem, 7vw, 4rem); font-weight: 750; letter-spacing: -.035em; }
.missing-words p:not(.kicker) { color: color-mix(in oklab, var(--cream) 74%, transparent); }
.missing-words .keys { justify-content: center; }
@media (prefers-reduced-motion: reduce) { .missing-glow { animation: none; } }

/* A game that needs a Homie: one honest paragraph. */
.needs { position: relative; min-height: 100svh; display: grid; align-content: end; padding: 2rem max(1.25rem, env(safe-area-inset-right)) max(2rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left)); }
.needs .cover { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: .45; mask-image: linear-gradient(to bottom, #000 40%, transparent); -webkit-mask-image: linear-gradient(to bottom, #000 40%, transparent); }
.needs-words { position: relative; max-width: 36rem; }
.needs h1 { font-size: clamp(2rem, 6vw, 3.6rem); }

/* ----------------------------------------------------------------- phone */
.phone { min-height: 100svh; display: grid; grid-template-rows: auto 1fr; }
/* The top 64pt belong to the page: the card's own layout starts below them (homie.js, "THE TOP 64 PT BELONG TO HOMIE"). */
.phone-top { position: fixed; top: 0; left: 0; right: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: calc(10px + env(safe-area-inset-top)) 14px 0; pointer-events: none; }
.phone-who { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 14px; border-radius: 999px; font-weight: 650; font-size: .95rem;
  background: rgba(11, 10, 14, .8); border: 1px solid rgba(255, 255, 255, .12); pointer-events: auto; max-width: 60vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.phone-who::before { content: ''; width: 10px; height: 10px; border-radius: 50%; background: var(--c, var(--dim)); flex: none; }
.phone-leave { display: inline-flex; align-items: center; min-height: 44px; padding: 0 16px; border-radius: 999px; color: var(--ink); text-decoration: none; font-size: .9rem;
  background: rgba(11, 10, 14, .8); border: 1px solid rgba(255, 255, 255, .12); pointer-events: auto; }
/* THE WAY IN, UNDER A THUMB. The block sits in the middle of the screen and
   the one key is the last thing above the safe area — a phone typing a name
   has a keyboard over the bottom third, and a Sit down key floating under a
   half-empty screen is a key somebody reaches up for. The page also wears
   the colour being chosen, from the top. */
.name, .gone { grid-row: 2; display: grid; align-content: center; gap: .4rem; min-height: 100svh;
  padding: calc(84px + env(safe-area-inset-top)) 20px calc(20px + env(safe-area-inset-bottom));
  background: radial-gradient(120% 60% at 50% 0%, color-mix(in oklab, var(--me, var(--lamp)) 22%, transparent), transparent 70%); }
.name h1, .gone h1 { font-size: clamp(2rem, 9vw, 2.8rem); }
.sit { display: grid; gap: 16px; margin-top: 1rem; }
.sit input:focus { border-bottom-color: var(--me, var(--lamp)); }
.sit input { width: 100%; padding: 18px; border: 1px solid var(--edge); border-bottom: 3px solid var(--lamp); border-radius: 16px 16px 6px 6px;
  background: #100e15; color: var(--ink); font: 500 22px/1.3 var(--font); outline: none; }
.sit input:focus { background: #141119; }
.swatches { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; }
.swatch { aspect-ratio: 1; min-height: 44px; border-radius: 50%; border: 3px solid transparent; background: var(--c); cursor: pointer; }
.swatch[aria-pressed="true"] { border-color: var(--ink); box-shadow: 0 0 0 3px var(--void), 0 0 0 5px var(--c); transform: scale(1.06); }
.swatch { transition: transform .18s cubic-bezier(.2, 1.3, .5, 1); }
.key-big { width: 100%; min-height: 64px; font-size: 1.3rem; border-radius: 20px; }

/* ------------------------------------------------------------ the typed door
   /join with no code on it (arcade.mjs, joinDoor). It borrows `.name` and
   `.sit` wholesale, because it IS the same moment as the one after it — a
   phone, one field, one key — and a second layout for the step before sitting
   down would be a different-looking door to the same room.

   The field is the code's own shape: four capitals, spaced, centred, in the
   size the television is showing them at. A person is copying a thing they
   can see across a room, and a 22px left-aligned text box is not what they
   are looking at. `text-transform` is decoration only — door.js rewrites the
   value itself, and a browser with nothing running still sends what was
   typed, which the worker uppercases before it reads it.
   --------------------------------------------------------------------- */
.door-say { margin: 0; color: var(--dim); }
.door-field { text-align: center; text-transform: uppercase; letter-spacing: .5em; text-indent: .5em; }
.door-form .door-field { font: 700 clamp(2rem, 14vw, 2.8rem)/1.2 var(--font); padding: 16px 12px; }
.door-form .door-field::placeholder { color: color-mix(in oklab, var(--dim) 45%, transparent); letter-spacing: .5em; }
/* One line, in the one colour on this page that is not calm. It sits BELOW
   the field and above the key, so the thing being corrected never moves when
   it appears — only the key does, and a key that moves down to make room for
   the reason is the right one to move. */
.door-bad { margin: -8px 0 0; min-height: 1.2em; color: #ff9d8a; font-size: .95rem; }
.door-bad[hidden] { display: none; }
.door-none { margin: 1.1rem 0 0; color: var(--dim); font-size: .95rem; }
.door-none a { color: var(--ink); }
.seat { grid-row: 2; position: relative; min-height: 100svh; }
.seat .card { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: var(--void); }
.seat .card[hidden] { display: none; }
/* The arrival. The phone's own colour, from the top, under its own name —
   the same colour that just landed as a chip on the television. */
.waiting { position: relative; display: grid; align-content: center; gap: .2rem; min-height: 100svh;
  padding: calc(84px + env(safe-area-inset-top)) 24px calc(24px + env(safe-area-inset-bottom));
  background: radial-gradient(120% 70% at 50% 0%, color-mix(in oklab, var(--me, var(--lamp)) 34%, transparent), transparent 70%); }
.waiting-seat { margin: 0; font-size: .74rem; letter-spacing: .16em; text-transform: uppercase; color: color-mix(in oklab, var(--me, var(--lamp)) 80%, var(--ink)); }
.waiting h1 { font-size: clamp(2.4rem, 12vw, 3.4rem); letter-spacing: -.03em; }
.waiting-dot { color: var(--me, var(--lamp)); }
.waiting-name { margin: .1rem 0 1rem; font-size: clamp(1.4rem, 7vw, 2rem); font-weight: 650; letter-spacing: -.02em; color: var(--me, var(--ink)); overflow-wrap: anywhere; }
.waiting p { color: var(--dim); font-size: 1.05rem; }
.waiting[hidden] { display: none; }
[hidden] { display: none !important; }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }

/* ========================================================================== */
/* THE SCREEN, WHEN THE SCREEN IS A PHONE                                     */
/* ========================================================================== */
/*
 * A controller drawn OVER a game, not beside it. There is one screen on a
 * phone and a driving game wants all of it, so the card the game composed is a
 * layer on top rather than a band underneath.
 *
 * `mix-blend-mode: screen` is what makes it a layer instead of a lid, and it
 * is chosen rather than `opacity` for a reason worth the line: opacity fades
 * the WHOLE card — the near-black it is drawn on included — so the game comes
 * through at 30% and everything looks like fog. `screen` blending is
 * `1-(1-a)(1-b)`: a black pixel in the card contributes NOTHING and the game
 * under it is untouched, while the stick, the buttons and the speed — which
 * are light on dark, because that is how a control scheme composes a card for
 * a phone — stay exactly as bright as they were drawn. The controller floats;
 * the game does not dim.
 *
 * It WAS honest about one failure mode and blind to the one that shipped. The
 * comment used to say: a card composed LIGHT blends to white, but every card
 * is dark, so this is safe. It reasoned about the card and never about the
 * GAME UNDER IT — and `screen` is 1-(1-a)(1-b), so a dark card over a BRIGHT
 * game blends to white just as completely.
 *
 * MEASURED on the live site, 2026-09-12, Kart Royale in landscape thirty
 * seconds into a race: whole-frame luminance 150/255 against Crush Boat's 68,
 * and the Drift button on the card reading mean luminance 234 with white text
 * on it — a contrast ratio of about 1.2:1. The stick, Drift, Item and Rear
 * view were all simply not there: Kart Royale was not playable on the mobile
 * web, and this line was why.
 *
 * THE FIX IS A FLOOR UNDER THE BLEND, not the end of it. A veil between the
 * game and the card gives `screen` a dark base to work against wherever the
 * game is bright, and costs a dim game almost nothing because it is already
 * dark there. The controller stays see-through — which is the whole point of
 * this layer on a phone, where the game needs the entire screen — and it stays
 * legible on a game lit like noon.
 */
.screen[data-pad="on"]::before {
  content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  background: rgb(6 6 10 / .34);
}
.screen .pad { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; z-index: 4;
  background: transparent; mix-blend-mode: screen; }
.screen .pad[hidden] { display: none; }
/* A phone is the only shape this layer is for. On a laptop the card belongs on
   a phone, and a controller blended over a game nobody is holding the laptop
   to play would be a picture of a feature rather than one. */
.screen:not([data-hand="phone"]) .pad { display: none; }

/* One key, and it is over the controller because the controller is the whole
   screen. Top centre, clear of both thumbs: a stick lives bottom-left and the
   buttons bottom-right in every scheme @homie/arcade composes. */
/* ==========================================================================
   THE WAY OUT. Top left, above everything including the controller, on every
   screen size, in every state. It is small and quiet — it is not competing
   with the game — but it is never hidden, because the states where a person
   wants it are exactly the states where everything else has gone away.
   ========================================================================== */
.way-out {
  position: fixed; z-index: 40;
  top: max(10px, env(safe-area-inset-top)); left: max(10px, env(safe-area-inset-left));
  display: inline-grid; place-items: center; width: 40px; height: 40px;
  border-radius: 999px; color: var(--cream); text-decoration: none;
  background: rgba(11, 10, 14, .62);
  border: 1px solid rgba(255, 255, 255, .14);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.way-out:active { transform: scale(.94); }
/* While the opening card is still up the game has not taken the screen and
   the page's own words carry the way back, so it would be a second one. */
.screen[data-state="opening"] .way-out { display: none; }
/* What made the game, under the way out: the only line on this road that says
   Orbit Smash once the opening card has gone (play-made-on.test.mjs). */
.made {
  position: fixed; z-index: 40;
  top: calc(max(10px, env(safe-area-inset-top)) + 48px); left: max(10px, env(safe-area-inset-left));
  display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px;
  border-radius: 999px; color: var(--cream); text-decoration: none; font-size: .78rem;
  background: rgba(11, 10, 14, .62);
  border: 1px solid rgba(255, 255, 255, .14);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.screen[data-state="opening"] .made { display: none; }

/* AND THE STUDIO'S OWN OFFER, UNDER IT — the second tenant of the same pinned
   column, never its replacement (nightwire-offer-route.test.mjs). Its own
   `top`, one row below the line above, so neither is drawn over the other at
   any width; the same opening-state rule, because the opening card already
   carries one and two in one frame is an advertisement. Quieter than the
   game and quieter than the way out: it is an offer to the one person who is
   already playing this studio's game, not a banner. */
.pass {
  position: fixed; z-index: 40;
  top: calc(max(10px, env(safe-area-inset-top)) + 88px); left: max(10px, env(safe-area-inset-left));
  display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px;
  border-radius: 999px; color: var(--cream); text-decoration: none; font-size: .78rem; font-weight: 650;
  background: rgba(11, 10, 14, .62);
  border: 1px solid rgba(62, 224, 200, .42);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.pass:active { transform: scale(.97); }
.screen[data-state="opening"] .pass { display: none; }

.solo { position: fixed; z-index: 11; top: max(10px, env(safe-area-inset-top)); left: 50%; transform: translateX(-50%);
  display: grid; justify-items: center; gap: 6px; pointer-events: none; }
.solo[hidden] { display: none; }
.solo-friends { pointer-events: auto; min-height: 40px; padding: .4rem 1.1rem; border: 0; border-radius: 999px;
  font: 650 .92rem/1 inherit; color: #14121a; background: var(--cream); box-shadow: 0 6px 20px rgba(0, 0, 0, .45); }
.solo-friends:active { transform: scale(.97); }
.solo-friends[hidden] { display: none; }
/* What is actually going out, on the host's own screen — small, and only the
   host sees it. The one person who can tell whether their game went slow
   because of the encoder is the one person holding the encoder. */
.solo-sending { margin: 0; padding: 3px 10px; border-radius: 999px; font-size: .72rem; color: var(--dim);
  background: rgba(11, 10, 14, .72); pointer-events: none; }
.solo-sending[hidden] { display: none; }

/* A sentence, never a gate: a phone locked in portrait cannot obey a
   rotate-your-device wall, and a 3D game plays in either shape. */
.rotate { position: fixed; z-index: 9; left: 50%; bottom: max(18px, env(safe-area-inset-bottom)); transform: translateX(-50%);
  padding: .5rem 1rem; border-radius: 999px; background: rgba(11, 10, 14, .82); border: 1px solid var(--edge); }
.rotate[hidden] { display: none; }
.rotate p { margin: 0; font-size: .85rem; color: var(--dim); }

/*
 * A NEW VERSION OF THIS WORLD HAS LANDED (docs/ARCADE.md, the living world).
 *
 * Top centre, not bottom: the bottom edge is where the rotate hint and, on a
 * phone, the controller's own thumb rests live, and this line has to be
 * readable by a room looking at a television from a sofa. Fixed and outside the
 * game's frame, at ONE size in every state, for the same reason the door is —
 * whatever the game draws, it cannot draw over the one sentence that explains
 * why the screen just blinked.
 *
 * `position: fixed` rather than a flow element: the page around the game is a
 * full-bleed grid and an announcement that reflowed the frame would make the
 * world jump a second time for a reason that has nothing to do with the world.
 */
.landed { position: fixed; z-index: 10; left: 50%; top: max(14px, env(safe-area-inset-top)); transform: translateX(-50%);
  margin: 0; max-width: min(92vw, 40rem); padding: .5rem 1.1rem; border-radius: 999px;
  background: rgba(11, 10, 14, .88); border: 1px solid var(--edge); color: var(--ink);
  font-size: 1rem; line-height: 1.3; text-align: center; text-wrap: balance; }
.landed[hidden] { display: none; }

/* The host's name and the way back, inside the door and only while it is big. */
.door-me, .door-back { display: none; }
.screen[data-hand="phone"][data-door="big"] .door-me { display: flex; gap: 8px; margin-top: 14px; width: min(90vw, 22rem); }
.screen[data-hand="phone"][data-door="big"] .door-me[hidden] { display: none; }
.door-me input { flex: 1; min-width: 0; padding: 10px 14px; border: 1px solid var(--edge); border-radius: 999px;
  background: #141119; color: var(--ink); font: inherit; font-size: 1rem; }
.door-me .key { min-height: 40px; padding: .4rem 1rem; font-size: .9rem; }
.screen[data-hand="phone"][data-door="big"] .door-back { display: inline-flex; margin-top: 14px; font-size: .85rem; padding: .4rem 1rem; min-height: 40px; }

/* ========================================================================== */
/* THE HOST'S PICTURE, ON A PHONE THAT JOINED                                 */
/* ========================================================================== */
/*
 * The game, from somebody else's phone, under this phone's own controller —
 * the identical arrangement to a phone playing alone, which is deliberate:
 * the two cases differ only in which device drew the frame, so they must not
 * look like two different products.
 *
 * `object-fit: contain` and not `cover`: the host may be holding their phone
 * the other way round from this one, and cropping a racing line off the edge
 * of somebody's screen to avoid two black bars is the wrong trade in a game
 * where the edge is where the corner is.
 */
.seat .stage { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; background: var(--void); z-index: 1; }
.seat .stage[hidden] { display: none; }
.phone[data-seeing="yes"] .card { background: transparent; mix-blend-mode: screen; z-index: 2; }
.phone[data-seeing="yes"] .waiting { display: none; }
.stage-note { position: fixed; z-index: 6; left: 50%; transform: translateX(-50%);
  /*
   * AT THE BOTTOM, NOT THE TOP, AND THE FRAME THAT DECIDED IT IS IN
   * ~/.homie/bench/phone-only/. Centred under the header it landed squarely on
   * the card's own heading — "Kart R____ grid" — and this page does not own
   * that document and cannot move what is under it. The bottom edge is the one
   * band a control scheme leaves alone often enough to borrow, and a sentence
   * somebody reads once is a fair thing to put in a borrowed band.
   */
  bottom: calc(max(14px, env(safe-area-inset-bottom)) + 8px); margin: 0; max-width: min(92vw, 26rem); text-align: center;
  padding: .5rem 1rem; border-radius: 16px; font-size: .9rem; line-height: 1.3;
  background: rgba(11, 10, 14, .86); border: 1px solid var(--edge); color: var(--ink); }
.stage-note[hidden] { display: none; }
/* Three states, three colours. "We could not measure it" and "we measured it
   and it was fine" must never render the same. */
.stage-note[data-kind="showing"] { border-color: color-mix(in oklab, var(--me, var(--lamp)) 60%, var(--edge)); }
.stage-note[data-kind="stalled"] { color: var(--dim); }
.stage-note[data-kind="none"] { color: var(--dim); }

/* ========================================================================== */
/* THE NETPLAY ROAD — every visitor is a player (play.js `netScreen`)          */
/* ========================================================================== */
/*
 * The first seconds are the whole of this section. The opening is the game's
 * own art, full-bleed and moving slowly, the title, and one line that says
 * what is happening; it stays until the game says it has a role, then fades
 * over a world that is already drawn. The frame is never shown black: it sits
 * at opacity 0 under the opening until then.
 */
.screen.net { background: var(--void); overflow: hidden; }
.screen.net .game { opacity: 0; transition: opacity .55s ease .05s; }
.screen.net[data-state="playing"] .game { opacity: 1; }

/* The opening's own colour is there at the first paint, before any image has arrived: never a flat frame. */
.net-opening { position: fixed; inset: 0; z-index: 20; display: grid; place-items: center; overflow: hidden;
  padding: max(24px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right)) max(28px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left));
  background:
    radial-gradient(60% 55% at 22% 30%, rgba(62, 224, 200, .28), transparent 70%),
    radial-gradient(55% 60% at 80% 72%, rgba(199, 146, 234, .3), transparent 70%),
    radial-gradient(40% 45% at 60% 18%, rgba(255, 209, 102, .18), transparent 70%), #0d0b14;
  transition: opacity .6s ease, transform .8s cubic-bezier(.2, .8, .2, 1), visibility 0s linear .8s; }
.screen.net[data-state="playing"] .net-opening { opacity: 0; transform: scale(1.04); visibility: hidden; pointer-events: none; }
/* The words leave first. With the card and the game's own boot curtain both fading over .6 s, the first
   playing frame read as two loading screens printed over the street (integration critic r1, walk A-01). */
.net-card { transition: opacity .6s ease; }
.screen.net[data-state="playing"] .net-card { opacity: 0; transition: opacity .16s ease; }
.net-backdrop { position: absolute; inset: -8%; width: 116%; height: 116%; object-fit: cover;
  filter: blur(28px) saturate(1.3) brightness(.52); transform: scale(1.06); animation: net-drift 26s ease-in-out infinite alternate; }
.net-backdrop-none { background:
  radial-gradient(60% 55% at 22% 30%, color-mix(in oklab, var(--lamp) 55%, transparent), transparent 70%),
  radial-gradient(55% 60% at 80% 72%, rgba(199, 146, 234, .55), transparent 70%),
  radial-gradient(40% 45% at 60% 18%, rgba(255, 209, 102, .35), transparent 70%), #0d0b14; filter: blur(18px) saturate(1.2); }
@keyframes net-drift { from { transform: scale(1.06) translate3d(-1.5%, -1%, 0); } to { transform: scale(1.14) translate3d(1.5%, 1.2%, 0); } }
.net-veil { position: absolute; inset: 0; background:
  radial-gradient(120% 90% at 50% 45%, transparent 30%, rgba(6, 5, 10, .72) 100%),
  linear-gradient(to bottom, rgba(6, 5, 10, .25), rgba(6, 5, 10, .55)); }
.net-card { position: relative; display: grid; gap: clamp(18px, 3.2vw, 44px); align-items: center; width: min(100%, 60rem);
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); animation: net-rise .7s cubic-bezier(.2, .9, .25, 1) both; }
/* Motion only: the words are readable in the very first frame, never faded up from nothing. */
.net-card:not(:has(.net-poster)) { grid-template-columns: minmax(0, 40rem); justify-content: center; text-align: center; }
@keyframes net-rise { from { transform: translateY(12px); } to { transform: none; } }
.net-poster { margin: 0; position: relative; border-radius: 20px; overflow: hidden; aspect-ratio: 16 / 9;
  background: linear-gradient(135deg, rgba(62, 224, 200, .22), rgba(199, 146, 234, .22) 60%, rgba(255, 209, 102, .16));
  box-shadow: 0 30px 80px -20px rgba(0, 0, 0, .8), 0 0 0 1px rgba(255, 255, 255, .1); animation: net-float 7s ease-in-out infinite alternate; }
.net-poster .cover { position: relative; display: block; width: 100%; height: 100%; object-fit: cover; }
/* The cover's tiny inline twin (arcade.mjs `netPlayPage`, cover-lqip.mjs), blurred, from the first paint;
   the real picture fades in over it only once play.js has seen it fully decoded — never half-arrived. */
.net-lqip { background-size: cover; background-position: center; }
.net-poster-lqip { position: absolute; inset: -12%; background-size: cover; background-position: center; filter: blur(14px) saturate(1.15); }
.screen.net img[data-cover] { opacity: 0; transition: opacity .6s ease; }
.screen.net img[data-cover].is-ready { opacity: 1; }
.net-poster::after { content: ''; position: absolute; inset: 0; background: linear-gradient(115deg, transparent 35%, rgba(255, 255, 255, .16) 48%, transparent 60%);
  transform: translateX(-100%); animation: net-shine 3.6s ease-in-out 1s infinite; }
@keyframes net-float { from { transform: translateY(0) rotate(-.4deg); } to { transform: translateY(-6px) rotate(.4deg); } }
@keyframes net-shine { 0%, 55% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
.net-words { min-width: 0; }
.net-words .kicker { color: color-mix(in oklab, var(--cream) 70%, transparent); }
.net-words h1 { font-size: clamp(2.3rem, 6.4vw, 4.8rem); font-weight: 750; letter-spacing: -.035em; text-shadow: 0 4px 30px rgba(0, 0, 0, .5); }
.net-words .line { color: color-mix(in oklab, var(--cream) 78%, transparent); font-size: clamp(1rem, 1.6vw, 1.15rem); max-width: 34rem; }
.net-card:not(:has(.net-poster)) .net-words .line { margin-inline: auto; }
.net-state { display: inline-flex; align-items: center; gap: 10px; margin: 1.1rem 0 .35rem; padding: .55rem 1.05rem .55rem .85rem; border-radius: 999px;
  font-weight: 700; font-size: clamp(1rem, 1.7vw, 1.12rem); color: var(--ink);
  background: rgba(11, 10, 14, .55); border: 1px solid rgba(255, 255, 255, .16); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }
.net-pulse { width: 10px; height: 10px; flex: none; border-radius: 50%; background: var(--lamp);
  box-shadow: 0 0 0 0 color-mix(in oklab, var(--lamp) 70%, transparent); animation: net-pulse 1.4s ease-out infinite; }
@keyframes net-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--lamp) 65%, transparent); } 100% { box-shadow: 0 0 0 14px transparent; } }
.net-sub { margin: 0; font-size: .86rem; color: color-mix(in oklab, var(--cream) 62%, transparent); }
/* THE BAR IS THE REAL LOAD (play.js `paintLoad`): how much of the game has arrived, measured in the
   game's own frame, then the room, then the first frame. A light still runs along it, so a bar
   waiting on one big file never looks stopped. */
.net-bar { --p: .03; position: relative; height: 4px; width: min(100%, 18rem); margin-top: 1rem; border-radius: 4px; overflow: hidden; background: rgba(255, 255, 255, .1); }
.net-card:not(:has(.net-poster)) .net-bar { margin-inline: auto; }
.net-bar i, .net-bar b { position: absolute; inset: 0; width: 100%; border-radius: 4px; transform-origin: 0 50%;
  background: linear-gradient(90deg, color-mix(in oklab, var(--lamp) 45%, transparent), var(--lamp)); }
/* Scaled, not resized: a transform runs on the compositor, so the bar keeps moving while the game's
   own thread (the same renderer thread as this page's) is busy parsing and building its world. */
.net-bar i { transform: scaleX(var(--p)); transition: transform .45s cubic-bezier(.2, .8, .2, 1); }
/* The guess between the frame's reports (play.js `guessBar`): one long transition to where this link
   should have brought the game by then, set once per phase; the real figure (`i`) passes it when it can. */
.net-bar b { transform: scaleX(var(--g, 0)); transition: transform var(--gms, 0ms) cubic-bezier(.3, .1, .6, 1); opacity: .8; }
.net-bar::after { content: ''; position: absolute; inset: 0 auto 0 0; width: 34%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .22), transparent);
  animation: net-bar 1.5s cubic-bezier(.45, 0, .55, 1) infinite; }
@keyframes net-bar { from { transform: translateX(-100%); } to { transform: translateX(300%); } }
.net-load { margin: .45rem 0 0; min-height: 1.2em; font-size: .78rem; font-variant-numeric: tabular-nums; color: color-mix(in oklab, var(--cream) 55%, transparent); }
.net-card:not(:has(.net-poster)) .net-load { text-align: center; }
.screen.net[data-net="failed"] .net-load, .screen.net[data-net="away"] .net-load { visibility: hidden; }
.screen.net[data-net="failed"] .net-pulse { background: #ef6f6c; animation: none; }
.screen.net[data-net="failed"] .net-bar { visibility: hidden; }
/* Away (play.js `goAway`): the opening is back over an unloaded game, calm, with a key to play again.
   It is visible at once (only its opacity fades), so its key can take the keyboard. */
.screen.net[data-net="away"] .net-opening { transition: opacity .6s ease, transform .8s cubic-bezier(.2, .8, .2, 1), visibility 0s; }
.screen.net[data-net="away"] .net-pulse { background: var(--muted, #9d97ab); animation: none; }
.screen.net[data-net="away"] .net-bar { visibility: hidden; }
.screen.net[data-net="away"] .game { visibility: hidden; }
/* A resting big screen keeps its QR up, beside the opening, so a phone can still scan in and wake it. */
.screen.net[data-net="away"][data-shell="rail"] .net-screen { z-index: 25; }
/* A phone held upright: the poster over the words, everything centred. */
@media (max-aspect-ratio: 1/1) {
  .net-card, .net-card:not(:has(.net-poster)) { grid-template-columns: minmax(0, 1fr); text-align: center; justify-items: center; gap: 22px; }
  .net-poster { width: min(100%, 26rem); }
  .net-words .line { margin-inline: auto; }
  .net-bar { margin-inline: auto; }
  .net-load { text-align: center; }
}
/* A phone on its side: less height, so a smaller title and poster. */
@media (orientation: landscape) and (max-height: 540px) {
  .net-card { gap: 20px; }
  .net-words h1 { font-size: clamp(1.8rem, 5.6vh + 1rem, 2.8rem); }
  .net-words .line { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .net-backdrop, .net-poster, .net-poster::after, .net-bar::after, .net-pulse, .net-card { animation: none; }
  .net-bar b { transition: none; }
}

.net-fail-keys { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: 1.1rem; }
.net-fail-keys[hidden] { display: none; }
.net-card:not(:has(.net-poster)) .net-fail-keys { justify-content: center; }
@media (max-aspect-ratio: 1/1) { .net-fail-keys { justify-content: center; } }

/* ==========================================================================
   THE SHELL AROUND THE GAME, AND WHY NONE OF IT IS OVER THE GAME.
   A game's HUD owns its corners and this page cannot know where they are:
   the first version put a dock top-left and a QR card bottom-right, and on
   Orbit Smash they sat on the scoreboard and on a player's bar (critic r1).
   So the page's keys live outside the game's rectangle, in one of three
   shells play.js chooses (`data-shell`):
     bar   a computer: a slim bar ABOVE the frame; the frame is shortened by it.
     rail  the big screen: the QR, the count and the people in a rail BESIDE
           the frame (below it on an upright screen); the frame is narrowed.
     tab   a phone, where every pixel is the game's: one small tab on the left
           edge, which opens the keys only when it is touched.
   ========================================================================== */
.screen.net { --bar-h: 44px; --rail-w: clamp(250px, 23vw, 380px); --rail-h: clamp(170px, 24vh, 260px); }
.screen.net[data-shell="bar"] .game { top: var(--bar-h); height: calc(100% - var(--bar-h)); }
.screen.net[data-shell="rail"] .game { width: calc(100% - var(--rail-w)); }
@media (max-aspect-ratio: 1/1) {
  .screen.net[data-shell="rail"] .game { width: 100%; height: calc(100% - var(--rail-h)); }
}

/* The dock: the same keys in every shell, laid out by the shell. */
.net-dock { position: fixed; z-index: 40; }
.net-keys { display: flex; align-items: center; gap: 8px; min-width: 0; }
.net-dock .way-out, .net-dock .made, .net-dock .pass { position: static; }
.net-dock .way-out { width: 34px; height: 34px; flex: none; background: rgba(255, 255, 255, .06); }
.net-title { margin: 0; font-weight: 700; font-size: .95rem; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; max-width: 16rem; }
.net-live { display: inline-flex; align-items: center; gap: 7px; margin: 0; font-size: .8rem; color: var(--dim); white-space: nowrap; }
.net-live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--lamp); box-shadow: 0 0 0 3px color-mix(in oklab, var(--lamp) 22%, transparent); flex: none; }
.screen.net[data-link="solo"] .net-live-dot, .screen.net[data-link="lost"] .net-live-dot { background: #ffb454; box-shadow: 0 0 0 3px rgba(255, 180, 84, .22); }
.net-status { display: inline-flex; align-items: center; gap: 8px; margin: 0; padding: 3px 4px 3px 11px; border-radius: 999px; font-size: .8rem; font-weight: 650;
  color: #ffd9a0; background: rgba(255, 180, 84, .12); border: 1px solid rgba(255, 180, 84, .35); white-space: nowrap; }
.net-status[hidden] { display: none; }
.net-status-retry { font: inherit; font-size: .78rem; font-weight: 700; cursor: pointer; border: 0; border-radius: 999px; padding: 4px 10px; color: #14121a; background: #ffd9a0; }
.net-gap { flex: 1 1 auto; }
.net-dock .made, .net-big, .net-invite, .net-dock .pass { min-height: 32px; padding: 0 12px; font-size: .78rem; background: rgba(255, 255, 255, .06); white-space: nowrap; }
.net-invite { font: inherit; font-size: .78rem; cursor: pointer; color: var(--ink); }
.net-big[hidden], .net-invite[hidden] { display: none; }
.net-tab { display: none; }
.screen.net[data-state="opening"] .net-dock { opacity: 0; pointer-events: none; }
.net-dock { transition: opacity .45s ease .15s; }

/* bar: above the game, the whole width. */
.screen.net[data-shell="bar"] .net-dock { top: 0; left: 0; right: 0; height: var(--bar-h); display: flex; align-items: center;
  padding: 0 max(10px, env(safe-area-inset-right)) 0 max(10px, env(safe-area-inset-left));
  background: linear-gradient(to bottom, #121019, #0d0b12); border-bottom: 1px solid rgba(255, 255, 255, .08); }
.screen.net[data-shell="bar"] .net-keys { flex: 1; }
.screen.net[data-shell="bar"] .net-dock .way-out { width: 30px; height: 30px; }

/* rail: beside the game (below it when the screen stands upright). */
.screen.net[data-shell="rail"] .net-dock { right: 0; bottom: 0; width: var(--rail-w); padding: 14px 18px max(14px, env(safe-area-inset-bottom)); }
.screen.net[data-shell="rail"] .net-keys { flex-wrap: wrap; }
.screen.net[data-shell="rail"] .net-title, .screen.net[data-shell="rail"] .net-dock .made, .screen.net[data-shell="rail"] .net-gap { display: none; }
.screen.net[data-shell="rail"] .net-live { order: 3; width: 100%; }
.screen.net[data-shell="rail"] .net-screen { position: fixed; top: 0; right: 0; bottom: 0; width: var(--rail-w); z-index: 12;
  display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start; gap: 18px; padding: 26px 22px 110px;
  border-radius: 0; border: 0; border-left: 1px solid rgba(255, 255, 255, .08); box-shadow: none; transform: none;
  background: radial-gradient(120% 60% at 50% 0%, rgba(62, 224, 200, .12), transparent 70%), linear-gradient(to bottom, #121019, #0b0a0e); }
.screen.net[data-shell="rail"] .net-screen .door-qr-box { align-self: center; padding: 10px; border-radius: 16px; }
.screen.net[data-shell="rail"] .net-screen-line { font-size: clamp(1.3rem, 2vw, 1.9rem); }
.screen.net[data-shell="rail"] .net-screen .door-people { max-width: none; }
@media (max-aspect-ratio: 1/1) {
  .screen.net[data-shell="rail"] .net-screen { top: auto; left: 0; width: auto; height: var(--rail-h); flex-direction: row; align-items: center; padding: 16px 20px 16px;
    border-left: 0; border-top: 1px solid rgba(255, 255, 255, .08); }
  .screen.net[data-shell="rail"] .net-dock { width: auto; left: auto; top: auto; bottom: 0; right: 0; }
}

/* tab: one grip on the left edge of a phone; the keys only when it is touched. */
.screen.net[data-shell="tab"] .net-dock { left: 0; top: 42%; transform: translateY(-50%); display: flex; align-items: center; }
.screen.net[data-shell="tab"] .net-tab { display: grid; place-items: center; width: 22px; height: 58px; padding: 0 0 0 max(0px, env(safe-area-inset-left)); border: 0; cursor: pointer;
  border-radius: 0 14px 14px 0; background: rgba(11, 10, 14, .55); border: 1px solid rgba(255, 255, 255, .14); border-left: 0;
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); color: var(--cream); }
.net-tab-grip { width: 4px; height: 22px; border-radius: 4px; background: rgba(244, 239, 230, .55);
  box-shadow: 0 0 0 0 rgba(62, 224, 200, 0); }
.screen.net[data-shell="tab"][data-link="solo"] .net-tab-grip, .screen.net[data-shell="tab"][data-link="lost"] .net-tab-grip { background: #ffb454; }
.screen.net[data-state="playing"][data-shell="tab"] .net-tab-grip { animation: net-tab-hint 1.6s ease-in-out .6s 2; }
@keyframes net-tab-hint { 50% { box-shadow: 0 0 0 6px rgba(62, 224, 200, .35); } }
.screen.net[data-shell="tab"] .net-keys { flex-direction: column; align-items: stretch; gap: 8px; margin-left: 8px; padding: 12px; width: min(76vw, 17rem);
  border-radius: 18px; background: rgba(11, 10, 14, .94); border: 1px solid rgba(255, 255, 255, .14); box-shadow: 0 24px 60px -18px rgba(0, 0, 0, .8);
  transition: opacity .25s ease, transform .25s ease; }
.screen.net[data-shell="tab"] .net-dock[data-dock="closed"] .net-keys { opacity: 0; transform: translateX(-12px); pointer-events: none; visibility: hidden; }
/* THE CLOSED DOCK IS ITS TAB AND NOTHING ELSE. The nav box is as big as the
   keys it holds (about 320x220 on a phone), and hiding the keys did not hide
   the box: every touch that landed in it went to the page and never reached
   the game — 21-28% of a phone's glass, right where Orbit Smash's fighters
   stand (critic r3, deadzone.mjs). So the box takes no touch at all; only the
   tab does, and the keys do only while they are open. Proved by hit-testing a
   10 px grid at four phone sizes (tools/studio-site-checks.mjs `touch`). */
.screen.net[data-shell="tab"] .net-dock { pointer-events: none; }
.screen.net:not([data-state="opening"])[data-shell="tab"] .net-tab { pointer-events: auto; }
.screen.net:not([data-state="opening"])[data-shell="tab"] .net-dock[data-dock="open"] .net-keys { pointer-events: auto; }
.screen.net[data-shell="tab"] .net-keys .way-out { width: auto; height: 38px; border-radius: 999px; justify-content: start; padding: 0 12px; display: inline-flex; gap: 8px; }
.screen.net[data-shell="tab"] .net-keys .way-out::after { content: 'Home'; font-size: .85rem; font-weight: 650; }
.screen.net[data-shell="tab"] .net-gap { display: none; }
.screen.net[data-shell="tab"] .net-title { max-width: none; }
.screen.net[data-shell="tab"] .net-status { white-space: normal; border-radius: 14px; padding: 8px 8px 8px 12px; }
.screen.net[data-shell="tab"] .net-dock .made, .screen.net[data-shell="tab"] .net-invite, .screen.net[data-shell="tab"] .net-dock .pass { min-height: 40px; justify-content: center; font-size: .85rem; }
.screen.net[data-shell="tab"] .net-big { display: none; }

/* The Invite card (a player's own QR, on purpose): under the bar on a computer, the middle of a phone. */
.net-screen { position: fixed; z-index: 42; display: flex; align-items: center; gap: 16px; padding: 14px 18px 14px 14px; border-radius: 20px;
  background: rgba(11, 10, 14, .94); border: 1px solid rgba(255, 255, 255, .14); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, .7); }
.net-screen[hidden] { display: none; }
.screen.net[data-shell="bar"] .net-screen { top: calc(var(--bar-h) + 10px); right: max(12px, env(safe-area-inset-right)); }
.screen.net[data-shell="tab"] .net-screen { left: 50%; top: 50%; transform: translate(-50%, -50%); flex-direction: column; text-align: center; width: min(88vw, 20rem); }
.screen.net[data-shell="tab"] .net-screen .door-people { justify-content: center; }
.net-screen-words { display: grid; gap: 2px; min-width: 0; }
.net-screen-line { margin: 0; font-weight: 750; font-size: 1.25rem; letter-spacing: -.01em; }
.net-screen-what { margin: 0; font-size: .85rem; color: var(--dim); }
.net-screen-count { margin: 4px 0 0; font-size: .92rem; color: var(--lamp); font-weight: 650; }

/* Notes: a sentence for a few seconds, over the top of the GAME's area (below the bar, left of the rail). */
.net-note { position: fixed; z-index: 30; left: 50%; top: max(14px, env(safe-area-inset-top)); transform: translateX(-50%);
  margin: 0; width: max-content; max-width: min(86vw, 30rem); padding: .5rem 1rem; border-radius: 14px; text-align: center; font-size: .9rem; line-height: 1.3;
  background: rgba(11, 10, 14, .9); border: 1px solid rgba(255, 255, 255, .16); color: var(--ink); pointer-events: none;
  box-shadow: 0 12px 30px -12px rgba(0, 0, 0, .7); animation: net-note-in .3s cubic-bezier(.2, .9, .25, 1) both; }
@keyframes net-note-in { from { opacity: 0; transform: translate(-50%, -6px); } to { opacity: 1; transform: translate(-50%, 0); } }
.screen.net[data-shell="tab"] .net-note { font-size: .8rem; padding: .42rem .8rem; }
.screen.net[data-link="solo"] .net-note, .screen.net[data-link="lost"] .net-note { border-color: rgba(255, 180, 84, .45); color: #ffe2b8; }
.net-note[hidden] { display: none; }
.screen.net[data-shell="bar"] .net-note { top: calc(var(--bar-h) + 12px); }
.screen.net[data-shell="rail"] .net-note { left: calc((100% - var(--rail-w)) / 2); }
.net-debug { position: fixed; z-index: 45; left: 0; right: 0; bottom: 0; display: flex; flex-wrap: wrap; gap: 3px 12px; padding: 4px 10px;
  padding-bottom: max(4px, env(safe-area-inset-bottom)); font: 11px/1.3 ui-monospace, "SF Mono", Menlo, monospace; color: #cfe;
  background: rgba(0, 0, 0, .62); pointer-events: none; }
.net-debug[hidden] { display: none; }
.net-debug b { color: var(--lamp); font-weight: 600; }
@media (prefers-reduced-motion: reduce) { .net-tab-grip { animation: none !important; } }
