/* ══════════════════════════════════════════════════════════════════════
   friendslop.fun  ·  core

   This file owns STRUCTURE — what is where, how it stacks, how it behaves
   on a phone — and nothing else. Every colour, font, radius, shadow and
   easing comes from a token, and a theme is a stylesheet that sets those
   tokens (plus a handful of lines of its own personality).

   The values in :root below are the fallback set, so the site is still
   whole if a theme stylesheet 404s. Theme files are loaded AFTER this one
   and scope their tokens to :root[data-theme="…"], which outranks :root
   regardless of order — see index.html for why the link goes where it does.

   Adding a rule here means adding it to ten themes at once. Before you
   write a colour, check there is a token for it.
   ══════════════════════════════════════════════════════════════════════ */

:root{
  color-scheme: light;

  /* ── surfaces & ink ───────────────────────────────────────────────── */
  --bg:#F7F1E6;                 /* the page */
  --surface:#FFFBF3;            /* cards, bars, anything raised off the page */
  --surface-2:#FDF4E5;          /* hover / a second step up */
  --surface-3:#EFE7D8;          /* sunken: inputs, wells, the code field */
  --ink:#2B2118;                /* body and headings */
  --ink-2:#5C4B3B;              /* secondary text */
  --ink-3:#77644F;              /* labels, meta — still clears 4.5:1 on --surface-3 */
  --line:#DCCFBA;               /* borders you are meant to see */
  --line-2:#EBE1CF;             /* borders you are not */
  --accent:#A85A28;             /* the one colour that means "do this" */
  --accent-ink:#FFF9F0;         /* text on top of --accent — 4.8:1 against it */
  --accent-soft:#F3DCC7;        /* a wash of the accent */
  --good:#3E7D5A;
  --bad:#B4462F;
  --warn:#9A6A12;

  /* ── type ─────────────────────────────────────────────────────────── */
  --f-display:ui-rounded,"SF Pro Rounded","Hiragino Maru Gothic ProN",
              "Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;
  --f-ui:ui-rounded,"SF Pro Rounded","Segoe UI",system-ui,-apple-system,sans-serif;
  --f-mono:ui-monospace,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
  --f-accent:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;

  --display-w:800;              /* display weight */
  --display-tr:-.02em;          /* display tracking */
  --display-case:none;          /* none | uppercase | lowercase */
  --display-lh:1.02;

  --em-family:var(--f-accent);  /* the highlighted half of a headline */
  --em-style:italic;
  --em-w:600;
  --em-color:var(--accent);

  --label-case:uppercase;       /* the tiny all-caps labels */
  --label-tr:.13em;
  --label-w:700;
  --label-size:9.5px;

  --body:14px;
  --body-lh:1.6;

  /* ── shape & depth ────────────────────────────────────────────────── */
  --r-1:8px;                    /* small: chips, tags */
  --r-2:12px;                   /* medium: rows, inputs */
  --r-3:18px;                   /* large: cards */
  --r-pill:999px;
  --bw:1px;                     /* the standard border width */
  --sh-1:0 1px 2px rgba(64,42,20,.05), 0 6px 18px -12px rgba(64,42,20,.30);
  --sh-2:0 2px 6px rgba(64,42,20,.07), 0 22px 48px -26px rgba(64,42,20,.44);
  --sh-in:inset 0 1px 2px rgba(64,42,20,.10);

  --btn-h:40px;
  --btn-r:var(--r-pill);
  --btn-pad:0 18px;
  --btn-size:11px;
  --btn-w:700;
  --btn-case:uppercase;
  --btn-tr:.10em;
  --btn-sh:var(--sh-1);
  --btn-lift:-1px;              /* how far a button rises on hover */
  --btn-press:0px;

  --focus:var(--accent);
  --focus-w:2px;
  --focus-off:2px;
  --scrim:rgba(0,0,0,.46);      /* behind the settings sheet, dark on every theme */

  /* ── motion ───────────────────────────────────────────────────────── */
  --ease:cubic-bezier(.22,1,.36,1);
  --dur:.22s;
  --dur-slow:.34s;

  /* ── the game surface ─────────────────────────────────────────────── */
  --board-face:#F2E9D8;         /* the punched plate */
  --board-hole:#2A2018;         /* what you see through the holes */
  --board-rim:rgba(0,0,0,.34);
  --bev-a:rgba(120,92,52,.34);  /* bevel gradient, top … */
  --bev-b:rgba(255,252,244,.55);/* … to bottom */
  --gloss:#FFFFFF;              /* highlight on a disc */
  --gloss-o:.40;
  --shade:#3A2510;              /* the disc's own shadow */
  --shade-o:.28;
  --disc-edge:rgba(0,0,0,.28);
  --emblem-d:rgba(0,0,0,.34);   /* the embossed emblem, pressed in … */
  --emblem-l:rgba(255,255,255,.72); /* … and its highlight */
  --win-line:#FFFFFF;

  /* A counter is built from ONE colour — the one its player chose, which
     arrives inline as --seat. These two say how far the moulded step and the
     raised centre are mixed away from it: 100% is flat, lower is more relief.
     A theme whose discs are printed dots rather than plastic sets both to
     100% instead of trying to repaint the parts.

     There is deliberately no --p1..--p4 any more. Player colours come from
     the palette in shared/identity.js, which is searched for the set that
     stays furthest apart under deuteranopia, protanopia and tritanopia — a
     theme re-tinting them by eye undid exactly the property they were picked
     for, and could not know which of the nine a given player had chosen. */
  --disc-step:74%;
  --disc-boss:87%;

  /* ── layout ───────────────────────────────────────────────────────── */
  --shell:900px;
  /* The lobby column's width: two cards at their 360px cap plus the gap.
     The row above and the preview below both read this, so they cannot drift
     apart into two different columns. Raised when a third card appears. */
  --row-w:733px;
  --strip:26px;                 /* the drop-arrow gutters around the board */
  --gap:9px;
}

/* ══ reset ════════════════════════════════════════════════════════════ */
*,*::before,*::after{box-sizing:border-box}
html,body{height:100%}
/* The clip lives on <html>, not on <body>. On <body> it makes the body its own
   scroll container, and a position:sticky header inside it then sticks to a box
   that never scrolls — which looks exactly like sticky being ignored. */
html{overflow-x:hidden}
body{
  margin:0;
  background:var(--bg);
  color:var(--ink);
  font-family:var(--f-ui);
  font-size:var(--body);
  line-height:var(--body-lh);
  -webkit-font-smoothing:antialiased;
  overscroll-behavior-y:none;
}
[hidden]{display:none!important}
:where(button,input){font:inherit;color:inherit}

/* ── touch ────────────────────────────────────────────────────────────
   This is a board game, not a document. Long-pressing a drop arrow to see
   where the disc would land was selecting whatever text happened to be
   nearby and raising the iOS Copy / Look Up callout over the board — and
   that callout cannot be suppressed any other way, only by taking the
   selection away. So selection is off by default and handed BACK to the
   few places that are genuinely text: the room code, which people read out
   and paste, and every field you type into.

   `touch-action:manipulation` removes double-tap-to-zoom, which is the
   other thing that fires while you are tapping arrows quickly. Pinch zoom
   is deliberately left alone — it is how somebody magnifies a board they
   cannot see, and taking it away is a real accessibility loss to fix an
   annoyance. */
#app,.sheet,.pk-root{
  -webkit-user-select:none;user-select:none;
  -webkit-touch-callout:none;
  -webkit-tap-highlight-color:transparent;
  touch-action:manipulation;
}
.code,input,textarea,[contenteditable]{
  -webkit-user-select:text;user-select:text;-webkit-touch-callout:default;
}

.sr{position:fixed;top:0;left:0;margin:0;width:1px;height:1px;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap}

/* Two empty layers a theme paints its atmosphere into: .bg sits behind the
   page, .fx in front of it. Core leaves both blank — no theme should have to
   undo somebody else's texture. */
.bg{position:fixed;inset:0;z-index:0;pointer-events:none}
.fx{position:fixed;inset:0;z-index:60;pointer-events:none}

#app{
  position:relative;z-index:1;min-height:100dvh;
  display:flex;flex-direction:column;align-items:center;
  width:100%;
  /* No top padding: the bar carries its own, so it can sit flush at top:0 when
     it sticks instead of leaving a gap for content to show through. */
  padding:0 16px calc(14px + env(safe-area-inset-bottom));
}

/* ══ top bar ══════════════════════════════════════════════════════════
   Follows you down the page. At rest it is invisible chrome sitting over the
   theme's own atmosphere; once you have scrolled it condenses and earns a
   backdrop, so it stays a thin strip rather than a slab. */
.topbar{
  position:sticky;top:0;z-index:40;
  width:100%;max-width:var(--shell);flex:none;
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:10px 0 9px;
}
/* Full-bleed, because the bar itself is capped at --shell and page content
   would otherwise slide past in the margins either side of it. */
.topbar::before{
  content:"";position:absolute;z-index:-1;pointer-events:none;
  top:0;bottom:0;left:50%;width:100vw;transform:translateX(-50%);
  background:color-mix(in srgb,var(--bg) 88%,transparent);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  border-bottom:var(--bw) solid transparent;
  opacity:0;transition:opacity var(--dur) var(--ease),border-color var(--dur);
}
/* Only the backdrop reacts to scrolling now. The bar used to shrink its own
   padding and the mark at the same moment: three properties stepping at once,
   two of which the browser cannot tween, so it read as a jump rather than as a
   condense. The bar is simply the smaller size all the time, and what changes
   on scroll is whether it has something behind it — which is a fade, and fades
   do not jump. */
html[data-scrolled] .topbar::before{opacity:1;border-bottom-color:var(--line-2)}
.brand{
  display:inline-flex;align-items:center;gap:9px;min-width:0;
  text-decoration:none;color:var(--ink);
  transition:opacity var(--dur) var(--ease),transform var(--dur) var(--ease);
}
.brand:hover{opacity:.82}
.brand:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:3px;border-radius:var(--r-1)}
.mark{display:block;flex:none;color:var(--accent)}
.mark svg{display:block;width:28px;height:28px}
/* The mark is one F plus itself again behind, offset down-right. The shadow is
   mixed from the same colour rather than being its own token, so a theme that
   sets --accent gets a coherent pair for nothing; --mark-2 is there for the
   themes where a darker mix would sink into the background. */
.mark .ff{fill:currentColor}
.mark .fb{fill:var(--mark-2,color-mix(in srgb,currentColor 55%,#000))}
/* Never truncated. An ellipsis through the site's own name reads as a
   rendering fault, so below the width where it fits whole the mark stands on
   its own — which is what a mark is for. */
.wordmark{
  font-family:var(--f-display);font-weight:var(--display-w);
  font-size:clamp(16px,4.2vw,20px);letter-spacing:var(--display-tr);
  text-transform:var(--display-case);line-height:1;
  white-space:nowrap;flex:none;
}
@media (max-width:359px){ .wordmark{display:none} }
.wordmark i{font-style:normal;color:var(--ink-3);font-weight:inherit}

.topright{display:flex;align-items:center;gap:8px;min-width:0}
.conn{
  display:inline-flex;align-items:center;gap:6px;flex:none;
  font-size:9px;font-weight:var(--label-w);letter-spacing:var(--label-tr);
  text-transform:var(--label-case);color:var(--ink-3);
}
.conn::before{content:"";width:6px;height:6px;border-radius:var(--r-pill);
  background:currentColor;box-shadow:0 0 8px currentColor}
.conn[data-s="live"]{color:var(--good)}
.conn[data-s="reconnecting"]{color:var(--warn);animation:blink 1.1s ease-in-out infinite}
@keyframes blink{50%{opacity:.34}}

/* Who you are, and the way into Settings — one control instead of a bordered
   pill wrapped round a text field standing next to a bordered button. It says
   more in less space, and it says it even when the name is too long to have
   fitted in the field it replaces. */
.whoami{
  appearance:none;cursor:pointer;flex:none;min-width:0;
  display:inline-flex;align-items:center;gap:9px;
  height:36px;padding:0 8px 0 14px;
  border:var(--bw) solid var(--line);border-radius:var(--r-pill);
  background:var(--surface);color:var(--ink);
  font-family:var(--f-ui);font-size:13px;font-weight:600;line-height:1;
  transition:color var(--dur),background var(--dur),border-color var(--dur);
  -webkit-tap-highlight-color:transparent;
}
.whoami .menm{max-width:min(44vw,220px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* No name yet: the label becomes the invitation to set one, which is the only
   moment this button has anything to ask for. */
.whoami.unnamed .menm{color:var(--ink-3);font-weight:500}
.whoami svg{width:18px;height:18px;flex:none;display:block;fill:currentColor;color:var(--ink-3);
  transition:color var(--dur),transform var(--dur) var(--ease)}
.whoami:hover{background:var(--surface-2)}
.whoami:hover svg{color:var(--ink-2);transform:rotate(22deg)}
.whoami:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:var(--focus-off)}
@media (hover:none){ .whoami{height:44px} }

.iconbtn{
  appearance:none;cursor:pointer;flex:none;
  width:36px;height:36px;padding:0;border-radius:var(--r-pill);
  border:var(--bw) solid var(--line);background:var(--surface);color:var(--ink-2);
  display:inline-flex;align-items:center;justify-content:center;
  transition:color var(--dur),background var(--dur),border-color var(--dur),
             transform var(--dur) var(--ease);
  -webkit-tap-highlight-color:transparent;
}
.iconbtn svg{width:19px;height:19px;display:block;fill:currentColor}
.iconbtn:hover{color:var(--ink);background:var(--surface-2);transform:rotate(22deg)}
.iconbtn:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:var(--focus-off)}

/* ══ screens ══════════════════════════════════════════════════════════ */
.screen{
  display:none;width:100%;max-width:var(--shell);
  flex-direction:column;flex:1;min-height:0;
}
.screen.on{display:flex;animation:fade var(--dur-slow) var(--ease) both}
/* Focused programmatically to carry the keyboard across a navigation — a
   landing place, not a control, so it must not draw a ring. */
.screen:focus{outline:none}
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

/* ══ shared controls ══════════════════════════════════════════════════ */
.btn{
  appearance:none;cursor:pointer;
  border:var(--bw) solid var(--line);border-radius:var(--btn-r);
  background:var(--surface);color:var(--ink-2);box-shadow:var(--btn-sh);
  height:var(--btn-h);padding:var(--btn-pad);
  font-family:var(--f-ui);font-size:var(--btn-size);font-weight:var(--btn-w);
  letter-spacing:var(--btn-tr);text-transform:var(--btn-case);white-space:nowrap;
  display:inline-flex;align-items:center;justify-content:center;gap:7px;
  transition:color var(--dur),border-color var(--dur),background var(--dur),
             transform var(--dur) var(--ease),box-shadow var(--dur) var(--ease);
  -webkit-tap-highlight-color:transparent;
}
.btn:hover:not(:disabled){color:var(--ink);background:var(--surface-2);
  border-color:var(--ink-3);transform:translateY(var(--btn-lift))}
.btn:active:not(:disabled){transform:translateY(var(--btn-press))}
.btn:disabled{opacity:.42;cursor:not-allowed;box-shadow:none}
.btn:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:var(--focus-off)}
.btn.solid{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}
.btn.solid:hover:not(:disabled){background:var(--accent);color:var(--accent-ink);
  border-color:var(--accent);filter:brightness(1.08)}
.btn.tiny{height:30px;padding:0 13px;font-size:calc(var(--btn-size) - 1.5px)}
.btn.wide{width:100%}
.btn.grow{flex:1}

.seg{
  display:inline-flex;align-items:center;gap:4px;flex-wrap:wrap;
  border:var(--bw) solid var(--line);border-radius:var(--r-pill);
  padding:4px;background:var(--surface-3);
}
.seg button{
  appearance:none;border:0;background:transparent;color:var(--ink-2);cursor:pointer;
  min-width:32px;height:26px;padding:0 11px;border-radius:var(--r-pill);
  font-family:var(--f-ui);font-size:12px;font-weight:600;
  transition:background var(--dur),color var(--dur);
  -webkit-tap-highlight-color:transparent;
}
.seg button:hover:not(:disabled){color:var(--ink);background:var(--surface-2)}
.seg button[aria-pressed="true"]{background:var(--accent);color:var(--accent-ink)}
/* Hover must not out-rank the chosen option. `.seg button:hover:not(:disabled)`
   carries three class-level components against the pressed rule's two, so
   pointing at the option you already picked turned it grey — and since your
   cursor is still on the button you just clicked, that read as the control
   flickering back off the moment you chose it. */
.seg button[aria-pressed="true"]:hover:not(:disabled){
  background:var(--accent);color:var(--accent-ink);filter:brightness(1.07)}
.seg button:disabled{opacity:.4;cursor:not-allowed}
.seg button:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:1px}

.card{
  border:var(--bw) solid var(--line-2);border-radius:var(--r-3);
  background:var(--surface);box-shadow:var(--sh-1);
  padding:16px 17px 17px;display:flex;flex-direction:column;gap:11px;min-width:0;
}
.label{
  font-size:var(--label-size);font-weight:var(--label-w);letter-spacing:var(--label-tr);
  text-transform:var(--label-case);color:var(--ink-3);margin:0;
}
/* Holds its line even when empty so a note appearing never shunts the layout.
   The clipboard fallback drops a whole URL in here, hence the hard wrap. */
.note{
  margin:0;font-size:11px;color:var(--ink-3);line-height:1.6;
  min-height:1.2em;overflow-wrap:anywhere;
}
.note b{color:var(--ink-2);font-weight:700}
.err{
  margin:8px 0 0;text-align:center;min-height:15px;
  font-size:10.5px;font-weight:700;letter-spacing:.08em;
  text-transform:var(--label-case);color:var(--bad);
}

/* ══ HOME ═════════════════════════════════════════════════════════════
   There is no hero and no strapline. The site is one shelf of games, the
   way a personal site should be — the header says whose it is and the card
   says what the game is, and anything else here would be marketing. */
#s-home{gap:clamp(14px,2.6vh,24px);padding:clamp(10px,3vh,34px) 0 24px}

/* ── the game card ────────────────────────────────────────────────────
   Art on top, three lines underneath. The art is a live loop of the game
   playing itself (js/gamecard.js), so it has to be given real height and
   allowed to be the loudest thing on the page. */
.games{display:grid;gap:14px;grid-template-columns:1fr}
.gamecard{
  position:relative;overflow:hidden;
  border:var(--bw) solid var(--line-2);border-radius:var(--r-3);
  background:var(--surface);box-shadow:var(--sh-1);
  display:flex;flex-direction:column;
  transition:border-color var(--dur),box-shadow var(--dur-slow) var(--ease),
             transform var(--dur-slow) var(--ease);
}
.gamecard:hover{border-color:var(--line);box-shadow:var(--sh-2);transform:translateY(-2px)}
.cardart{
  position:relative;width:100%;
  height:clamp(148px,26vw,208px);
  background:var(--surface-3);
  border-bottom:var(--bw) solid var(--line-2);
  overflow:hidden;
}
/* Taken out of the flow on purpose. The card art sizes ITSELF from the host it
   is mounted in (gamecard.js re-fits on every resize), so leaving the SVG in
   flow makes the host's height depend on the SVG and the SVG's height depend on
   the host — a loop the ResizeObserver walks upward a few pixels at a time. It
   took a couple of minutes on a live page to reach fifty thousand pixels tall.
   Absolutely positioned, the art can never contribute to its host's height. */
.cardart svg{position:absolute;inset:0;display:block;width:100%;height:100%}
.cardbody{padding:15px 17px 16px;display:flex;flex-direction:column;gap:4px}
.gt{
  margin:0;font-family:var(--f-display);font-weight:var(--display-w);
  font-size:clamp(21px,5.2vw,28px);line-height:1.05;
  letter-spacing:var(--display-tr);text-transform:var(--display-case);
}
.gtag{margin:0;font-size:13px;color:var(--ink-2);line-height:1.5}
.ga{display:flex;gap:9px;flex-wrap:wrap;align-items:center;margin-top:10px}
/* The visibility choice rides with the create button because that is the
   moment it matters — but it is a footnote, not a headline. */
/* `margin-left:auto` pushes this to the far end of the button row, which is
   right while it shares a line with them. The moment it wraps it is alone on
   its own line and being flung to the right edge reads as belonging to
   something else — which is exactly what the min-width:640 rule below says,
   and it only ever applied on desktop. */
.gvis{display:flex;align-items:center;gap:7px;margin-left:auto}
@media (max-width:639px){ .gvis{margin-left:0} }
.gvis .vlabel{
  font-size:9px;font-weight:var(--label-w);letter-spacing:var(--label-tr);
  text-transform:var(--label-case);color:var(--ink-3);
}
.gvis .seg{padding:3px}
.gvis .seg button{height:22px;min-width:0;padding:0 9px;font-size:10.5px;gap:4px;
  display:inline-flex;align-items:center}
.gvis .seg button svg{width:11px;height:11px;fill:currentColor;display:block}

/* ── join + open tables ───────────────────────────────────────────────
   A field and a button. It used to sit in a pill inside a pill: a bordered
   bar wrapping a bordered input wrapping nothing that needed wrapping. The
   input already looks like an input. */
.joinrow{display:flex;flex-direction:column;gap:2px}
.joinform{display:flex;align-items:center;gap:9px}
.joinform input{
  appearance:none;border:var(--bw) solid var(--line);border-radius:var(--r-pill);
  background:var(--surface-3);color:var(--ink);
  font-family:var(--f-mono);font-size:17px;font-weight:700;letter-spacing:.28em;
  text-transform:uppercase;text-indent:.28em;text-align:center;
  width:118px;height:var(--btn-h);padding:0 4px;
}
.joinform input:focus{outline:none;border-color:var(--accent);background:var(--surface)}
.joinform input::placeholder{color:var(--ink-3);letter-spacing:.28em}

.public{display:flex;flex-direction:column;gap:10px}
.public-head{display:flex;align-items:center;gap:10px}
.public-head h2{
  margin:0;font-size:var(--label-size);font-weight:var(--label-w);
  letter-spacing:var(--label-tr);text-transform:var(--label-case);color:var(--ink-3);
}
.public-count{
  font-size:10px;font-weight:600;color:var(--ink-3);opacity:.8;
  margin-right:auto;white-space:nowrap;
}
.publicList{display:grid;gap:8px}
.tbl{
  display:flex;align-items:center;gap:12px;min-width:0;
  border:var(--bw) solid var(--line-2);border-radius:var(--r-2);
  background:var(--surface);padding:9px 10px 9px 14px;
  transition:border-color var(--dur),background var(--dur),transform var(--dur) var(--ease);
}
.tbl:hover{border-color:var(--line);background:var(--surface-2);transform:translateX(2px)}
.tbl .tcode{
  font-family:var(--f-mono);font-size:16px;font-weight:700;letter-spacing:.14em;
  color:var(--ink);flex:none;
}
.tbl .tmeta{
  flex:1;min-width:0;font-size:11px;color:var(--ink-3);font-weight:600;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.tbl .tmeta b{color:var(--ink-2);font-weight:700}
/* An empty list is not a component. It is a sentence. */
.empty{margin:0;padding:2px 0;font-size:12px;color:var(--ink-3)}

/* ══ ROOM ═════════════════════════════════════════════════════════════ */
#s-room{gap:13px;padding-bottom:20px;justify-content:flex-start}
/* Flex, not grid. The room-code card is hidden in pass-and-play, and a grid
   keeps the empty track it would have filled — which is what left the two
   remaining cards sitting 159px left of centre with a column of nothing to
   their right. A hidden flex item takes no space at all, so the row is
   centred whether it holds two cards or three. */
.roomgrid{
  display:flex;flex-wrap:wrap;justify-content:center;align-items:stretch;
  gap:13px;width:100%;margin-top:clamp(4px,2vh,18px);
  /* Capped to exactly what the cards inside will occupy, so the row has real
     edges for the preview card below it to line up with. Left uncapped, the
     row was 900px of container holding 733px of centred cards, and the
     full-width preview beneath sat 40px proud on either side — three cards in
     a column, none of them agreeing where the column was. */
  max-width:var(--row-w);margin-inline:auto;
}
/* Online there is a third card, so the row is wider. */
#s-room:has(.codecard:not([hidden])){--row-w:var(--shell)}
.roomgrid>.card{flex:1 1 300px;max-width:360px;min-width:0}
.roomgrid>.codecard{flex:0 1 250px}
/* Same track as the row above, whatever that row currently is. */
.previewcard{width:100%;max-width:var(--row-w);margin-inline:auto}
.codecard{align-items:stretch;text-align:center}
.codecard .label,.codecard .note{text-align:center}
.code{
  font-family:var(--f-mono);font-weight:700;
  font-size:clamp(38px,11vw,54px);letter-spacing:.18em;text-indent:.18em;line-height:1.1;
  color:var(--ink);user-select:all;
}

/* ── the players ──────────────────────────────────────────────────────
   One list, six columns, and every row on the same six whoever is in it:

     [handle] [disc] [who ······ 1fr] [colour] [action]

   Fixed widths on all four of the narrow ones, which is what makes a
   person's swatch and a computer's swatch land on the same pixel — a row
   with nothing to put in a cell EMPTIES it rather than dropping it, so the
   columns cannot drift apart from one row to the next.

   The card is the only edge in here. A row is a line in a list, so it has no
   border of its own until you point at it — and then only a faint one, on a
   border that was always reserved so nothing moves when it appears. */
.seatlist{
  list-style:none;margin:0;padding:0;display:grid;gap:2px;
  /* The three numbers everything in this list lines up against. Anything that
     has to start at the name column reads them rather than guessing. */
  --seat-pad:8px; --seat-gap:10px; --grip-w:26px;
  --name-x:calc(var(--seat-pad) + var(--grip-w) + var(--seat-gap));
}
.seat{
  display:flex;align-items:center;gap:var(--seat-gap);min-width:0;min-height:44px;
  border:var(--bw) solid transparent;border-radius:var(--r-2);
  padding:6px var(--seat-pad);background:transparent;
  transition:border-color var(--dur),background var(--dur),opacity var(--dur);
}
.seat:hover,.seat:focus-within{border-color:var(--line-2);background:var(--surface-2)}
/* Your own row, online. A tint rather than an outline: it says "this one is
   you" without adding another rectangle to a panel full of them. */
.seat.mine{background:color-mix(in srgb,var(--c,var(--accent)) 9%,transparent)}
.seat.mine:hover,.seat.mine:focus-within{
  background:color-mix(in srgb,var(--c,var(--accent)) 14%,var(--surface-2));
}
.seat svg{width:24px;height:24px;display:block;flex:none}
.seat .ds{flex:none;width:24px;height:24px}
/* Everything about who is in the seat, and the only column that flexes. */
.seat .who{display:flex;align-items:center;gap:7px;flex:1;min-width:0}
.seat .nm{
  font-size:13.5px;font-weight:600;flex:0 1 auto;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.seat.free .nm{color:var(--ink-3);font-weight:400;font-style:italic}
.seat .tags{display:flex;align-items:center;gap:7px;flex:0 1 auto;min-width:0;overflow:hidden}
.seat .tags:empty{display:none}
.seat .tag{
  font-size:8.5px;font-weight:var(--label-w);letter-spacing:.08em;
  text-transform:var(--label-case);color:var(--ink-3);white-space:nowrap;
}
.seat .tag.you{color:var(--accent)}
/* Not a badge and not a pill — a word. It is short because the row's name
   column is shared with a difficulty picker, and "cpu" is the shorthand every
   player of every game already knows. The full phrase goes to a screen
   reader, and the note under the list names the level in full. */
.seat .tag.cpu{color:var(--ink-2)}
/* Held open, not taken out: a cell with nothing to say still owes the rows
   below it its width. Invisible also drops it from the tab order and from a
   screen reader, which is the whole reason not to use opacity here. */
.seat .off{visibility:hidden;pointer-events:none}
.seat .sit{
  appearance:none;cursor:pointer;flex:none;margin-left:auto;
  border:var(--bw) solid var(--line);border-radius:var(--r-pill);
  background:transparent;color:var(--ink-2);height:26px;padding:0 11px;
  font-family:var(--f-ui);font-size:9.5px;font-weight:var(--label-w);
  letter-spacing:var(--label-tr);text-transform:var(--label-case);
  transition:color var(--dur),border-color var(--dur),background var(--dur);
}
.seat .sit:hover{color:var(--accent-ink);background:var(--accent);border-color:var(--accent)}
.seat .sit:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:1px}

/* ── editing a player ─────────────────────────────────────────────────
   Pass-and-play edits every seat, because one person is setting the table up
   for everyone in the room; online you edit your own and nobody else's. The
   field is the name — it is not a control that opens a dialog to change the
   name, it is just the name, and you type over it. */
/* 16px is not a taste decision: iOS Safari zooms the whole page in when you
   focus a field smaller than that, and there is no reliable way to zoom it
   back out afterwards. Every typable field on the site clears the bar. */
.seat .nmin{
  appearance:none;flex:1 1 60px;min-width:0;
  border:0;border-bottom:var(--bw) solid transparent;border-radius:0;
  background:transparent;color:var(--ink);
  font-family:var(--f-ui);font-size:16px;font-weight:600;
  height:30px;padding:0;
  transition:border-color var(--dur);
}
/* An underline, not a box. It is the name — you type over it — and a full
   outline around it makes it a control sitting inside a row inside a card. */
.seat .nmin:hover{border-bottom-color:var(--line-2)}
.seat .nmin:focus{outline:none;border-bottom-color:var(--accent)}
.seat .nmin::placeholder{color:var(--ink-3);font-weight:400;font-style:italic}

/* The swatch is the current colour, and pressing it opens the row of nine
   underneath rather than a floating panel — a popover has to be positioned,
   and on a phone it lands over the thing you are trying to compare against. */
.swatch{
  appearance:none;cursor:pointer;flex:none;position:relative;
  width:30px;height:30px;padding:0;border-radius:var(--r-pill);
  border:2px solid var(--line);background:var(--c,var(--accent));
  transition:transform var(--dur) var(--ease),border-color var(--dur);
  -webkit-tap-highlight-color:transparent;
}
.swatch:hover{transform:scale(1.08);border-color:var(--ink-3)}
.swatch:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:2px}
.swatch[aria-expanded="true"]{border-color:var(--ink-2)}

/* Nine circles under the row they belong to. No box around them: they are
   already grouped by sitting in a line under one seat, and a panel drawn
   round them was one more rectangle inside a rectangle inside the card. */
.picker{
  display:flex;flex-wrap:wrap;gap:8px;margin:0;
  /* Plus the 1px transparent border every seat row carries — that is what
     put the swatch grid one pixel left of the column it lines up with. */
  padding:2px 0 6px calc(var(--name-x) + var(--bw));
}
.picker button{
  appearance:none;cursor:pointer;position:relative;
  width:32px;height:32px;padding:0;border-radius:var(--r-pill);
  border:2px solid transparent;background:var(--c);
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.22);
  transition:transform var(--dur) var(--ease),border-color var(--dur);
  -webkit-tap-highlight-color:transparent;
}
.picker button:hover{transform:scale(1.12)}
.picker button:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:2px}
.picker button[aria-pressed="true"]{border-color:var(--ink)}
/* Somebody else is this colour. Not disabled — picking it trades with them,
   which is the only way two people at one table can ever swap. */
.picker button[data-taken="1"]::after{
  content:"";position:absolute;inset:5px;border-radius:var(--r-pill);
  background:var(--surface);opacity:.82;
}
.picker button[data-taken="1"]{cursor:pointer}

/* ── the drag handle ──────────────────────────────────────────────────
   Quiet until you go near the row, because on a table of four it would
   otherwise be four pieces of furniture in front of four names. Never quiet
   on a touch screen: there is no hover there, so "reveal on hover" is a
   control that simply does not exist for half the people using the site. */
.seat .grip{
  appearance:none;cursor:grab;flex:none;position:relative;
  width:var(--grip-w);height:30px;padding:0;
  display:flex;align-items:center;justify-content:center;
  border:0;border-radius:var(--r-1);background:transparent;color:var(--ink-3);
  opacity:0;
  transition:opacity var(--dur),color var(--dur),background var(--dur);
  /* The browser must not pan the page when a finger starts here, and it must
     not select text when a mouse does. */
  touch-action:none;-webkit-tap-highlight-color:transparent;user-select:none;
}
/* A 26px control is a 26px target. Grow the hit area without growing the row —
   the row is 44px tall already, so this reaches the whole height of it. */
.seat .grip::before{content:"";position:absolute;inset:-7px -9px}
.seat .grip svg{width:17px;height:17px}
.seat:hover .grip,.seat:focus-within .grip,.seat .grip:focus-visible{opacity:1}
.seat .grip:hover{color:var(--ink-2);background:var(--surface-3)}
.seat .grip:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:0}
.seat .grip:active{cursor:grabbing}
@media (hover:none){
  .seat .grip{opacity:.55}
  .seat .grip svg{width:19px;height:19px}
}

/* While a row is being carried, the ones it passes slide out of its way. The
   carried row itself follows the pointer exactly and must never be animated —
   a transition on it is a row that lags behind the finger holding it. */
.seatlist.dragging{user-select:none}
.seatlist.dragging .seat{transition:transform var(--dur) var(--ease)}
.seatlist.dragging .seat .grip{opacity:1}
.seat.lifting{
  transition:none;position:relative;z-index:3;
  border-color:color-mix(in srgb,var(--c,var(--accent)) 60%,transparent);
  background:var(--surface);
  box-shadow:0 16px 30px -18px rgba(0,0,0,.5),0 2px 0 -1px var(--line-2);
}
.seat.lifting .grip{cursor:grabbing;color:var(--ink-2)}

/* ── computer players ─────────────────────────────────────────────────
   A computer's row is the SAME row — same height, same six columns, same
   everything. Where a person has a name you type over, it has the difficulty
   that is its name, in that same slot. What a difficulty does is a sentence,
   so it lives under the list rather than on the row: a row twice as tall as
   its neighbours breaks the one thing this list has, which is a rhythm. */
.seat .lvl{
  appearance:none;flex:0 1 auto;min-width:0;width:auto;cursor:pointer;
  border:0;border-bottom:var(--bw) solid transparent;border-radius:0;
  background:transparent;color:var(--ink);
  font-family:var(--f-ui);font-size:13.5px;font-weight:600;
  height:30px;padding:0 20px 0 0;
  /* The caret is drawn here because a native select arrow is a different
     colour, size and inset in every one of the ten themes. Two squares, each
     with one diagonal band: "\" then "/". The percentages are load-bearing —
     the band is a slice of the gradient LINE, which on a 7px square is 9.9px
     long, so 42%–58% is the 1.6px stroke it looks like. At 47%–53% it is half
     a pixel and antialiases away to nothing on every screen. */
  background-image:linear-gradient(45deg,transparent 42%,currentColor 42% 58%,transparent 58%),
                   linear-gradient(-45deg,transparent 42%,currentColor 42% 58%,transparent 58%);
  background-size:7px 7px,7px 7px;
  background-position:calc(100% - 13px) 12px,calc(100% - 8px) 12px;
  background-repeat:no-repeat;
  transition:border-color var(--dur);
}
.seat .lvl:hover{border-bottom-color:var(--line-2)}
.seat .lvl:focus{outline:none;border-bottom-color:var(--accent)}
.seat .lvl option{color:var(--ink);background:var(--surface)}
/* No outline: the glyph IS the button, and a circle drawn round a cross is a
   second rectangle's worth of noise for something that is already unmistakable. */
.seat .unseat{
  appearance:none;cursor:pointer;flex:none;position:relative;
  width:26px;height:26px;padding:0;
  display:flex;align-items:center;justify-content:center;
  border:0;border-radius:var(--r-pill);
  background:transparent;color:var(--ink-3);
  transition:color var(--dur),background var(--dur);
}
.seat .unseat::before{content:"";position:absolute;inset:-9px}
.seat .unseat svg{width:14px;height:14px}
.seat .unseat:hover{color:var(--bad);background:color-mix(in srgb,var(--bad) 12%,transparent)}
.seat .unseat:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:1px}

/* Actions, on the same columns as the rows above them — the first plus sign
   stands where the discs stand and its label where the names do. Two halves,
   because there are two kinds of player and neither is a sub-case of the
   other; online only the computer half exists, because people arrive with the
   code and there is nothing for the other one to do.

   Not drawn as an empty box: these are things you press, not slots waiting to
   be filled, and the empty seats above are already saying that. */
/* Same 44px pitch as every other row. It was 42 — not a gap you can name
   when you look at it, only one that makes the list feel slightly off all
   the way down. */
.seat.addrow{padding:0;min-height:44px;gap:0}
.seat.addrow .add{
  /* Sized to their labels, then given an equal share of what is left over.
     An even 50/50 split is a pixel short for the longer of the two at 360, and
     "Add a play…" is not an offer anybody can act on. */
  appearance:none;cursor:pointer;flex:1 1 auto;min-width:0;min-height:40px;
  display:flex;align-items:center;gap:10px;
  padding:0 var(--seat-pad);
  border:0;border-radius:var(--r-2);background:transparent;color:var(--ink-3);
  font-family:var(--f-ui);font-size:12.5px;font-weight:600;text-align:left;
  transition:color var(--dur),background var(--dur);
}
/* Only the first one lines its plus up with the discs; a second column of
   plus signs mid-row would read as a table that had grown a new column. */
.seat.addrow .add:first-child{padding-left:var(--name-x)}
.seat.addrow .add + .add{
  padding-left:calc(var(--seat-gap) + 2px);
  box-shadow:inset var(--bw) 0 0 var(--line-2);   /* a hairline, not a border */
}
.seat.addrow .add .ds{display:flex;align-items:center;justify-content:center}
.seat.addrow .add svg{width:15px;height:15px}
.seat.addrow .add .nm{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.seat.addrow .add:hover:not(:disabled){color:var(--ink);background:var(--surface-2)}
.seat.addrow .add:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:1px}
.seat.addrow .add:disabled{opacity:.42;cursor:not-allowed}
/* Below 360 the two halves cannot both hold their label — measured at 344,
   where "Add a player" is four pixels short. Half a word is not an offer, so
   they stack rather than clip. */
@media (max-width:359px){
  .seat.addrow{flex-wrap:wrap}
  .seat.addrow .add{flex:1 0 100%}
  .seat.addrow .add + .add{box-shadow:none;padding-left:var(--name-x)}
}

/* ── the seat row on a touch screen ───────────────────────────────────
   Every control in this row is something you aim a finger at, and the row is
   dense by design. Nothing here changes what it LOOKS like on a mouse — it
   grows the things you press, and only where there is no cursor to press
   them with. */
.swatch::before{content:"";position:absolute;inset:-6px}
@media (hover:none){
  .seatlist{--grip-w:30px}
  .seat{padding:8px var(--seat-pad);min-height:52px}
  .seat .nmin,.seat .lvl{height:36px}
  .seat .lvl{background-position:calc(100% - 13px) 15px,calc(100% - 8px) 15px}
  .swatch{width:36px;height:36px}
  .swatch::before{inset:-5px}
  .seat .unseat{width:32px;height:32px}
  .seat .unseat::before{inset:-7px}
  .picker button{width:40px;height:40px}
  .seat.addrow .add{min-height:48px}
  /* Every OTHER control in this block was grown; the segmented ones were
     missed, and they are the densest things on the site — 22px for the
     visibility pair, 26px for mode, board size and both Settings toggles.
     Chrome's touch adjustment stretches a tap onto the nearest candidate,
     which was hiding it: the box is 26px but a finger lands from about 5px
     outside either edge. That is still ~35px against a 44px minimum, and it
     depends on there being nothing else nearby to snap to instead. */
  .seg{padding:5px}
  .seg button{min-height:40px;padding:0 15px;font-size:13px}
  .gvis .seg button{min-height:40px;padding:0 13px;font-size:12px}
  /* Header and in-game strip: 30px and 28px respectively. */
  .btn.tiny{height:40px;padding:0 16px}
  .brand{padding:6px 0}
}

/* ── the rules preview ────────────────────────────────────────────────
   Two demos, one per mode, and only the chosen one is mounted in the DOM —
   both running at once is two physics loops for a card nobody is looking at.
   The board inside sizes itself off this box (js/gamecard.js), so it needs a
   real height and must never be allowed to size the box back. */
.previewcard{gap:9px}
/* Square, and no wider than it is tall. At `size:"inline"` the demo plate is
   sized off this box's HEIGHT and draws into a square viewBox, so every pixel
   of width past that is runway it never uses — full-card width left a small
   board marooned in 700px of nothing, which reads as a broken image rather
   than as a demo. Matching the width to the height instead means the extra
   room goes into making the board BIGGER. `max-width` is the guard for a
   screen narrower than the height asks for; there the box simply letterboxes
   the way it always did. */
.preview{
  /* No edge and no fill. The card already has one border, and gamecard.js
     draws the plate as a filled, shadowed object in its own right — a tray
     between the two was a second frame and a third tone around something that
     was already clearly a thing on a surface. */
  position:relative;width:100%;aspect-ratio:1;
  height:auto;max-height:clamp(190px,26vh,260px);
  margin-inline:auto;overflow:hidden;
}
.preview svg{position:absolute;inset:0;display:block;width:100%;height:100%}
.preview-head{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap}
.preview-head .pname{font-size:12.5px;font-weight:700;color:var(--ink)}
.preview-head .pblurb{font-size:11px;color:var(--ink-3);flex:1;min-width:0}
/* The board is square and capped, so on a wide screen a stacked card is a
   square in the middle of a very empty rectangle. Side by side instead: the
   width goes to the words, which are the part that wants it. */
@media (min-width:760px){
  .previewcard{flex-direction:row;align-items:center;gap:20px;max-width:var(--row-w);margin-inline:auto}
  .previewcard .preview-head{
    flex:1 1 0;min-width:0;flex-direction:column;align-items:flex-start;gap:5px;
  }
  .previewcard .preview-head .pname{font-size:15px}
  .previewcard .preview-head .pblurb{font-size:12.5px;line-height:1.5;flex:none}
  .previewcard .preview{flex:none;margin-inline:0}
}

.cfg-row{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
/* A text field on a settings row. Underlined rather than boxed, like the name
   fields in the room — a rounded rectangle inside a row inside a sheet is one
   rectangle too many, and this is the same kind of thing they are. */
.txtin{
  appearance:none;flex:1 1 140px;min-width:0;max-width:220px;
  border:0;border-bottom:var(--bw) solid var(--line);border-radius:0;
  background:transparent;color:var(--ink);text-align:right;
  font-family:var(--f-ui);font-size:16px;font-weight:600;
  height:34px;padding:0 2px;
  transition:border-color var(--dur);
}
.txtin:hover{border-bottom-color:var(--ink-3)}
.txtin:focus{outline:none;border-bottom-color:var(--accent)}
.txtin::placeholder{color:var(--ink-3);font-weight:400;font-style:italic}
.cfg-name{
  font-size:10.5px;font-weight:var(--label-w);letter-spacing:var(--label-tr);
  text-transform:var(--label-case);color:var(--ink-3);
}
.roomactions{display:flex;gap:9px;margin-top:4px}
/* The two visibility buttons carry an icon, so they need to be a touch wider
   than a plain number in a segmented control. */
#visSeg button{display:inline-flex;align-items:center;gap:5px}
#visSeg button svg{width:12px;height:12px;fill:currentColor;display:block;flex:none}

/* ══ GAME ═════════════════════════════════════════════════════════════ */
#s-game{gap:0}
/* Three tracks, with the outer two equal, so whatever is centred here is
   centred against the SCREEN and not against whatever happens to be sitting
   beside it. As a flex row this line drifted by half the width of the buttons
   on the right — invisible while both sides were empty, then noticeably wrong
   the moment "Play again" appeared. */
.gamehead{
  display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
  align-items:center;gap:10px;min-height:30px;
}
.gamehead>.rules{grid-column:2}
.gamehead>.btn{grid-column:3;justify-self:end}
/* Two buttons can share the right-hand track without either shoving the
   middle one off its axis. */
.gamehead>.btn~.btn{grid-column:3;margin-right:0}
.headright{grid-column:3;justify-self:end;display:flex;gap:8px;align-items:center}
.rules{
  text-align:center;min-width:0;
  font-size:10px;font-weight:600;letter-spacing:.04em;color:var(--ink-3);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.rules b{color:var(--ink-2);font-weight:700}

.turnbar{
  display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;
  margin:clamp(7px,1.7vh,15px) 0 clamp(3px,.8vh,8px);min-height:38px;
}
.turnbar .chip{
  display:flex;align-items:center;gap:8px;min-width:0;max-width:100%;
  padding:5px 10px;border-radius:var(--r-pill);
  /* No border and no fill on ANY chip. The old pair carried an outline plus a
     tint when active and an outline alone when not, and the "you" marker was a
     filled pill inside that outline — three edges deep, and the active chip
     ended up taller than its neighbour, so the row jumped as the turn passed.
     Whose turn it is now reads as presence: the player to move is at full
     strength, everyone else fades. Nothing moves and nothing changes size. */
  border:0;background:transparent;
  opacity:.38;
  transition:opacity var(--dur-slow) var(--ease);
}
.turnbar .chip.on{opacity:1}
.turnbar .chip.gone{opacity:.2}
/* Higher specificity so it wins whatever the order: the player to move is never
   the dimmest thing on screen, even when they have dropped out. */
.turnbar .chip.on.gone{opacity:.6}
.turnbar .chip svg{width:23px;height:23px;display:block;flex:none}
.turnbar .chip .nm{font-size:13px;font-weight:600;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The player to move gets their own colour in the name as well as full opacity
   — on a monochrome theme, opacity alone is a weak signal. Mixed toward --ink
   so it stays readable as text rather than as a counter. */
.turnbar .chip.on .nm{color:color-mix(in srgb,var(--c) 45%,var(--ink))}
/* "you" and "cpu" are asides, not badges. A parenthetical says the same thing
   without another filled rectangle inside an outline. */
.turnbar .chip .me{
  /* Text, not a control. Stated exhaustively — height and display included —
     because this used to share a class name with the topbar identity button,
     inherited its 36px, and made any chip carrying a marker taller than one
     without. That is precisely the mismatch this rework removed. */
  display:inline;height:auto;min-height:0;line-height:inherit;
  font-size:11px;font-weight:600;color:var(--ink-3);flex:none;
  background:none;border:0;padding:0;letter-spacing:0;text-transform:none;
}
.turnbar .chip .me::before{content:"("}
.turnbar .chip .me::after{content:")"}
/* A computer's turn is a beat where the board is deliberately not answering,
   and a chip that just sits there reads as a hang. Motion-off kills this the
   same way it kills everything else, and the status line still says so. */
.turnbar .chip.thinking{animation:botPulse 1.5s var(--ease) infinite}
/* The chip has no fill to glow any more, so the beat lives in the counter —
   which is the part that means "this player" — rather than in a ring around
   a rectangle that is no longer drawn. */
@keyframes botPulse{
  0%,100%{opacity:1}
  50%    {opacity:.55}
}
.status .think{display:inline-flex;gap:2px;margin-left:2px;vertical-align:baseline}
.status .think i{
  width:3px;height:3px;border-radius:var(--r-pill);background:currentColor;
  opacity:.3;animation:botDot 1.1s var(--ease) infinite;
}
.status .think i:nth-child(2){animation-delay:.16s}
.status .think i:nth-child(3){animation-delay:.32s}
@keyframes botDot{0%,70%,100%{opacity:.25}35%{opacity:.9}}

/* The disc colours are tuned to read as counters on a board, not as body text
   on a page — set raw, "Blue connects four" lands at 2.2:1 on a light theme.
   Mixing toward --ink keeps the player's hue while borrowing the one colour
   every theme guarantees is readable against its own surface, and it works in
   both directions: it darkens on a light theme and lightens on a dark one. */
.turnbar .verdict{
  font-family:var(--f-display);font-weight:var(--display-w);
  font-size:clamp(17px,4.2vw,26px);letter-spacing:var(--display-tr);
  text-transform:var(--display-case);text-align:center;
  color:color-mix(in srgb,var(--vc,var(--ink)) 42%,var(--ink));
  animation:pop .5s var(--ease) both;
}
@keyframes pop{from{opacity:0;transform:scale(.88)}to{opacity:1;transform:none}}

.stage{
  flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:clamp(7px,1.5vh,13px);min-height:0;width:100%;
}
/* Thumb-sized tip buttons cost about 90px of height, which a 1280x720 laptop
   does not have to give — the board would have had to shrink to pay for them.
   On anything wide enough the pad goes BESIDE the board instead, where there
   was always spare room, and sits next to the thing it tips. Below that width
   it goes back under the board, where the extra height is affordable because
   the board is sized off a smaller share of the viewport anyway. */
@media (min-width:860px){
  .stage{
    display:grid;justify-content:center;align-items:center;
    grid-template-columns:auto auto;
    grid-template-areas:"board grav" "status status";
    column-gap:clamp(10px,2vw,22px);
  }
  .stage .pad{grid-area:board}
  .stage .gravity{
  /* The label and the cross of arrows already read as one object by position,
     and every button carries its own edge. A container around them was a
     border 7px outside four borders. */
  display:flex;align-items:center;gap:14px;
  padding:0;background:transparent;border:0;
}
  /* Centred on the BOARD, not on the board plus the tip pad beside it —
     spanning both tracks put the sentence about the board 100px to its
     right. */
  .stage .status{grid-area:status;grid-column:board}
  /* Stacked, so the cross of arrows sits under its own label rather than
     stretching the pad into a wide bar that crowds the board. */
  .gravity{flex-direction:column;gap:9px;padding:0}
  .gravity .glabel{max-width:none;text-align:center}
}
.pad{
  display:grid;
  grid-template-columns:var(--strip) var(--bs) var(--strip);
  grid-template-rows:var(--strip) var(--bs) var(--strip);
  gap:var(--gap);
  --bs:min(calc(100vw - 2*(var(--strip) + var(--gap)) - 34px),52dvh,520px);
}
.strip{display:grid;gap:2px}
.strip.top{grid-area:1/2/2/3;grid-template-columns:repeat(var(--n),1fr)}
.strip.bottom{grid-area:3/2/4/3;grid-template-columns:repeat(var(--n),1fr)}
.strip.left{grid-area:2/1/3/2;grid-template-rows:repeat(var(--n),1fr)}
.strip.right{grid-area:2/3/3/4;grid-template-rows:repeat(var(--n),1fr)}
.boardwrap{grid-area:2/2/3/3;position:relative;will-change:transform}

.slot{
  position:relative;appearance:none;border:0;padding:0;cursor:pointer;
  background:transparent;color:var(--ink-3);
  display:flex;align-items:center;justify-content:center;border-radius:var(--r-1);
  transition:color .16s,background .16s;
  -webkit-tap-highlight-color:transparent;
}
.slot svg{width:18px;height:18px;display:block;transition:transform .16s var(--ease)}
/* A thumb-sized target without changing the layout: grow only away from the
   strip's axis, so neighbouring slots never overlap and steal taps. */
.slot::after{content:"";position:absolute}
.strip.top .slot::after,.strip.bottom .slot::after{inset:-12px -1px}
.strip.left .slot::after,.strip.right .slot::after{inset:-1px -12px}
.slot .tri{fill:currentColor}
.slot:not([aria-disabled="true"]):hover,
.slot:not([aria-disabled="true"]):focus-visible{
  color:var(--c);background:color-mix(in srgb,var(--c) 16%,transparent)}

/* The arrow points into the lane it drops down, and while you are pointing at
   it it rocks that way — the direction a disc is about to travel, shown rather
   than described. Which way is "in" depends on which gutter the slot is in, so
   the offset is a per-strip token and one keyframe serves all four. */
.strip.top    .slot{--nx:0px;  --ny:5px}
.strip.bottom .slot{--nx:0px;  --ny:-5px}
.strip.left   .slot{--nx:5px;  --ny:0px}
.strip.right  .slot{--nx:-5px; --ny:0px}
@keyframes slotNudge{
  0%,100%{transform:translate(0,0) scale(1.22)}
  55%    {transform:translate(var(--nx),var(--ny)) scale(1.22)}
}
.slot:not([aria-disabled="true"]):hover svg,
.slot:not([aria-disabled="true"]):focus-visible svg{
  transform:scale(1.22);
  animation:slotNudge .8s var(--ease) infinite;
}
/* Keyboard players need to see where they are; the lit lane alone reads exactly
   like a hover. */
.slot:focus-visible,.gbtn:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:1px}
.slot[aria-disabled="true"]{color:var(--line);cursor:default}
/* The held state a finger gets in place of hover. Same look, driven by a class
   rather than by a pseudo-class the touch screen never sets. */
.slot.held{color:var(--c);background:color-mix(in srgb,var(--c) 16%,transparent)}
.gbtn.held{color:var(--c);border-color:color-mix(in srgb,var(--c) 60%,transparent);
  background:color-mix(in srgb,var(--c) 16%,var(--surface))}
.slot[aria-disabled="true"] svg{transform:scale(.42)}

#board{display:block;width:100%;height:100%;overflow:visible}
.face{fill:var(--board-face)}
.socket{fill:var(--board-hole)}
.socketRim{fill:none;stroke:var(--board-rim);stroke-width:.02}
.bevel{fill:none;stroke-width:.05}
/* The gradients live in board.js's <defs>; their stops are themed from here so
   the same markup can be bone-white plastic or a flat terminal glyph. */
.bev-a{stop-color:var(--bev-a)}
.bev-m{stop-color:color-mix(in srgb,var(--bev-b) 55%,transparent)}
.bev-b{stop-color:var(--bev-b)}
.gloss-a{stop-color:var(--gloss);stop-opacity:var(--gloss-o)}
.gloss-m{stop-color:var(--gloss);stop-opacity:calc(var(--gloss-o) * .14)}
.gloss-b{stop-color:var(--gloss);stop-opacity:0}
.shade-a{stop-color:var(--shade);stop-opacity:var(--shade-o)}
.shade-b{stop-color:var(--shade);stop-opacity:0}
.discEdge{stroke:var(--disc-edge)}
/* The emblem is stamped twice — a dark impression offset down, a light one on
   top. It is the only thing identifying a player on a monochrome theme, so a
   theme that flattens the discs must strengthen these rather than drop them. */
.em-d{fill:none;stroke:var(--emblem-d)}
.em-l{fill:none;stroke:var(--emblem-l)}
/* The emblem is an aid, not decoration, so it can be switched off in Settings
   by anyone who would rather have the plain counter. Off is a choice; on is
   the default, because it is the only thing identifying a player on a
   monochrome theme or to someone who cannot separate two hues. */
html[data-emblems="off"] .em,
html[data-emblems="off"] .gcEmD,
html[data-emblems="off"] .gcEmL{display:none}

/* One colour in, three surfaces out. The step is mixed toward black and the
   centre toward white, so a counter reads as moulded plastic rather than as a
   flat sticker — and it works for any hue, which it has to, because the hue
   is the player's choice and arrives at runtime. */
.discBody{fill:var(--seat)}
.discStep{stroke:color-mix(in srgb,var(--seat) var(--disc-step),#000);stroke-width:.055}
.discBoss{fill:color-mix(in srgb,var(--seat) var(--disc-boss),#FFF)}
.ghost{opacity:.34}
.lane{fill:currentColor;opacity:.12}
.ring{fill:none;stroke:var(--win-line);stroke-width:.05;transform-box:fill-box;
  transform-origin:center;animation:ringOut .5s var(--ease) forwards}
@keyframes ringOut{from{opacity:.7;transform:scale(.45)}to{opacity:0;transform:scale(1.55)}}
.winline{fill:none;stroke:var(--win-line);stroke-linecap:round;stroke-width:.13;
  stroke-dasharray:100;stroke-dashoffset:100;animation:draw .5s var(--ease) .1s forwards}
@keyframes draw{to{stroke-dashoffset:0}}
.win .discBody{animation:thump 1.5s ease-in-out .45s infinite}
@keyframes thump{0%,100%{transform:scale(1)}50%{transform:scale(1.07)}}
.dead{opacity:.44;transition:opacity .5s var(--ease) .2s}

.gravity{
  /* No edge and no fill. The label and the cross of arrows already read as one
     object by position, and every button in the cross carries its own border —
     a container around them was one more rounded rectangle sitting 7px outside
     four rounded rectangles. */
  display:flex;align-items:center;gap:14px;
  border:0;background:transparent;padding:0;
}
.gravity .glabel{
  font-size:10px;font-weight:var(--label-w);letter-spacing:var(--label-tr);
  text-transform:var(--label-case);color:var(--ink-3);max-width:78px;line-height:1.5;
}
/* Sized for a thumb, not a cursor. Tipping the board is one of only two moves
   in the game and it has a keyboard shortcut on the arrow keys, so the pad it
   shares that job with has no business being the smallest target on screen. */
.gpad{display:grid;grid-template-columns:repeat(3,46px);grid-template-rows:repeat(2,40px);gap:4px}
.gbtn{
  appearance:none;cursor:pointer;padding:0;
  border:var(--bw) solid var(--line-2);border-radius:var(--r-1);
  background:var(--surface-3);color:var(--ink-2);
  display:flex;align-items:center;justify-content:center;
  transition:color .16s,background .16s,border-color .16s,transform .16s var(--ease);
  -webkit-tap-highlight-color:transparent;
}
.gbtn svg{width:21px;height:21px;fill:currentColor;display:block}
.gbtn[data-dir="up"]{grid-area:1/2}
.gbtn[data-dir="left"]{grid-area:2/1}
.gbtn[data-dir="down"]{grid-area:2/2}
.gbtn[data-dir="right"]{grid-area:2/3}
.gbtn:not(:disabled):hover{
  color:var(--c);border-color:color-mix(in srgb,var(--c) 60%,transparent);
  background:color-mix(in srgb,var(--c) 16%,var(--surface));transform:translateY(-1px)}
.gbtn:disabled{opacity:.28;cursor:not-allowed}

.status{
  text-align:center;min-height:17px;margin:0;padding:0 6px;
  font-size:11px;color:var(--ink-3);font-weight:600;line-height:1.7;
}
.status b{color:var(--ink-2);font-weight:700}

/* The footer is gone. It said "Friendslop Fridays", which is what the mark in
   the header already says, on every screen, forever. */

/* ══ SETTINGS ═════════════════════════════════════════════════════════
   A right-hand drawer on a desktop, a bottom sheet on a phone. */
/* A scrim is a shadow, not ink: mixing it from --ink laid a 42% *white* veil
   over every dark theme and washed the whole page out the moment Settings
   opened. It stays dark in both directions, and --scrim is there for a theme
   that wants its own. */
.scrim{
  position:fixed;inset:0;z-index:70;
  background:var(--scrim);
  backdrop-filter:blur(2px);
  animation:fadeIn var(--dur) var(--ease) both;
}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}

/* ── are you sure ─────────────────────────────────────────────────────
   The settings sheet's twin: same scrim, same `inert` behind it, same
   Escape. It sits in the middle rather than at the edge because it is a
   question rather than a panel — you answer it and it goes. */
.confirm{
  position:fixed;z-index:72;left:50%;top:50%;transform:translate(-50%,-50%);
  width:min(400px,calc(100vw - 32px));
  display:flex;flex-direction:column;gap:9px;
  padding:20px;
  background:var(--bg);border:var(--bw) solid var(--line);border-radius:var(--r-3);
  box-shadow:var(--sh-2);
  animation:confirmIn var(--dur-slow) var(--ease) both;
}
@keyframes confirmIn{from{transform:translate(-50%,-46%);opacity:0}
                     to{transform:translate(-50%,-50%);opacity:1}}
.confirm h2{
  margin:0;font-family:var(--f-display);font-weight:var(--display-w);
  font-size:clamp(17px,4.4vw,21px);letter-spacing:var(--display-tr);
  text-transform:var(--display-case);color:var(--ink);
}
.confirm p{margin:0;font-size:13px;line-height:1.5;color:var(--ink-2)}
.confirm-acts{display:flex;gap:9px;margin-top:7px}
.confirm-acts .btn{flex:1 1 0;min-height:44px}
/* The destructive one is the site's warning colour rather than its accent —
   an accent button is "do the thing you came here to do", and this is not. */
.confirm-acts .btn.danger{
  background:var(--bad);border-color:var(--bad);color:#fff;
}
.confirm-acts .btn.danger:hover{filter:brightness(1.08)}

.sheet{
  position:fixed;z-index:71;top:0;right:0;bottom:0;
  width:min(420px,100%);
  display:flex;flex-direction:column;
  background:var(--bg);border-left:var(--bw) solid var(--line);
  box-shadow:var(--sh-2);
  animation:slideIn var(--dur-slow) var(--ease) both;
}
@keyframes slideIn{from{transform:translateX(18px);opacity:0}to{transform:none;opacity:1}}
.sheet-head{
  display:flex;align-items:center;justify-content:space-between;gap:12px;flex:none;
  padding:16px 16px 12px;border-bottom:var(--bw) solid var(--line-2);
}
.sheet-head h2{
  margin:0;font-family:var(--f-display);font-weight:var(--display-w);
  font-size:19px;letter-spacing:var(--display-tr);text-transform:var(--display-case);
}
.sheet-body{
  flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  padding:16px 16px 22px;display:flex;flex-direction:column;gap:22px;
}
.sheet-sec{display:flex;flex-direction:column;gap:9px}
.sheet-foot{
  flex:none;display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:10px 16px calc(12px + env(safe-area-inset-bottom));
  border-top:var(--bw) solid var(--line-2);
  font-size:9.5px;font-weight:var(--label-w);letter-spacing:var(--label-tr);
  text-transform:var(--label-case);color:var(--ink-3);
}

.themegrid{display:grid;gap:9px;grid-template-columns:repeat(2,minmax(0,1fr))}
.themecard{
  appearance:none;cursor:pointer;text-align:left;padding:0;overflow:hidden;
  border:2px solid var(--line-2);border-radius:var(--r-2);
  background:var(--surface);color:inherit;
  display:flex;flex-direction:column;
  transition:border-color var(--dur),transform var(--dur) var(--ease),box-shadow var(--dur);
  -webkit-tap-highlight-color:transparent;
}
.themecard:hover{transform:translateY(-2px);box-shadow:var(--sh-1);border-color:var(--line)}
.themecard[aria-checked="true"]{border-color:var(--accent)}
.themecard:focus-visible{outline:var(--focus-w) solid var(--focus);outline-offset:2px}
/* The swatch is painted from the theme's own colours, inline, so the picker
   shows what you are choosing without loading ten stylesheets at once. */
.themecard .sw{
  height:52px;position:relative;display:flex;align-items:center;gap:5px;
  padding:0 10px;border-bottom:var(--bw) solid var(--line-2);
}
.themecard .sw i{display:block;border-radius:99px;flex:none}
.themecard .sw i.a{width:20px;height:20px}
.themecard .sw i.b{width:13px;height:13px}
.themecard .sw i.c{width:9px;height:9px}
.themecard .sw .bar{position:absolute;right:9px;top:50%;transform:translateY(-50%);
  width:34%;height:7px;border-radius:99px}
.themecard .tn{
  font-size:12.5px;font-weight:700;padding:8px 10px 2px;color:var(--ink);
  display:flex;align-items:center;gap:6px;
}
.themecard[aria-checked="true"] .tn::after{
  content:"";width:6px;height:6px;border-radius:99px;background:var(--accent);flex:none;
}
.themecard .td{font-size:10.5px;color:var(--ink-3);padding:0 10px 10px;line-height:1.45}

/* ══ responsive ═══════════════════════════════════════════════════════ */
@media (min-width:640px){
  /* Side by side once there is room. A full-width strip of art on a desktop
     leaves the board stranded in the middle of a very wide, very short frame;
     given a column of its own it is nearly square and can actually be seen. */
  .gamecard{display:grid;grid-template-columns:minmax(0,40%) minmax(0,1fr);align-items:stretch}
  .cardart{
    height:100%;min-height:238px;
    border-bottom:0;border-right:var(--bw) solid var(--line-2);
  }
  .cardbody{justify-content:center;padding:22px 24px}
  /* In a column this narrow the visibility toggle always wraps below the
     buttons, and a wrapped control pinned to the right edge reads as though it
     belongs to something else. */
  .gamecard .gvis{margin-left:0}

  .roomgrid{grid-template-columns:minmax(0,.92fr) minmax(0,1.06fr) minmax(0,1.06fr)}
  /* Three short cards pinned to the top of a tall screen look abandoned — but
     only once they sit side by side. `safe` so a short landscape window falls
     back to top-aligned instead of centring the overflow out of reach;
     browsers without it drop the declaration and land on the same fallback. */
  #s-room{justify-content:safe center}
}
@media (max-width:639px){
  :root{--strip:22px;--gap:6px}
  #app{padding-left:12px;padding-right:12px}
  .gravity{padding:0;gap:10px}
  .gravity .glabel{max-width:60px;font-size:9.5px}
  /* thumbs, not cursors */
  .gpad{grid-template-columns:repeat(3,54px);grid-template-rows:repeat(2,46px)}
  .gbtn svg{width:24px;height:24px}
  /* The board is capped by whichever runs out first: the width of the phone,
     the height LEFT OVER once everything else on the screen has had its share,
     or 520px.

     That middle term used to be a flat 46dvh, and on a 360px-wide phone the
     width term (278px) always won — so the height cap, which exists precisely
     to keep this screen on one screen, never bound at all. Nothing then
     stopped the column growing past the fold as the turn bar gained a row per
     extra player, and at 360x640 with three players three of the four tip
     buttons sat entirely below it. The tip pad is how you play Shift; it
     cannot be the part that scrolls off.

     --chrome is measured by js/app.js, because the thing that varies is the
     turn bar and only the turn bar knows how tall it is. It is written once
     per render and never read back into the height it describes, so there is
     no loop. If it is missing — app.js not loaded, an old cached module — the
     fallback is the flat cap this replaced, which was merely too generous
     rather than broken. */
  .pad{--bs:min(calc(100vw - 2*(var(--strip) + var(--gap)) - 26px),
                calc(100dvh - var(--chrome,54dvh)),
                520px)}
  .cardbody{padding:14px 15px 15px}
  .sheet{
    top:auto;left:0;right:0;bottom:0;width:100%;max-height:88dvh;
    border-left:0;border-top:var(--bw) solid var(--line);
    border-radius:var(--r-3) var(--r-3) 0 0;
    animation-name:sheetUp;
  }
  @keyframes sheetUp{from{transform:translateY(22px);opacity:0}to{transform:none;opacity:1}}
}
@media (max-width:380px){
  /* Two pixels off each of the row's five gaps is ten pixels back for the
     name column, which is exactly what "Intermediate" was short of here. */
  .seatlist{--seat-gap:8px}
  .topright{gap:6px}
  .themegrid{grid-template-columns:1fr}
  .gvis{margin-left:0;width:100%;justify-content:space-between}
}

/* Honoured two ways: the OS setting, and the explicit "off" in Settings —
   which exists because the OS setting is all-or-nothing and someone may want
   the discs to stop falling on this site alone. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.01ms!important;animation-iteration-count:1!important;
    animation-delay:0ms!important;transition-duration:.01ms!important;
  }
}
html[data-motion="off"] *,
html[data-motion="off"] *::before,
html[data-motion="off"] *::after{
  animation-duration:.01ms!important;animation-iteration-count:1!important;
  animation-delay:0ms!important;transition-duration:.01ms!important;
}
