@charset "UTF-8";

/* ---------------------------------------------------------------- the face

   gg sans, self-hosted, so the shell renders in it for every visitor rather
   than only for the ones who happen to have it installed. One family, five
   weights, each with its italic; the monospaced cut is a second family and is
   not wired into the shell's stack — it is here because the pack ships it.

   Weights follow the file names: 400 regular, 500 medium, 600 semibold,
   700 bold, 800 extrabold. `swap` keeps text visible on the fallback while
   the face arrives, which matters most on the message river.
   ---------------------------------------------------------------------- */

@font-face { font-family: "gg sans"; font-style: normal; font-weight: 400; font-display: swap;
             src: url("/assets/fonts/ggsans.woff2") format("woff2"); }
@font-face { font-family: "gg sans"; font-style: italic; font-weight: 400; font-display: swap;
             src: url("/assets/fonts/ggsansitalic.woff2") format("woff2"); }
@font-face { font-family: "gg sans"; font-style: normal; font-weight: 500; font-display: swap;
             src: url("/assets/fonts/ggsansmedium.woff2") format("woff2"); }
@font-face { font-family: "gg sans"; font-style: italic; font-weight: 500; font-display: swap;
             src: url("/assets/fonts/ggsansmediumitalic.woff2") format("woff2"); }
@font-face { font-family: "gg sans"; font-style: normal; font-weight: 600; font-display: swap;
             src: url("/assets/fonts/ggsanssemibold.woff2") format("woff2"); }
@font-face { font-family: "gg sans"; font-style: italic; font-weight: 600; font-display: swap;
             src: url("/assets/fonts/ggsanssemibolditalic.woff2") format("woff2"); }
@font-face { font-family: "gg sans"; font-style: normal; font-weight: 700; font-display: swap;
             src: url("/assets/fonts/ggsansbold.woff2") format("woff2"); }
@font-face { font-family: "gg sans"; font-style: italic; font-weight: 700; font-display: swap;
             src: url("/assets/fonts/ggsansbolditalic.woff2") format("woff2"); }
@font-face { font-family: "gg sans"; font-style: normal; font-weight: 800; font-display: swap;
             src: url("/assets/fonts/ggsansextrabold.woff2") format("woff2"); }
@font-face { font-family: "gg sans"; font-style: italic; font-weight: 800; font-display: swap;
             src: url("/assets/fonts/ggsansextrabolditalic.woff2") format("woff2"); }
@font-face { font-family: "gg mono"; font-style: normal; font-weight: 400; font-display: swap;
             src: url("/assets/fonts/ggsansmono.woff2") format("woff2"); }
@font-face { font-family: "gg mono"; font-style: normal; font-weight: 700; font-display: swap;
             src: url("/assets/fonts/ggsansmonobold.woff2") format("woff2"); }

/* ================================================================
   donutdice — the client shell

   The site is a chat client: a server rail, a channel list, a message
   river, a member list, and a composer. Play happens inside a channel,
   so a round in progress is a card in the river that everyone watching
   the channel sees move.

   This sheet loads after theme.css and dd.css and takes the page over.
   It borrows those files' palette (--secondary is still the lilac accent)
   and replaces their page chrome, which was built for a scrolling
   document rather than an app that owns the viewport.
   ================================================================ */

:root {
  /* The shell's greys. Darker than the panels in theme.css and stepped so
     the four columns read as depth without a single border between them. */
  --sh-rail: #131316;
  --sh-side: #1b1c1f;
  --sh-main: #232428;
  --sh-raise: #2a2b30;
  --sh-embed: #2c2d33;
  /* Width of a card's left accent bar. Towers draws a meter in the same strip and offsets
     itself out by exactly this, so the two read as one bar — see .sh_tower_meter. */
  --sh-embed-bar: 4px;
  --sh-line: #34353b;
  /* board tiles: the card's stand-in for the game screen's --bg / --border pair */
  --sh-tile: #26272c;
  --sh-tile-line: #3a3b42;
  /* A box in a game's stake panel — the amount, a count, a difficulty. One step *down* from
     the panel around it (#26272c, see .sh_shelf_body .dd_panel) so it reads as recessed
     into the panel. The game screens' own --bg is a near-black, which is right on a black
     page and reads as a hole punched in the panel here. */
  --sh-field: #1e1f23;
  --sh-text: #e3e4e8;
  --sh-dim: #9a9ca5;
  --sh-mute: #6a6c75;
  /* Listing greys — the app-directory scale. A heading white above body text that is
     lighter than --sh-mute, because a description that has to be *read* cannot sit at
     the weight of a timestamp. The three row greys are one step apart so rest, hover
     and press are distinguishable without a border between them. */
  --sh-head: #f2f3f5;
  --sh-sub: #b5bac1;
  --sh-row: #2b2d31;
  --sh-row-hover: #35373c;
  --sh-row-active: #3f4147;
  --sh-rail-w: 68px;
  --sh-side-w: 236px;
  --sh-members-w: 232px;
  --sh-head-h: 48px;
  /* How a settled round reads. The loss side is the board's own red (--mine, see
     theme.css) so the bar, the figure a loss colours, and the tile that ended the round
     are all one colour. */
  /* A win is the accent lilac, the same colour the payout figure takes — the site has
     one colour for "you won" and this is it. */
  --sh-win: var(--secondary);
  --sh-lose: var(--mine);
  --sh-lose-soft: var(--mine);
  /* The shell's face. The rest of the site is monospace and stays that way — this
     is the chat client, and a chat client is set in a UI sans. The stack falls back
     the same way the client this one is shaped like does. */
  --sh-font: "gg sans", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
  /* The same family's own mono cut, for a figure that has to hold a column against the ones
     above and below it — the stats down a profile card. It is a *type of the main font*, not
     a second face, so the shell stays set in one thing while those numbers still line up.
     It is deliberately not used in the game interfaces: see the note above #game_root. */
  --sh-mono: "gg mono", "gg sans", ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* theme.css sets font-family on the universal selector, which breaks inheritance, so
   the shell has to re-set it per element. Icon fonts are excluded by class rather than
   listed, so a new Font Awesome glyph anywhere in here keeps working. */
body.chat_app,
body.chat_app *:not([class*="fa-"]) { font-family: var(--sh-font); }

/* …and back to mono for two card parts that are not figures: the title, and the slot
   reels, whose glyphs are drawn on the mono's metrics. The card's *numbers* — the stat
   figures, the big result, the pips and the board cells — are set in the shell's sans
   instead; gg sans carries tabular figures, so those columns still line up at the same
   size and weight without the mono. Declared after the rule above on purpose — the two
   carry the same specificity, so source order decides. */
body.chat_app .sh_reel,
body.chat_app .sh_embed_title { font-family: var(--font); }

/* ---------------------------------------------------------------- game interfaces
   The interfaces in the shelf (#game_root, built by assets/game.js and the per-game
   modules) used to be the one part of the client still set in the site's mono, because
   they were lifted whole from the standalone game pages. They are not a page any more —
   they are a panel inside a chat client — and a bet panel in a different face to the
   channel it posts into read as an embedded application rather than as part of the app.

   So the whole interface is in the shell's face now, and the *variety* comes out of the
   family instead of out of a second one: gg sans ships 400/500/600/700/800 with italics, and
   each part of the panel is given the cut that says what it is. No part of it takes the
   family's mono cut — a mono is a mono whichever family it comes from, and a panel with one
   in it reads as the old mono panel. Tabular figures do that job instead.
   The rules below are the whole mapping — nothing under #game_root should be naming a
   face of its own.

     labels, small caps-ish furniture ..... 600, tracked
     typed figures (the stake box) ........ 500
     quick-stake chips .................... 500
     the action button .................... 700, tracked
     the round box's figures .............. 700, its labels 600
     board figures ........................ 500–600
     the big result ....................... 800
     asides and notes ..................... 400 italic

   …and every figure anywhere in it is tabular (the block below).

   dd.css names var(--font) on several of these; every rule here is scoped to
   #game_root and so outweighs it, and the standalone pages that share that stylesheet
   (wallet, fair, auth) are untouched — they are still mono pages and stay that way. */
body.chat_app #game_root,
body.chat_app #game_root *:not([class*="fa-"]) {
  font-family: var(--sh-font);
  /* Every figure in a game panel is read against another figure — a stake against a
     balance, a row's return against the row above it — so they hold a column throughout. */
  font-variant-numeric: tabular-nums;
}

/* The only exception, and it is a drawn thing rather than type: a slot reel's symbol is
   drawn on the mono's metrics the same way the embed's is. */
body.chat_app #game_root .dd_reel { font-family: var(--font); }

/* Labels. A sans at .18em is a row of loose letters where the mono was a row of stamps, so
   the tracking comes in as the weight goes up — same job, said in the new face. */
body.chat_app #game_root .dd_field label,
body.chat_app #game_root .round_stat span,
body.chat_app #game_root .keno_pay span {
  font-weight: 600;
  letter-spacing: .1em;
}

/* The stake box, and every other box a number is typed into.
   This was the family's mono cut for a while, on the theory that digits being typed are
   where proportional figures are felt — the box shifting under the caret as 1.00 becomes
   11.11. It was the wrong trade: the amount is the biggest number on the panel, a mono is a
   mono whichever family it belongs to, and setting it in one left the interface looking like
   it had never been changed. The sans, one step up in weight so a typed figure carries more
   than the label above it, and tabular figures (inherited from the block above) to hold the
   column the mono was there for. */
body.chat_app #game_root .dd_input,
body.chat_app #game_root .dd_select {
  font-weight: 500;
  letter-spacing: .01em;
}

/* ½ 2× min max — a row of small controls, one step above the label that names the box. */
body.chat_app #game_root .dd_chip { font-weight: 500; }

/* The one thing on the panel that does something. Heaviest cut in the interface, which is
   also what it is: bet, or cash out. */
body.chat_app #game_root .dd_btn {
  font-weight: 700;
  letter-spacing: .12em;
}

/* The round box, drawn as the channel embed draws the same two figures — so it takes the
   same weight the embed's stats do rather than a lighter one. */
body.chat_app #game_root .round_stat b { font-weight: 700; }

/* Board figures: a tower row's return, a plinko bucket, a keno prize. Set a step above the
   panel's body so a figure inside a tile reads as a value and not as a caption. */
body.chat_app #game_root .dd_tile,
body.chat_app #game_root .dd_bucket,
body.chat_app #game_root .round_strip b,
body.chat_app #game_root .roul_pay_mult,
body.chat_app #game_root .roul_pocket { font-weight: 600; }
body.chat_app #game_root .roul_pay_name { font-weight: 500; }

/* The big one — a dice roll, a limbo crash. The extrabold cut, because at 40px in the
   middle of the stage it is the result and there is nothing else to read. */
body.chat_app #game_root .dd_result .big {
  font-weight: 800;
  letter-spacing: .01em;
}

/* Asides: the panel's own note line, and a game's explainer under its paytable. Italic 400
   is the family's way of saying "this is commentary", which is exactly what both are. */
body.chat_app #game_root .dd_meta,
body.chat_app #game_root .dd_note {
  font-style: italic;
  font-weight: 400;
}
body.chat_app #game_root .dd_meta b { font-style: normal; font-weight: 600; }

/* ---- the boxes in the stake panel -------------------------------------------------------
   Every control the player fills in or picks from, on the left of any game: the amount, a
   mine count, a dice target, a plinko risk, a towers difficulty. They were filled with the
   game screens' --bg, a near-black that belongs to a black page — inside a grey panel in a
   grey app it read as a hole punched through the card rather than as a field sitting in it.
   --sh-field is one step down from the panel instead: still clearly recessed, still clearly
   the thing you type into, but part of the same surface as everything around it.

   Scoped to the shelf, like every rule in this section. wallet.html, fair.html and auth.html
   share dd.css and are still black pages — their fields stay as they are. */
body.chat_app #game_root .dd_input,
body.chat_app #game_root .dd_select { background: var(--sh-field); }

/* ---- the panel, in slide's grammar ------------------------------------------------------
   The stake panel on every game reads the way slide's does: captions in slide's quiet
   lowercase, rounded boxes, the quick chips as a filled row under the field, and the
   outlined rounded button. All presentation — the controls and their ids are untouched. */
body.chat_app #game_root .dd_field label {
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--sh-mute);
  margin-bottom: 6px;
}
body.chat_app #game_root .dd_input,
body.chat_app #game_root .dd_select {
  border-radius: 6px;
  border-color: var(--sh-line);
  /* Sans, not the family's mono cut. The mapping above already says the interface takes no
     mono anywhere; this shorthand was quietly re-setting the family and winning on source
     order, which is why the shelf panels still read as the old mono ones while slide's did
     not. (slide's own .sl_input names the mono too, but it is a bare class and the shell's
     `body.chat_app *` rule outranks it, so slide has always painted in the sans — this is
     what makes the two match rather than what makes them differ.) */
  font: 600 14px/1.2 var(--sh-font);
  padding: 10px 12px;
}
body.chat_app #game_root .dd_input:focus,
body.chat_app #game_root .dd_select:focus { border-color: var(--secondary); }

/* The quick-stake rail: slide's chips — filled, rounded, sharing the row evenly. */
body.chat_app #game_root .dd_quick { display: flex; gap: 6px; margin: 8px 0 14px; }
body.chat_app #game_root .dd_quick .dd_chip { flex: 1 1 auto; margin: 0; }
body.chat_app #game_root .dd_chip {
  background: var(--sh-row);
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--sh-sub);
  font: 600 12px/1 var(--sh-font);
  padding: 8px 6px;
  text-align: center;
  transition: background .12s ease, color .12s ease, border-color .12s ease;
}
body.chat_app #game_root .dd_chip:hover { background: var(--sh-row-hover); color: var(--sh-head); }
body.chat_app #game_root .dd_chip:active { background: var(--sh-row-active); }
body.chat_app #game_root .dd_chip.on {
  border-color: var(--secondary);
  color: var(--secondary);
  background: color-mix(in srgb, var(--secondary) 7%, var(--sh-row));
}
/* The segmented control in the shelf: the same track the fields are recessed into, no
   outer border of its own, and the sliding highlight lit the way a picked chip is —
   accent border, accent letters, a breath of the accent under them. */
body.chat_app #game_root .dd_seg {
  background: var(--sh-row);
  border-color: transparent;
}
body.chat_app #game_root .dd_seg_hl {
  background: color-mix(in srgb, var(--secondary) 7%, var(--sh-row));
}
body.chat_app #game_root .dd_seg_opt {
  color: var(--sh-sub);
  font: 600 12px/1 var(--sh-font);
  letter-spacing: 0;
  padding: 10px 6px;
}
body.chat_app #game_root .dd_seg_opt:hover:not(:disabled):not(.on) { color: var(--sh-head); }
body.chat_app #game_root .dd_seg_opt.on { color: var(--secondary); }
/* Two groups under one label — roulette's colour over its parity — read as one control
   split in two, so they sit tighter to each other than the field does to the next field. */
body.chat_app #game_root .dd_seg + .dd_seg { margin-top: 6px; }

/* The button is slide's: outlined, rounded, plain words instead of tracked-out caps. */
body.chat_app #game_root .dd_btn {
  border-radius: 6px;
  font: 700 14px/1 var(--sh-font);
  letter-spacing: 0;
  text-transform: none;
  padding: 14px;
  transition: background .12s ease, color .12s ease, opacity .12s ease;
}
body.chat_app #game_root .dd_btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--secondary) 16%, transparent);
  color: var(--secondary);
}
body.chat_app #game_root .dd_btn.cashout { border-radius: 6px; }

/* What the bet is worth: the multiplier at the left edge, the money at the right, a short
   hairline between them. No caption, and no plate behind the figures — the label and the
   patches of panel colour that used to break a full-width rule around them are gone. */
body.chat_app #game_root .dd_pays {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 2px 0 12px;
  min-height: 18px;
}
body.chat_app #game_root .dd_pays_m {
  flex: 0 0 auto;
  white-space: nowrap;
  font: 700 13px/1 var(--sh-font);
  color: var(--sh-sub);
  font-variant-numeric: tabular-nums;
  transition: color .35s ease;
}
/* The hairline between them: short, centred in the space the two figures leave, and the
   first thing to give when they grow — it shrinks to nothing before either figure wraps. */
body.chat_app #game_root .dd_pays_rule {
  flex: 0 1 24px;
  min-width: 0;
  height: 1px;
  margin: 0 auto;
  background: var(--sh-line);
}
body.chat_app #game_root .dd_pays_v {
  flex: 0 0 auto;
  white-space: nowrap;
  font: 700 14px/1 var(--sh-font);
  color: var(--sh-head);
  font-variant-numeric: tabular-nums;
  transition: color .35s ease;
}
body.chat_app #game_root .dd_pays_m.pos,
body.chat_app #game_root .dd_pays_v.pos { color: var(--secondary); }
/* The multiplier takes slide's five tiers, so a figure gets warmer the higher it reads —
   on a climb (mines, towers) it warms as the round goes, and on a one-step game it lands on
   the tier it paid. Restated at this specificity because the bare `.sl_t*` rules lose to the
   selector above. The glow from t3 up is slide's, kept smaller: this is a 13px figure on a
   bare panel, not a box on a strip. It is the only glow in the panel — nothing here glows a
   box, and the money beside it never glows at all. */
body.chat_app #game_root .dd_pays_m.sl_t0 { color: var(--sh-dim); }
body.chat_app #game_root .dd_pays_m.sl_t1 { color: var(--sl-t1); }
body.chat_app #game_root .dd_pays_m.sl_t2 { color: var(--sl-t2); }
body.chat_app #game_root .dd_pays_m.sl_t3 {
  color: var(--sl-t3);
  text-shadow: 0 0 10px color-mix(in srgb, var(--sl-t3) 55%, transparent);
}
body.chat_app #game_root .dd_pays_m.sl_t4 {
  color: var(--sl-t4);
  text-shadow: 0 0 12px color-mix(in srgb, var(--sl-t4) 65%, transparent);
}
body.chat_app #game_root .dd_pays_m.sl_t5 {
  color: var(--sl-t5);
  text-shadow: 0 0 14px color-mix(in srgb, var(--sl-t5) 75%, transparent);
}
body.chat_app #game_root .dd_pays_m.muted,
body.chat_app #game_root .dd_pays_v.muted { color: var(--sh-mute); }
/* A bust: the figures the round died holding, held in the board's own red for a beat
   before game.js runs them down to the resting $0.00 (see loseRoundBox). */
body.chat_app #game_root .dd_pays_m.neg,
body.chat_app #game_root .dd_pays_v.neg { color: var(--mine); }

/* ---- the boards in the shelf ------------------------------------------------------------
   Every tile you click — mines, towers, keno — was drawn with the game screens' --bg and
   --border, a near-black square with hard corners. Inside the grey shelf that read as a hole
   in the panel, and it did not match the same board on the embed, which the card draws as a
   grey rounded cell (.sh_cell). The embed is the shape the player sees first, so the shelf
   follows it: the tile takes the card's tile pair and the card's 8px radius.

   Done by redefining --bg / --border on the tile rather than restating background and
   border-color, so every state dd.css derives from them — .mine, .mine_hint, .spent, the
   keno resting border, the keno fill animation — re-mixes against the grey on its own, and
   the game screens on wallet.html/fair.html keep the black board they were drawn for. */
body.chat_app #game_root .dd_tile {
  --bg: var(--sh-tile);
  --border: var(--sh-tile-line);
  border-radius: 8px;
}
/* The one state dd.css fills from --surface rather than --bg: a keno number that was
   drawn and not picked. It is pushed back to .16 opacity so it barely shows, but what
   shows through is the page's near-black — on the shelf that is the wrong floor. */
body.chat_app #game_root .dd_keno .dd_tile.drawn { background: var(--sh-tile); }

/* theme.css scales the whole document up for reading a long page. An app that
   owns the viewport has to stay at 1:1 or the columns stop fitting. */
html { zoom: 1 !important; background: var(--sh-main); }

/* The scanline overlay and the raining-ascii field belong to the document
   pages. Behind four opaque columns they are a repaint nobody can see. */
body.chat_app::before { display: none !important; }
body.chat_app .ascii_field { display: none !important; }

body.chat_app {
  margin: 0;
  padding: 0 !important;
  height: 100vh;
  /* dvh, where it is understood, is the one that survives a mobile browser's
     collapsing address bar; the vh above stays as the fallback. */
  height: 100dvh;
  overflow: hidden;
  display: flex;
  background: var(--sh-main);
  color: var(--sh-text);
  font-size: 14px;
  line-height: 1.45;
}

body.chat_app ::-webkit-scrollbar { width: 8px; height: 8px; }
body.chat_app ::-webkit-scrollbar-thumb { background: #16171a; border-radius: 4px; }
body.chat_app ::-webkit-scrollbar-thumb:hover { background: #101114; }

/* ---------------------------------------------------------------- rail */

/* The rail is hidden while the site has one server (see app.html); `display: flex` below
   would otherwise beat the `hidden` attribute. */
.sh_rail[hidden] { display: none; }

.sh_rail {
  width: var(--sh-rail-w);
  flex: 0 0 var(--sh-rail-w);
  background: var(--sh-rail);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 12px 0;
  /* Tooltips pop out past the rail's right edge, so the rail cannot clip.
     Four icons never outgrow the column, so losing the scrollbar costs nothing. */
  overflow: visible;
  position: relative;
  z-index: 10;
}

/* The pill that grows on the left of the active icon — the one piece of
   Discord's navigation grammar that carries all of its state. */
.sh_guild {
  position: relative;
  width: 48px;
  height: 48px;
  flex: 0 0 48px;
  border: 0;
  border-radius: 16px;
  background: var(--sh-side);
  color: var(--sh-text);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-radius .16s ease, background .16s ease, color .16s ease;
  font-size: 17px;
  padding: 0;
}
.sh_guild:hover { border-radius: 15px; background: var(--secondary); color: #17151d; }
.sh_guild.on { border-radius: 15px; background: var(--secondary); color: #17151d; }
.sh_guild::before {
  content: "";
  position: absolute;
  left: -14px;
  top: 50%;
  width: 4px;
  height: 8px;
  margin-top: -4px;
  border-radius: 0 4px 4px 0;
  background: var(--sh-text);
  opacity: 0;
  transform: scaleY(.3);
  transition: opacity .16s ease, transform .16s ease, height .16s ease;
}
.sh_guild:hover::before { opacity: 1; transform: scaleY(1); }
.sh_guild.on::before { opacity: 1; transform: scaleY(1); height: 32px; margin-top: -16px; }
.sh_guild .sh_guild_mark { width: 26px; height: 26px; }

/* The name popout beside a hovered icon — Discord's tooltip, minus the caret.
   ::before is the pill, so the tooltip takes the button's other pseudo-element. */
.sh_guild::after {
  content: attr(data-tip);
  position: absolute;
  left: calc(100% + 14px);
  top: 50%;
  transform: translateY(-50%) scale(.96);
  background: #111214;
  color: #f2f3f5;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0;
  white-space: nowrap;
  padding: 7px 12px;
  border-radius: 8px;
  box-shadow: 0 8px 16px rgba(0, 0, 0, .35);
  opacity: 0;
  pointer-events: none;
  transition: opacity .1s ease, transform .1s ease;
  z-index: 100;
}
.sh_guild:hover::after { opacity: 1; transform: translateY(-50%) scale(1); }
.sh_rail_rule { width: 32px; height: 2px; background: #26272b; border-radius: 1px; flex: 0 0 2px; }

/* ---------------------------------------------------------------- sidebar */

.sh_side {
  width: var(--sh-side-w);
  flex: 0 0 var(--sh-side-w);
  background: var(--sh-side);
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.sh_side_head {
  height: var(--sh-head-h);
  flex: 0 0 var(--sh-head-h);
  display: flex;
  align-items: center;
  padding: 0 16px;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .04em;
  box-shadow: 0 1px 0 rgba(0, 0, 0, .25);
  cursor: default;
}
.sh_side_head .sh_wordmark { color: var(--sh-text); }
.sh_side_head .sh_dot { color: var(--secondary); }
/* A named server (not the donutdice home) gets a bigger head. */
.sh_side_head .sh_wordmark.server_name { font-size: 17px; letter-spacing: .01em; }

.sh_channels { flex: 1 1 auto; overflow-y: auto; padding: 12px 8px 8px; min-height: 0; }

.sh_cat {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 14px 8px 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--sh-mute);
  cursor: pointer;
  user-select: none;
}
.sh_cat:hover { color: var(--sh-text); }
.sh_cat_add { margin-left: auto; font-size: 15px; line-height: 1; padding: 0 2px; }

.sh_chan {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--sh-dim);
  padding: 7px 8px;
  margin: 1px 0;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
  text-align: left;
  transition: background .1s ease, color .1s ease;
}
.sh_chan:hover { background: #26272c; color: var(--sh-text); }
.sh_chan.on { background: #303136; color: #fff; }
.sh_chan_hash { color: var(--sh-mute); font-size: 17px; line-height: 1; flex: 0 0 auto; }
.sh_chan.on .sh_chan_hash { color: var(--sh-dim); }
.sh_chan_name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sh_chan_badge {
  margin-left: auto;
  background: var(--secondary);
  color: #17151d;
  font-size: 10px;
  font-weight: 700;
  border-radius: 8px;
  padding: 1px 6px;
  flex: 0 0 auto;
}

/* ------------------------------------------------- the bottom-left panel */

/* Balance lives here, where Discord puts the account. It is the same chip the
   rest of the site uses (theme.css .wallet_box), re-seated: hovering reads the
   balance in LTC, clicking opens the wallet. */
.sh_me {
  flex: 0 0 auto;
  background: #151619;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.sh_me_balance {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px 7px 40px;
  border-radius: 6px;
  background: #1d1e22;
  border: 1px solid transparent;
  cursor: pointer;
  transition: border-color .14s ease, background .14s ease;
}
.sh_me_balance:hover { border-color: var(--secondary); background: #212227; }

/* theme.css sizes .wallet_box for a 212px floating chip in a topbar. In the
   panel it is the whole row, so the absolute positioning is undone here. */
body.chat_app .wallet_box {
  position: static;
  width: auto;
  height: auto;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  flex: 1 1 auto;
  min-width: 0;
}
body.chat_app .wallet_box:hover { border-color: transparent; }
body.chat_app .wallet_icon { left: 12px; margin-top: -13px; }
body.chat_app .wallet_value { font-size: 15px; text-align: left; }
body.chat_app .wallet_num { text-align: left; }
body.chat_app .wallet_irl { text-align: left; font-size: 8px; }

/* The account row is the handle for your own profile card, so it lights up the way a
   clickable row does and the whole strip left of the buttons is the target. The buttons
   sit outside it: signing out is not "look at my profile". */
.sh_me_row { display: flex; align-items: center; gap: 8px; padding: 0 2px; }
.sh_me_open {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  padding: 3px 4px;
  margin: -3px -4px;
  border-radius: 4px;
  text-align: left;
  cursor: pointer;
  transition: background .1s ease;
}
.sh_me_open:hover, .sh_me_open.on { background: var(--sh-row-hover); }
.sh_me_avatar {
  width: 32px;
  height: 32px;
  flex: 0 0 32px;
  border-radius: 50%;
  background: var(--secondary);
  color: #17151d;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 13px;
  position: relative;
}
.sh_me_avatar::after {
  content: "";
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: #23a55a;
  border: 2.5px solid #151619;
}
.sh_me_id { min-width: 0; flex: 1 1 auto; }
.sh_me_name {
  font-size: 13px;
  font-weight: 600;
  color: var(--sh-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sh_me_sub { font-size: 11px; color: var(--sh-mute); }
.sh_me_acts { display: flex; gap: 2px; }
.sh_icon_btn {
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--sh-dim);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  transition: background .1s ease, color .1s ease;
}
.sh_icon_btn:hover { background: #2a2b30; color: var(--sh-text); }

/* ---------------------------------------------------------------- main */

/* theme.css styles the bare `main` element for a centred article: 96px of top padding
   and a 1400px cap. The shell's main is a viewport column, not an article, so all three
   of those are undone here — without this the channel floats in the middle of a wide
   monitor with black down both sides. */
.sh_main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--sh-main);
  padding: 0;
  margin: 0;
  max-width: none;
}

.sh_head {
  height: var(--sh-head-h);
  flex: 0 0 var(--sh-head-h);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 16px;
  box-shadow: 0 1px 0 rgba(0, 0, 0, .2);
  z-index: 2;
}
.sh_head_hash { color: var(--sh-mute); font-size: 22px; line-height: 1; }
.sh_head_name { font-weight: 700; font-size: 15px; }
.sh_head_topic {
  color: var(--sh-mute);
  font-size: 12px;
  padding-left: 10px;
  margin-left: 8px;
  border-left: 1px solid var(--sh-line);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sh_head_acts { margin-left: auto; display: flex; gap: 2px; }

.sh_body { flex: 1 1 auto; display: flex; min-height: 0; }
/* app.js hides the whole chat surface on a table channel with the `hidden` attribute, and
   `display: flex` above beats the UA's `display: none` — without this the board opened
   underneath a still-visible channel instead of replacing it. */
.sh_body[hidden] { display: none; }
.sh_stream { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }

.sh_river {
  flex: 1 1 auto;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 16px 0 8px;
  min-height: 0;
}

.sh_river_top { padding: 24px 16px 8px; }
.sh_river_top h2 { font-size: 28px; font-weight: 800; margin: 0 0 6px; letter-spacing: -.01em; }
.sh_river_top p { color: var(--sh-dim); font-size: 14px; margin: 0; }

/* ---------------------------------------------------------------- messages */

/* The row grid is one measurement repeated: a 40px gutter, a 16px gap, then the body.
   Every row in the river uses it — avatar rows, continuation rows, and system lines —
   which is what makes a column of messages line up down its left edge. */
.sh_msg { padding: 2px 16px; display: flex; gap: 16px; position: relative; }
.sh_msg:hover { background: rgba(2, 2, 2, .18); }
.sh_msg.lead { margin-top: 16px; }

.sh_avatar {
  width: 40px;
  height: 40px;
  flex: 0 0 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 15px;
  color: #17151d;
  margin-top: 2px;
  user-select: none;
}
/* Continuation rows keep the 40px gutter but show the timestamp on hover, so a
   run of messages from one person reads as one block. The line box is the text's
   own 22px so the stamp sits on the first line rather than above it, and it never
   wraps — "1:47 AM" folding in half is what made the column look crooked. */
.sh_msg .sh_gutter {
  width: 40px;
  flex: 0 0 40px;
  font-size: 11px;
  line-height: 22px;
  color: var(--sh-mute);
  text-align: right;
  white-space: nowrap;
  opacity: 0;
  user-select: none;
}
.sh_msg:hover .sh_gutter { opacity: 1; }

.sh_msg_body { min-width: 0; flex: 1 1 auto; }
.sh_msg_head { display: flex; align-items: baseline; gap: 8px; line-height: 22px; }
.sh_author { font-size: 16px; font-weight: 500; color: var(--sh-text); }
.sh_author.me { color: var(--secondary); }
.sh_stamp { font-size: 12px; color: var(--sh-mute); font-weight: 500; }
.sh_text {
  font-size: 16px;
  line-height: 22px;
  color: #dbdce1;
  word-wrap: break-word;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}
/* theme.css desaturates every <img> on the site — that is the house look, and it is
   wrong for emoji, which are the one image on the page whose whole job is colour. */
img.sh_emoji,
img.sh_emoji_img { filter: none; }

/* Twemoji images, sized to the line so they sit in running text like glyphs would. */
.sh_text .sh_emoji { width: 22px; height: 22px; vertical-align: -5px; object-fit: contain; }
/* A message that is nothing but emoji renders them large, as chat clients do. The row
   has to grow with them: at the message line-height a 48px image spills over its
   neighbours instead of sitting in a box of its own. */
.sh_text.jumbo { line-height: 1.15; }
.sh_text.jumbo .sh_emoji { width: 48px; height: 48px; vertical-align: -8px; }

/* Indented onto the same text column as everything else: 16px padding + 40px gutter
   + 16px gap. */
.sh_sys { padding: 4px 16px 4px 72px; font-size: 14px; line-height: 22px; color: var(--sh-mute); font-style: italic; }

/* ---------------------------------------------------------------- embeds */

/* The card a round lives in. The bar down its left side is the whole state: lilac while
   the round is open, then it travels — green when it paid, red when it did not. The travel
   is the point, so it is a transition rather than a swap, and app.js hands a settling card
   its old tone for one frame to give the change something to run from.
   The bar is the only thing that takes the result's colour. The card behind it stays the
   grey it always was; a tinted card turns a 4px signal into a coloured rectangle.

   Width is fit-content, so a card is the size of the board it is reporting on: a two-tile
   tower gets a narrow card instead of the same 460px slab a 40-square keno board needs. */
.sh_embed {
  margin-top: 6px;
  /* No floor. `fit-content` already resolves to the widest thing on the card, and the
     footer is a nowrap flex row, so its own max-content is one of the candidates — a
     card can never come out narrower than its seed-and-nonce line. The 272px minimum
     that used to be here was doing nothing for the footer and a lot to the boards: a
     210px mines grid sat in a 236px well, so the card had 20px of air on the left of the
     board and 42px on the right, and the whole thing read as mis-centred. */
  width: fit-content;
  max-width: min(460px, 100%);
  background: var(--sh-embed);
  border-radius: 6px;
  border-left: var(--sh-embed-bar) solid var(--sh-line);
  /* The right padding carries the 4px the accent bar takes off the left, so the gap the
     eye sees is 20px on both sides of the board. Symmetry here is what puts the footer
     flush with the grid, and `verify` — right-aligned in that footer — lands centred on
     the last column of tiles. */
  padding: 12px 20px 12px 16px;
  animation: sh_embed_in .22s cubic-bezier(.2, .8, .3, 1);
  transition: border-left-color .55s ease;
}
@keyframes sh_embed_in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
/* Staked, but nothing has happened in it yet — see the note in buildEmbed. The bar keeps
   the neutral grey it is built with and travels to the accent on the first safe click. */
.sh_embed.pending { border-left-color: var(--sh-line); }
.sh_embed.open { border-left-color: var(--secondary); }
/* A card patched in place is not arriving; it is the card that was already there. */
.sh_embed.no_intro { animation: none; }
.sh_embed.win { border-left-color: var(--sh-win); }
.sh_embed.loss { border-left-color: var(--sh-lose); }
/* Towers replaces the bar with a bottom-up meter (below) instead of a single colour flip
   while a climb is live or won — the bar is hidden rather than never drawn, so a loss
   still gets the plain flip to red every other game's card gets. */
.sh_embed.tower_meter { border-left-color: transparent; }
/* app.js parks a settling card here for one frame to set where the change starts from. */
.sh_embed.tone_hold,
.sh_embed.tone_hold .sh_stat b,
.sh_embed.tone_hold .sh_big { transition: none; }
@media (prefers-reduced-motion: reduce) { .sh_embed { transition: none; } }

.sh_embed_head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.sh_embed_title { font-size: 15px; font-weight: 700; letter-spacing: .01em; }

/* Three equal columns across the card rather than three blocks pushed together, so the
   middle one — mult — lands over the footer's single centred slot. A grid rather than a
   flex row because the columns have to hold those positions whatever the figures in them
   happen to be wide; a settled 0.00x must not shift the row.

   The board is the centred thing on a card; everything written in words starts at the left
   of its own column, so the title and the figures all begin on the same line down the
   side. This is buildEmbed's own row, so every game's card is laid out this way. */
.sh_embed_stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px 12px;
  margin-bottom: 10px;
}
/* The row is centred; a column inside it is not. Label and figure share a left edge, so
   "bet" sits over the "$" and not over the middle of "$1.00" — the group is what is
   centred on the card, not each word in its own slot. */
.sh_stat { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; text-align: left; }
/* The labels never take the result's colour — they are furniture, and a row of red
   words reads as an error rather than a loss. Only the figures move. */
.sh_stat span { font-size: 11px; color: var(--sh-mute); letter-spacing: .04em; }
.sh_stat b {
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--sh-text);
  transition: color .55s ease;
}
/* Tone comes from the card, not from the figure, so a figure built while the round was
   still open has somewhere to travel from when the card settles.
   A loss colours exactly one number — the stake, because on a loss that is what the
   stake became. The multiplier stays the grey it has always been: 0.00x is not news,
   and two red figures side by side read as an error rather than a result. */
.sh_embed.win .sh_stat b.pos { color: var(--secondary); }
/* .loss is in this list for the one figure on a lost card that is not a loss: on a game
   you climb out of, the mult column keeps the high-water mark the round reached rather
   than blanking to 0.00x (see buildEmbed), and a figure that does not change its reading
   when the card settles does not change its colour either. The payout beside it is still
   the grey nothing. */
.sh_embed.win .sh_stat b.acc,
.sh_embed.loss .sh_stat b.acc,
.sh_embed.open .sh_stat b.acc { color: var(--secondary); }
/* A card whose board is still playing out has no result to report yet: the bar keeps
   the neutral grey it is built with, the stake reads as the plain figure it is, and the
   multiplier and payout stay muted until the last number has landed. */
.sh_embed.revealing .sh_stat b.pos,
.sh_embed.revealing .sh_stat b.neg,
.sh_embed.revealing .sh_stat b.acc { color: var(--sh-mute); }
/* 0.00x on a round that has not moved yet: the same grey the settled zero takes, so the
   figure changes colour when it changes meaning and not before. */
.sh_stat b.muted { color: var(--sh-mute); }

/* ---- the coin reading ----

   Every money figure on a card is fiat. Hovering one says what that amount is in the coin
   it was actually staked in, in a bubble above the figure.

   Out of flow on purpose: "0.01048576 LTC" is three times the width of "$1.23", and
   stacking the two readings in the same slot would size every money column to the coin
   string and shove the stats row about at rest for the sake of a hover. Absolute means the
   row keeps the width its fiat figures give it.

   `color: inherit` is the whole trick. The bubble hangs off the <b>, so it is drawn in
   whatever colour that figure is wearing right now — lilac on a win, red on a lost stake,
   grey while a board is still revealing — and it travels through the settling transition
   with the figure rather than needing a rule per tone. */
/* No `cursor: help`: the coin bubble is a passive second reading of the figure, not a
   definition being offered, and a question mark trailing the pointer over every money
   column in the river reads as an error state. The pointer stays as it is. */
.sh_stat b[data-coin] { position: relative; }
/* The coin's mark, in front of the figure. It is a child of the <b>, not a sibling, so the
   badge and the digits are one hover target and one bubble — and so the mark inherits the
   line the figure sits on instead of needing the row re-laid around it. Baseline-nudged
   rather than flex-centred because the stats row aligns on text, and a flex <b> would drop
   the figure a pixel off the labels beside it. */
.sh_stat_coin {
  display: inline-block;
  vertical-align: -1px;
  margin-right: 4px;
  /* The disc is art, not a letter: it does not take the win/loss colour the figure wears,
     and it does not fade with the `lost` tone either — a Litecoin badge in red is a
     different coin. Held a touch back so the figure still leads. */
  opacity: .85;
}
.sh_stat_coin .coin_mark { display: block; }
.sh_stat b[data-coin]::after {
  content: attr(data-coin);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%) translateY(3px);
  z-index: 3;
  padding: 4px 8px;
  border-radius: 5px;
  background: var(--sh-raise);
  border: 1px solid var(--sh-line);
  box-shadow: 0 4px 14px rgba(0, 0, 0, .38);
  color: inherit;
  font-size: 11.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity .13s ease, transform .13s ease;
}
.sh_stat b[data-coin]:hover::after,
.sh_stat b[data-coin]:focus-visible::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .sh_stat b[data-coin]::after { transition: opacity .1s linear; transform: translateX(-50%); }
  .sh_stat b[data-coin]:hover::after { transform: translateX(-50%); }
}
.sh_embed.loss .sh_stat b.neg { color: var(--sh-mute); }
.sh_embed.loss .sh_stat b.lost { color: var(--sh-lose-soft); }
@media (prefers-reduced-motion: reduce) { .sh_stat b { transition: none; } }

/* One centred slot, and it holds one of two things — see the note above .sh_embed_cash.
   Fixed height, on the row and on both of the things that can stand in it, because the
   whole point of the crossfade below is that the card does not move: with the height left
   to the content, the button's own padding and the word's differed by a pixel or two and
   the card grew a sliver taller the moment it was pressed.

   The row is not drawn at all when there is nothing to put in it, which is most cards —
   a rule and 19px of space under the stats, saying nothing, is worse than no footer. */
.sh_embed_foot {
  display: grid;
  grid-template-columns: 1fr;
  justify-items: center;
  align-items: center;
  height: 22px;
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px solid rgba(255, 255, 255, .06);
  box-sizing: content-box;
  font-size: 11px;
  color: var(--sh-mute);
}
.sh_embed_foot:empty { display: none; }
/* The middle of the footer is where a round is decided, and afterwards where it says so.
   Four states of one slot, drawn to the same box so any of them can turn into any other:

     .sh_embed_wait    — "make one move", grey, on a round of your own that has not been
                         played yet. Not a button: there is nothing here to press, the move
                         is on the board above, and this is the footer saying why it is not
                         offering to cash out yet.
     .sh_embed_cash    — the button, on an open round, for its own player only.
     .sh_embed_cashed  — the word, in green, once the round is settled and was walked away
                         from.
     .sh_embed_again   — "replay bet", on your own settled loss.

   Green rather than the card's lilac --sh-win: the lilac is what "this round paid" already
   looks like everywhere on the card, and this word is saying the other thing — that the
   player chose to stop. --success is the one green in the palette (theme.css).

   Both are text and nothing else — no edge, no fill, no radius. The row they sit in is grey
   furniture on a grey card, and a boxed control there was the loudest thing in the footer for
   no reason: green words on their own already read as the one thing here that can be pressed.

   They are written as one box — same fixed height, same padding, same 11px — so nothing about
   the card's size depends on which of them is standing here. That is what the fixed height is
   for: left to the content, the button's padding and the word's differed by a pixel and the
   card grew a sliver taller the moment it was pressed. */
.sh_embed_foot .sh_embed_cash,
.sh_embed_foot .sh_embed_cashed,
.sh_embed_foot .sh_embed_wait,
.sh_embed_foot .sh_embed_again {
  display: grid;
  place-items: center;
  height: 22px;
  padding: 0 6px;
  border: 0;
  background: transparent;
  font-size: 11px;
  font-family: inherit;
  line-height: 1;
}
/* A round with nothing on it yet. Deliberately the quietest thing on the card — a step below
   --sh-mute, which is already what the stat labels are set in: this is not a figure, not a
   control, and not news. It is here so the footer's rule is drawn from the moment the round
   is staked and the card does not change height the first time a tile is turned.
   No pointer treatment at all, because there is nothing here to press. */
.sh_embed_foot .sh_embed_wait {
  color: #55575e;
  font-weight: 400;
  letter-spacing: .07em;
  cursor: default;
  user-select: none;
}
.sh_embed_foot .sh_embed_cashed {
  /* 400 rather than lighter: gg sans ships no thin cut, and a synthesised one would be a
     blurrier 400. Beside the 500 the labels above are set in, it is the light thing here. */
  font-weight: 400;
  letter-spacing: .07em;
  color: var(--success);
  /* It fades in rather than appearing: the card is rebuilt when the round settles, so
     without this the word is simply there in the same frame the board stops moving. The
     rise is a couple of pixels — enough to read as arriving, not as an entrance. */
  animation: sh_foot_in .42s ease both;
}
/* …except when the player pressed the button on this very client, where the word is
   already on screen and has been since the click. Fading it in again would blink it. */
.sh_embed_cashed.instant { animation: none; }
/* Shared by both of the things that can arrive in this slot when a round ends — the word on
   a cash-out, the button on a loss. One keyframe, so a round that settles either way resolves
   its footer at the same speed and in the same direction. */
@keyframes sh_foot_in {
  from { opacity: 0; transform: translateY(2px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .sh_embed_cashed { animation: none; }
}

/* The button: the two words, in the green they are about to leave behind, and nothing around
   them. Hover lights the text itself to white — the same grammar the `?` in the header uses,
   and the only thing that moves is the colour of the letters.

   `position: relative` because the word it turns into is its own ::after, laid over the
   label so the two can cross without either one moving. */
.sh_embed_foot .sh_embed_cash {
  position: relative;
  cursor: pointer;
  color: var(--success);
  letter-spacing: .04em;
}
.sh_embed_cash span {
  color: var(--success);
  transition: color .14s ease, opacity .18s ease, transform .24s ease;
}
.sh_embed_foot .sh_embed_cash:hover span,
.sh_embed_foot .sh_embed_cash:focus-visible span { color: #fff; }
.sh_embed_foot .sh_embed_cash:focus-visible { outline: none; }
.sh_embed_cash::after { transition: opacity .18s ease, transform .24s ease; }
/* The word, in place, waiting. Letter-spaced to .07em so what settles here is exactly what
   .sh_embed_cashed will be when the card is rebuilt. */
.sh_embed_cash::after {
  content: "cashed";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-weight: 400;
  letter-spacing: .07em;
  opacity: 0;
  transform: translateY(3px);
}
/* Pressed: the label leaves upwards and the word rises into its place. What is left is the
   green text, which is exactly what the settled card will draw here a moment later. */
.sh_embed_cash.cashing { cursor: default; }
/* …and it stops answering the pointer, so a cursor still resting on it does not hold the
   departing label at white while it goes. */
.sh_embed_foot .sh_embed_cash.cashing span { color: var(--success); }
.sh_embed_cash.cashing span { opacity: 0; transform: translateY(-3px); }
.sh_embed_cash.cashing::after { opacity: 1; transform: none; transition-delay: .1s; }
@media (prefers-reduced-motion: reduce) {
  .sh_embed_cash span, .sh_embed_cash::after { transition: opacity .1s linear; }
  .sh_embed_cash.cashing span, .sh_embed_cash.cashing::after { transform: none; }
}

/* "replay bet": the round again, offered where a lost one ends.
   Grey rather than a colour, and lighting to white on hover — the same grammar the `?` in
   the header uses. A loss card is already carrying red on the bar, the stake and the tile
   that ended it, and a coloured control under all of that would read as a fifth result
   rather than as the one thing left to do.

   It arrives on the same fade-and-rise the cashed word does, and for the same reason: the
   round ending is the moment this appears, and a button that is simply *there* in the frame
   the board stops moving reads as something that was always on the card. On a bust the slot
   it rises into is the one the cash-out button just left, so what the player sees is the
   footer changing its mind rather than the card losing a row and growing another. */
.sh_embed_foot .sh_embed_again {
  cursor: pointer;
  color: var(--sh-mute);
  font-weight: 400;
  letter-spacing: .05em;
  transition: color .14s ease, opacity .18s ease;
  animation: sh_foot_in .42s ease both;
}
.sh_embed_foot .sh_embed_again:hover,
.sh_embed_foot .sh_embed_again:focus-visible { color: var(--sh-text); }
.sh_embed_foot .sh_embed_again:focus-visible { outline: none; }
/* Pressed: it stops answering and fades back, until the fresh card arrives underneath it.
   The old card is not replaced by the new round — it stays where it is, settled — so this
   is the only thing on screen that can say the click was taken.
   `animation: none` is what lets the fade happen at all: the arrival above is filled `both`,
   so without dropping it the held final keyframe would keep opacity pinned at 1 and this
   rule would do nothing. */
.sh_embed_foot .sh_embed_again.again_busy {
  cursor: default;
  color: var(--sh-mute);
  opacity: .45;
  animation: none;
}
@media (prefers-reduced-motion: reduce) {
  .sh_embed_foot .sh_embed_again { animation: none; }
}

/* The way into the round's fairness record: a question mark at the top right of the card,
   opposite the game's name, where the coin mark used to sit.
   `verify game` used to be two words along the footer. They were the same two words on
   every card, they were the widest thing on that row, and they were competing with the one
   control that is actually a decision. A mark in the corner asks the question the words were
   answering — what happened, and can I check it — in one glyph.

   Grey furniture at rest, and on hover it is the *mark* that lights, not a box around it: no
   fill, no border, nothing that reads as a button growing under the pointer. The disc it
   sits in is only there to give the mark a target bigger than a 10px glyph. */
.sh_embed_fair {
  margin-left: auto;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 17px;
  height: 17px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--sh-mute);
  font-family: inherit;
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  transition: color .14s ease, transform .14s ease;
}
.sh_embed_fair:hover,
.sh_embed_fair:focus-visible {
  color: var(--secondary);
  /* A shade larger as it lights, so the change reads as the mark answering rather than as a
     colour swap on a static glyph. Small enough that nothing around it moves. */
  transform: scale(1.15);
  outline: none;
  background: transparent;
}

/* -- the card on the clipboard ------------------------------------------- */

/* A card is a picture, and a picture selects as gibberish — forty loose digits and a
   stray "nonce". So nothing visible on a card is selectable, and .sh_copy carries the
   round in one readable line instead. It is clipped rather than hidden, because
   display:none and visibility:hidden both leave the selection, and the selection is the
   entire point: Ctrl+A, Ctrl+C, Ctrl+V puts the round wherever text goes. */
.sh_embed { position: relative; }
.sh_embed > * { user-select: none; -webkit-user-select: none; }
.sh_copy {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  user-select: text;
  -webkit-user-select: text;
}

/* The meter that stands in for towers' accent bar (buildEmbed / layoutTowerMeter in app.js):
   a strip where the border-left would be, filled from the bottom of the card up to the top
   edge of the highest row the player has cleared, and to the top on a cash-out.

   The track spans the whole card; only the fill is board-aligned. So the bar reads as the
   accent bar every other card has — full height, same place, same width — and the lilac in
   it is the climb. Its height is set in px from app.js, because the mark it has to stand on
   is the position of a row and the card has a header above the board and stats and a footer
   below it: no percentage of the card lands on a row edge. */
.sh_tower_meter {
  position: absolute;
  /* Absolute offsets resolve against the padding box, and the border sits outside it, so
     the offset back out is the border's own width. Kept as a variable because the two have
     to agree: --sh-embed-bar is also what .sh_embed's border-left-width is set from. */
  top: 0;
  bottom: 0;
  left: calc(-1 * var(--sh-embed-bar));
  width: var(--sh-embed-bar);
  /* Matches the card's own border-radius so the bar is the card's edge, not a tab stuck to
     it. Clips the fill, which is what keeps the fill's square bottom corner inside it. */
  border-radius: 6px 0 0 6px;
  background: var(--sh-line);
  overflow: hidden;
}
.sh_tower_meter_fill {
  position: absolute;
  /* Anchored bottom-left-right and grown by height alone: transitioning `height` on an
     element whose bottom is pinned is the one form of this that does not need a transform,
     so there is no blurred edge from a scaled layer and no rounding drift on hi-dpi. */
  left: 0;
  right: 0;
  bottom: 0;
  height: 0;
  background: var(--secondary);
  transition: height .4s cubic-bezier(.3, .7, .4, 1);
}
/* Held for one frame while layoutTowerMeter puts the fill back at its previous height, so
   that jump is invisible and only the grow to the new height animates. */
.sh_tower_meter_fill.hold { transition: none; }
@media (prefers-reduced-motion: reduce) { .sh_tower_meter_fill { transition: none; } }

/* -- boards inside a card ------------------------------------------------ */

/* A card is a small copy of the table it is reporting on, so the board here is drawn
   in the same visual language as the live game screen (dd.css): outline-first tiles,
   sharp edges, one accent (--secondary) for everything that went the player's way, and
   --mine reserved for the single tile that ended the round. The only thing that differs
   is the fill, because a card sits on --sh-embed rather than the page black. */
/* Towers stacks one of these per row, so the margin here is the gap between rows and is
   kept to the grid's own gap — anything more and an eight-row tower grows a card taller
   than the board it is drawing. */
/* `auto` on the sides for the one case where the card is wider than its board: a hard
   tower is two tiles across and the footer underneath it is not, so left-aligning it hung
   a narrow column off the left edge of the card the footer had sized. Where the board is
   the widest thing on the card — mines, keno — the card is already sized to it and auto
   resolves to zero. */
.sh_grid { display: grid; gap: 5px; margin: 0 auto 8px; }
/* Cells are shaped like the small filled buttons a real message component row is made of
   — 32px tall whatever the card's width makes them wide, and rounded to 8px — so a board
   in the river reads as the same object the bot posts rather than as a square grid. */
.sh_cell {
  height: 38px;
  border: 1px solid var(--sh-tile-line);
  border-radius: 8px;
  background: var(--sh-tile);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 700;
  color: var(--sh-mute);
  transition: border-color .12s ease, background .18s ease, color .18s ease, transform .18s ease;
}
/* Cells carry no glyph, so a revealed one is outline plus a wash of its own colour —
   at 34px an outline alone is too quiet to pick out of a grid. Safe and hit are filled
   to the same strength: a filled square next to an unfilled one reads as two different
   kinds of thing when it is meant to read as the same tile in the other state. */
.sh_cell.safe {
  border-color: var(--secondary); color: var(--secondary);
}
/* the only red on the board — see the --mine note in theme.css */
.sh_cell.hit {
  border-color: var(--mine); color: var(--mine);
  background: color-mix(in srgb, var(--mine) 16%, var(--sh-tile));
}
/* A towers tile is a mines tile: the same 38px height, so the two boards read as the same
   object rather than as two different games' furniture. The only difference is width — the
   row is sized to a wider tile in app.js (TOWER_TILE_W) because a tile here carries a money
   figure, and at mines width a four-figure sum has nowhere to sit. */
.sh_grid.tower { margin-bottom: 5px; }
/* Tower rows are separate grids, so the gap between them is the margin above rather than
   the grid's own; it matches the in-row gap so the ladder reads as one block of tiles. */
/* The figure on a tile is what cashing out standing on that row would return. It is the
   smaller of the two type sizes on the card on purpose: the tile is the thing being
   pointed at, and a number that filled it would read as a button label. */
.sh_grid.tower .sh_cell {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
  padding: 0 1px;
  overflow: hidden;
}
/* On a towers tile the colour is the tile's, not the figure's: a lit tile already says
   safe or hit with its border and its wash, and tinting the money on top of that made the
   one number the player is reading the hardest thing on the board to read. The fill stays,
   the figure stays white. */
.sh_grid.tower .sh_cell.safe, .sh_grid.tower .sh_cell.hit { color: var(--sh-text); }
.sh_cell.pick { border-color: var(--secondary); color: var(--secondary); }
.sh_cell.faded { opacity: .35; }
/* A mine on a row the climb never reached, after it ended on one — the same half-strength
   tint the game screen uses (.dd_tile.mine_hint in dd.css), read through the .faded the
   row already carries. Deliberately not .hit: the tile the player actually clicked stays the
   loudest thing on the board. */
.sh_cell.mine_hint {
  border-color: color-mix(in srgb, var(--mine) 60%, var(--sh-tile-line));
  background: color-mix(in srgb, var(--mine) 32%, var(--sh-tile));
}
/* A tile on a row the player has already climbed past and did not stand on. Solid, at full
   strength, and deliberately a shade above the resting tile: the rows behind the player are
   the board's floor, and it is against them that the dimmed rows above read as untouched. */
.sh_cell.spent {
  opacity: 1;
  background: color-mix(in srgb, var(--sh-text) 13%, var(--sh-tile));
  border-color: color-mix(in srgb, var(--sh-text) 20%, var(--sh-tile-line));
}
/* the row a live towers climb is standing on, matching .dd_row_tiles.current */
/* Its figures are the only ones on the board at full strength: that row is the decision in
   front of the player, and every row above it is what the decision after this one is worth.
   The rows above are already pushed back by .faded, so they grey out with their tiles. */
.sh_grid.current .sh_cell {
  border-color: color-mix(in srgb, var(--sh-text) 28%, var(--sh-tile-line));
  color: var(--sh-text);
}
/* A tile on your own open round that is still a move: the card's board is the game, not a
   picture of it (see embedMove in app.js). It rests exactly as an untouched tile does —
   lighting the whole board would say something has happened on it — and answers the pointer
   the way the panel's own tiles do, brightening its outline and lifting a pixel.
   Only ever on tiles this client's own player may click; the server re-checks anyway. */
.sh_cell.live { cursor: pointer; }
.sh_cell.live:hover {
  border-color: color-mix(in srgb, var(--secondary) 55%, var(--sh-tile-line));
  background: color-mix(in srgb, var(--secondary) 9%, var(--sh-tile));
  transform: translateY(-1px);
}
.sh_cell.live:active { transform: none; }
/* A move already in flight. The board stops taking clicks until the server's answer lands —
   the same drop the panel's `busy` makes — and says so by going quiet rather than by
   disappearing, so the round is not seen to leave the screen between two picks. */
.sh_embed.moving .sh_cell.live { cursor: default; }
.sh_embed.moving .sh_cell.live:hover {
  border-color: var(--sh-tile-line);
  background: var(--sh-tile);
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .sh_cell.live:hover { transform: none; }
}
.sh_cell.pop { animation: sh_cell_pop .26s cubic-bezier(.2, .9, .3, 1.4); }
@keyframes sh_cell_pop { from { transform: scale(.72); } to { transform: scale(1); } }

/* -- the climb ----------------------------------------------------------- */
/* One step of a towers climb: the row being left runs down and the row being arrived at
   runs up, over the same 180ms, so the ladder moves once instead of blinking twice. See
   markMovedRow() in app.js for why these are `from`-only keyframes — the card is rebuilt
   on every click, so there is no previous value to transition from, and each animation
   instead runs from a stated start into whatever the cell's own resting style is. That is
   also what makes clicking through a tower as fast as you can stay clean: every click gets
   a fresh node, so nothing has to be cancelled or restarted mid-flight.
   The tile that was actually clicked is excluded — it has its own pop, and it is turning
   its colour rather than dimming out. */
.sh_grid.tower.dimming .sh_cell:not(.safe):not(.hit) {
  animation: sh_row_dim .18s cubic-bezier(.4, 0, .6, 1);
}
@keyframes sh_row_dim {
  from { opacity: 1; border-color: var(--secondary); color: var(--sh-text); }
}
.sh_grid.tower.lighting .sh_cell {
  animation: sh_row_lit .18s cubic-bezier(.4, 0, .6, 1);
}
@keyframes sh_row_lit {
  from { opacity: .35; border-color: var(--sh-tile-line); color: var(--sh-mute); }
}
@media (prefers-reduced-motion: reduce) {
  .sh_grid.tower.dimming .sh_cell, .sh_grid.tower.lighting .sh_cell { animation: none; }
}

/* keno: the 8-column board the game screen uses, shrunk to card width. The three
   states are the game screen's — picked, drawn, and a hit where the two meet. */
/* An explicit width, not a max-width: the card is sized to fit its board, so the board
   has to state a size the card can be measured against. A max-width alone leaves the
   grid with the intrinsic width of the digits in it and the whole card collapses. */
.sh_keno { display: grid; grid-template-columns: repeat(8, 1fr); gap: 4px; margin: 0 auto 6px; width: 336px; max-width: 100%; }
.sh_keno .sh_cell { font-size: 12px; font-weight: 600; }

/* The board rests white — the same border and text as a live towers row (.sh_grid.current)
   — so forty legible numbers read as one object rather than as a dark field with a few
   lit squares in it. Against that floor the states subtract: a number that came out and
   was not theirs drops out of the board entirely, and the numbers they picked are the
   only lilac on it. */
.sh_keno .sh_cell {
  opacity: 1;
  border-color: color-mix(in srgb, var(--sh-text) 28%, var(--sh-tile-line));
  color: var(--sh-text);
}
/* their selection: outline and figure only. The fill is reserved for a hit, so a picked
   number and a hit number are not two shades of the same filled square. */
.sh_keno .sh_cell.picked {
  border-color: var(--secondary);
  color: var(--secondary);
  background: var(--sh-tile);
}
/* a drawn number the player was not on: it loses the white and is pushed back further
   than the resting board, so the draw reads as numbers leaving rather than arriving */
.sh_keno .sh_cell.drawn {
  opacity: .12;
  border-color: var(--sh-tile-line);
  color: var(--sh-mute);
  background: var(--sh-raise);
  animation: sh_drawn .25s steps(3);
}
/* a hit — their number, and it came out. This is the one square that fills, and it fills
   as it lands: the outline they have been looking at floods with its own colour. */
.sh_keno .sh_cell.picked.drawn {
  opacity: 1;
  background: var(--secondary);
  border-color: var(--secondary);
  color: var(--sh-embed);
  animation: sh_keno_fill .32s cubic-bezier(.2, .9, .3, 1.2);
}
@keyframes sh_drawn { from { opacity: 1; } to { opacity: .12; } }
@keyframes sh_keno_fill {
  0%   { background: var(--sh-tile); color: var(--secondary); transform: scale(1); }
  45%  { background: var(--secondary); color: var(--sh-embed); transform: scale(1.14); }
  100% { background: var(--secondary); color: var(--sh-embed); transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .sh_keno .sh_cell.drawn, .sh_keno .sh_cell.picked.drawn { animation: none; }
}

/* keno prize boxes — the row under the board, the same boxes the game screen puts below
   its divider (.keno_pay in dd.css), at card width. One per reachable hit count; the
   one the round is on lights as the numbers land. The row is exactly as wide as the
   board above it, so the two read as one object. */
.sh_keno_pays {
  display: flex; flex-wrap: wrap; gap: 4px;
  width: 336px; max-width: 100%; margin: 0 0 8px;
}
.sh_pay {
  flex: 1 1 62px; min-width: 62px; padding: 5px 4px; text-align: center;
  border: 1px solid var(--sh-tile-line); background: var(--sh-tile);
  transition: border-color .18s ease, background .18s ease, opacity .18s ease;
  opacity: .5;
}
.sh_pay span {
  display: block; font-size: 9.5px; letter-spacing: .04em; color: var(--sh-mute);
}
.sh_pay b {
  display: block; margin-top: 2px; font-size: 11.5px; font-weight: 600;
  color: var(--sh-dim); font-variant-numeric: tabular-nums;
}
/* the hit count the round has reached */
.sh_pay.on {
  opacity: 1;
  border-color: var(--secondary);
  background: color-mix(in srgb, var(--secondary) 12%, var(--sh-tile));
}
.sh_pay.on span, .sh_pay.on b { color: var(--secondary); }
@media (prefers-reduced-motion: reduce) { .sh_pay { transition: none; } }

/* Same rule as the figures above: the card's tone colours it, so it travels too. The
   palette is the board's, not the bar's — lilac for what went the player's way. */
.sh_big {
  font-size: 34px;
  font-weight: 800;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  margin: 2px 0 8px;
  color: var(--sh-text);
  transition: color .55s ease;
}
.sh_embed.win .sh_big.win { color: var(--secondary); }
.sh_embed.loss .sh_big.loss { color: var(--sh-mute); }
@media (prefers-reduced-motion: reduce) { .sh_big { transition: none; } }

.sh_reels { display: flex; gap: 8px; margin: 2px 0 8px; }
.sh_reel {
  width: 52px;
  height: 62px;
  border: 1px solid var(--sh-tile-line);
  background: var(--sh-tile);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 26px;
  color: var(--sh-text);
}
.sh_reel.win { border-color: var(--secondary); color: var(--secondary); }

.sh_chips { display: flex; flex-wrap: wrap; gap: 4px; margin: 2px 0 8px; }
.sh_pip {
  min-width: 26px;
  height: 26px;
  padding: 0 6px;
  border: 1px solid var(--sh-tile-line);
  background: var(--sh-tile);
  color: var(--sh-mute);
  font-size: 12px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
}
.sh_pip.on { border-color: var(--sh-dim); color: var(--sh-text); }
.sh_pip.hit {
  border-color: var(--secondary);
  color: var(--secondary);
  background: color-mix(in srgb, var(--secondary) 10%, var(--sh-tile));
}

/* -- the same board, on the bet slip ------------------------------------- */

/* The slip (.bet_modal in dd.css) is opened from a card's `?` and is about the round that
   card is reporting, so it shows that round's board — the same markup, drawn by the same
   function, registered from app.js. Only three things have to change for it to sit in a
   modal instead of on a card, and they are all here:

     - it is centred. On a card the board is the widest thing on it and the card is sized
       to fit, so a left-aligned flex row needs no help; the slip is a fixed 420px and a
       four-tile tower or a row of plinko pips hung off its left edge.
     - it is shorter. An eight-row tower at card height is 344px of tiles, which pushes the
       proof material off the bottom of a laptop screen; the tiles come down to 28px, and
       the slip scrolls if a board still cannot fit.
     - it carries its own tone. A board says part of its result in colour, and on a card
       that colour comes off the card's state class — the slip has no card, so the host
       node carries win/loss and these rules read it instead.

   Everything else — tile colours, states, the reserved --mine — is inherited from the card
   rules above, deliberately: one board, one description of it. */
.bet_board { margin: 0 0 16px; }
.bet_board[hidden] { display: none; }
/* Boards that lay out as a flex row are left-aligned for a card sized to them. */
.bet_board .sh_reels,
.bet_board .sh_chips,
.bet_board .sh_keno_pays { justify-content: center; margin: 0 auto 8px; }
.bet_board .sh_big { text-align: center; margin: 0 0 6px; }
/* The tone the card would have carried, on the host node — see drawBoard in dd.js. */
.bet_board.win .sh_big.win { color: var(--secondary); }
.bet_board.loss .sh_big.loss { color: var(--sh-mute); }
/* Tiles come down from the card's 38px. A card is as tall as it likes in a river that
   scrolls; the slip has the proof material under the board and every pixel the board does
   not take is a pixel of it that stays on screen. 30px is the smallest a 5×5 grid reads as
   a board at, and a tower — eight rows of it, the one board that really has to give up
   height — goes two lower again, with its figures following the tiles down so a four-digit
   sum still sits inside one. */
.bet_board .sh_cell { height: 30px; }
.bet_board .sh_grid.tower .sh_cell { height: 28px; font-size: 9.5px; }

/* Which is also why the slip can scroll. It is `overflow: hidden` in dd.css for its
   rounded corners, which is right on a card of figures and wrong the moment a board is in
   it: on a short viewport the seeds and the rotate button would simply be unreachable. The
   horizontal axis stays hidden, so the corners still clip. */
.bet_modal { max-height: 92vh; overflow-y: auto; }

/* ---------------------------------------------------------------- shelf */

/* The activity: a launched game, docked above the composer the way Discord
   docks one over a channel. It is the live surface; the cards above are what
   everyone else sees of it. */
.sh_shelf {
  /* A popover over the river, anchored to the composer it sits in — the same anchoring
     .sh_pop uses for the emoji and activity popups. In the stream's flow it was a row,
     and a row spans the channel however narrow the game inside it is, which left a band
     of empty shelf beside the game and squeezed the river. Out of flow there is no row
     to leave empty: the window is exactly `--shelf-w` wide (the game's own width, set
     under the shelf body), hugs the composer's right edge under the activity button, and
     grows upward over the river instead of pushing it.

     Below .sh_pop's z-index on purpose: the activity popup is what launched the game and
     it stays on top of the window it opened. */
  position: absolute;
  right: calc(16px / var(--shelf-zoom));
  bottom: 100%;
  z-index: 30;
  margin-bottom: calc(8px / var(--shelf-zoom));
  width: min(var(--shelf-w, 520px), calc(100% - 32px));
  box-shadow: 0 10px 28px rgba(0, 0, 0, .5);
  transform-origin: bottom right;
  background: var(--sh-raise);
  border-radius: 8px;
  border: 1px solid var(--sh-line);
  overflow: hidden;
  /* Per-game, like --shelf-w below and for the same reason: how much of the screen the
     window is allowed is a fact about what is in it. 62vh suits a game whose panel is a
     control or two; mines raises it, because the bet column alone is 330px and a window
     that cannot hold its own controls scrolls, which is the one thing this surface must
     not do. */
  max-height: calc(var(--shelf-h, 62vh) / var(--shelf-zoom));
  display: flex;
  flex-direction: column;
  animation: sh_shelf_in .24s cubic-bezier(.2, .8, .3, 1);
  /* The interface is read at arm's length from the composer, and every figure on it — the
     stake, the multiplier, the numbers on a keno tile — was set at the size a full-width
     page gave it. It is presented a quarter larger than it is authored, as one zoom on the
     window rather than as 1.25× written into forty lengths across this file and game.css:
     the geometry inside stays the geometry the boards were designed to, and the per-game
     widths below stay the numbers their comment explains.

     `zoom` and not `transform: scale()`, because the window has to *take* the room it is
     given: a transform would leave a 570px hole in the layout with a 712px picture drawn
     over it, hanging off the composer and clipping at the viewport. Zoom is in the layout,
     so the shelf still hugs the composer's edge and still wraps and scrolls correctly.

     Percentages resolve against the containing block in the zoomed element's own units, so
     they need no correction — `width: min(--shelf-w, 100% - 32px)` still fits the stream.
     Absolute lengths do scale, so the three that place the window rather than fill it —
     its offset, its gap above the composer, and its height cap — are divided back out,
     leaving the window in the same place at the same height with a larger game in it. */
  --shelf-zoom: 1.25;
  zoom: var(--shelf-zoom);
}
/* Out of the corner the activity button is in, rather than straight up off the composer. */
@keyframes sh_shelf_in { from { opacity: 0; transform: translateY(10px) scale(.96); } to { opacity: 1; transform: none; } }
.sh_shelf[hidden] { display: none; }

.sh_shelf_head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  background: #202126;
  border-bottom: 1px solid var(--sh-line);
  flex: 0 0 auto;
}
/* The game's name, and nothing else. It is the only label on the surface, so it is set
   at the size a title gets rather than the size a caption gets. */
.sh_shelf_name {
  font-weight: 700;
  font-size: 20px;
  letter-spacing: -.01em;
  line-height: 1.1;
}
.sh_shelf_acts { margin-left: auto; display: flex; gap: 4px; }
/* Big and thin. `.sh_icon_btn` is sized for a 14px icon-font glyph; a bare ✕ at that size
   reads as a stray character, so it takes the header's own scale and the lightest weight
   the stack has, and the box grows to keep it on a 32px target. */
.sh_shelf_x {
  width: 32px;
  height: 32px;
  font-size: 22px;
  font-weight: 200;
  line-height: 1;
  /* The glyph's own side bearings are uneven; nudging it up sits it on the name's optical
     centre instead of a little below it. */
  padding-bottom: 2px;
}
/* `overflow-x: clip` is load-bearing, not tidiness. With only `overflow-y: auto` set, the
   other axis computes from `visible` to `auto` — that is the rule, not a quirk — so the
   body was a horizontal scroll container too, and anything that drew a pixel wide of it
   put a scrollbar along its bottom edge. The win pop does exactly that: its entrance
   keyframe overshoots to `scale(1.1)` on a box that is `inset: 0` on the stage panel, so
   for the third of a second the animation ran, the body's scroll width went 659 → 668 and
   a bar flicked in and out under the board. Nothing in a game panel is ever meant to be
   scrolled sideways — the boards are capped to fit — so the axis is clipped outright.
   Written as `clip` for intent; beside an `auto` it computes to `hidden`, which is the
   same picture. Either way no bar is ever painted along the bottom. */
.sh_shelf_body { padding: 12px; overflow-y: auto; overflow-x: clip; min-height: 0; }

/* The game shell (assets/game.js) renders into #game_root. Its own stylesheet
   assumes a full-width page, so the shelf re-proportions it. */
.sh_shelf_body .dd_grid { grid-template-columns: 186px 1fr; gap: 10px; }
.sh_shelf_body .dd_panel { background: #26272c; border-color: #34353b; padding: 10px; }
.sh_shelf_body .dd_panel.stage_panel { min-height: 0; }
/* Roulette's strip runs off both edges into the panel behind it, so the colour it fades
   from has to be that panel's own — dd.css assumes the near-black of a standalone page,
   which reads as two black holes cut into this grey one. Restated wherever the panel is
   painted a different grey (the popped-out game window, below). */
.sh_shelf_body .roul_win { --roul-fade: #26272c; }
.sh_shelf_body .dd_btn { margin-top: 0; }

/* Per-game widths.
   The bet column is a fixed 186px, so what varies is the board beside it. Each figure
   below is that board at the size it is meant to be read at, plus the column, the 10px
   gap and the body's 12px padding — nothing here is a round number for its own sake:
     dice / limbo / slots — the stage is one big figure or three reels, so the board wants
       no more room than the panel it sits in.
     mines / towers — square-ish grids, capped by .dd_board / .towers_board. Mines is
       the widest of the two because the board is the entire interface — twenty-five
       tiles and nothing else — so the window is sized for the board and the board is
       then given nearly all of it (see the pair of rules below).
     keno — eight columns; below ~360px of board the numbers turn into stamps.
     plinko — sixteen rows of pegs; the widest board in the set.
     roulette — the 560px strip is the whole board now that the bet boxes under it are
       gone, and it wants all of that: the tiles are the result, and a strip cut to half
       its width shows two pockets either side of the marker instead of four. */
.sh_shelf[data-game="dice"]     { --shelf-w: 470px; }
.sh_shelf[data-game="limbo"]    { --shelf-w: 440px; }
/* 480 did not fit: three 92px reels plus their gaps are 284px of board, and with the bet
   column and the padding around it that wants 552. The shortfall used to show up as a
   horizontal scrollbar along the body — the third reel was off the edge until you dragged
   it into view — and clipping that axis (see .sh_shelf_body) turned an ugly bar into a
   missing reel. So the window is the width the board actually needs. */
.sh_shelf[data-game="slots"]    { --shelf-w: 560px; }
.sh_shelf[data-game="mines"]    { --shelf-w: 660px; --shelf-h: 72vh; }
/* Towers wants mines' window: its tiles are now mines tiles a little wider (see
   .dd_row_tiles in dd.css), so the board needs the same width and even more height —
   eight rows to mines' five — and below the extra headroom the board gives, not the
   window (the height-budget cap on .towers_board below). */
.sh_shelf[data-game="towers"]   { --shelf-w: 660px; --shelf-h: 80vh; }
.sh_shelf[data-game="keno"]     { --shelf-w: 600px; }
.sh_shelf[data-game="plinko"]   { --shelf-w: 640px; }
.sh_shelf[data-game="roulette"] { --shelf-w: 824px; }

/* The boards' own caps assume a full-width page; inside the window they should fill the
   stage they are given instead of centring in a column wider than they are. */
.sh_shelf_body .dd_board,
.sh_shelf_body .dd_keno { max-width: none; }
.sh_shelf_body .towers_stage { max-width: none; }
/* The towers tile is defined as the mines tile a little wider: same height, 10/9ths the
   width (the aspect-ratio in dd.css). This cap is the mines board cap above solved for
   that: whatever formula is currently sizing a mines tile — the stage's width on a roomy
   viewport, the 72vh height budget on a short one — a tower row's four tiles are sized to
   land on the same height.

   The constants restate mines' geometry in towers' terms. A mines tile is (M − 32) / 5
   tall; a towers tile is 9/10 × (T − 24) / 4, so matching heights means
   T = 8/9 × (M − 32) + 24. Width-bound, M is this stage's width plus the 44px of panel
   chrome the mines shelf strips and this one keeps, which folds to 88.89% + 35px;
   height-bound, M is 72vh / zoom − 92 (mines' own budget — 72vh regardless of this
   window's --shelf-h), which folds to 64vh / zoom − 86px. An eight-row tower at five-row
   tile heights is taller than the window, so the shelf body scrolls where mines would
   not: matching mines' tile is the point, and the height is what gives. */
.sh_shelf_body .towers_board {
  max-width: min(
    100%,
    calc(88.89% + 35px),
    calc(64vh / var(--shelf-zoom) - 86px)
  );
  margin: 0 auto;
}

/* Mines takes the whole stage.
   .dd_result's 28px/12px is breathing room for a full-width page where the stage holds
   a headline figure; in the window the stage holds the board and nothing else, and the
   panel around it already has 10px of its own, so that padding was forty pixels of grey
   taken off a board that wanted them. The board then fills what is left.

   The cap is the window's own height budget, not a taste: the board is square, so its
   width is also its height, and a board taller than the room above the composer would
   arrive scrolled. The window is meant to be one box that is entirely there — no bar
   down its side, no wiggle — so the board is the thing that gives.

   The budget is the window's own max-height in the window's own zoomed pixels
   (`--shelf-h / --shelf-zoom`, the two values the .sh_shelf rule is written in, so this
   tracks them), less what is above and around the board inside it: the 51px header, and
   71px of body padding, panel padding, panel border and stage padding. Both figures are
   measured, not guessed — the body's scroll height then lands on its client height rather
   than a few pixels over it. (69 is the exact chrome; the extra two absorb the subpixel
   rounding the window's zoom introduces, which was worth a one-pixel scrollbar.) If the
   header or that chrome is ever restyled, the 122 moves with it.

   Below roughly a 640px-tall viewport the bet column is taller than any window that fits
   over the composer, and at that point the body has to scroll whatever the board does.
   Nothing here can fix that; it is a window smaller than its own controls. */
/* Mines has no furniture around the board: the board *is* the interface, so the stage panel
   it used to sit in was a grey box drawn around a grey box. Its fill, border and padding all
   come off and the field takes the whole stage, which is what the embed's board does too —
   there the cells sit straight on the card. */
.sh_shelf[data-game="mines"] .dd_panel.stage_panel {
  background: transparent;
  border-color: transparent;
  padding: 0;
}
.sh_shelf[data-game="mines"] .dd_result { padding: 0; }
.sh_shelf[data-game="mines"] .dd_board.mines {
  /* The chrome above and around the board, less the 30px the rule above just removed
     (10px panel padding twice, its 1px border twice, 4px stage padding twice). */
  max-width: min(100%, calc(var(--shelf-h, 62vh) / var(--shelf-zoom) - 92px));
}

/* The mines tiles are the embed's cells: the same #26272c fill and the same line, now read
   against the shelf body rather than against a panel of their own colour.

   The pointer states are the two the board answers with. Resting hover is a breath of the
   accent — the tile is not doing anything yet, it is only under the cursor, so 5% of wash is
   the whole of it and the outline moves only part of the way to the accent. A turned tile
   goes the other way and darkens: the safe tiles are the board's spent squares, and sinking
   them is what makes the ones still face down read as the live part of the board.

   The resting fill itself is not restated here: the tile already takes --bg / --border from
   the shelf-wide rule above, which is the card's own tile pair, and leaving it that way keeps
   the .mine wash mixing against it rather than being overridden by a flat colour. */
.sh_shelf[data-game="mines"] .dd_board.mines .dd_tile {
  transition: border-color .12s ease, background .18s ease, color .18s ease;
}
/* The idle board — twenty-five placeholders before a round is staked. With the panel gone
   the .35 the placeholders carried left them as ghosts on the shelf floor; they are the
   whole of what mines looks like at rest, so they sit at the tile's full colour. Only the
   resting field is lifted — a finished round is also twenty-five disabled tiles, and there
   the fade is the point, which is what the `idle` class in mines.js separates. */
.sh_shelf[data-game="mines"] .dd_board.mines.idle .dd_tile.dim { opacity: 1; }
.sh_shelf[data-game="mines"] .dd_board.mines .dd_tile:hover:not(:disabled) {
  border-color: color-mix(in srgb, var(--secondary) 40%, var(--sh-tile-line));
  background: color-mix(in srgb, var(--secondary) 5%, var(--sh-tile));
}
/* Staking changes nothing about the field: the twenty-five face-down tiles are the resting
   board, live or not, because that is what mines looks like and a stake is not a reason to
   restyle it. What moves is the tile you turn — it *sinks* to the dark, with the accent on
   its edge. The squares you have opened are the holes cut in the board and the ones still
   face down are the board, which is the way round the player reads it. */
.sh_shelf[data-game="mines"] .dd_board.mines .dd_tile.safe {
  background: color-mix(in srgb, #000 32%, var(--sh-tile));
  border-color: color-mix(in srgb, var(--secondary) 45%, var(--sh-tile-line));
}
/* A finished round is the board dimmed back with the round left standing in it: mines.js
   puts `.dim` on every tile that was never turned, so what stays at full strength is exactly
   what the player opened — the safe tiles, and the bomb that ended it. The mines take the
   sunk fill rather than the wash dd.css gives .mine, so a turned bomb reads as one more
   opened square and the ones never turned are the same square seen through the dim. */
.sh_shelf[data-game="mines"] .dd_board.mines .dd_tile.mine {
  background: color-mix(in srgb, #000 32%, var(--sh-tile));
  border-color: var(--mine);
}

/* The towers rows take the mines board's reveal treatment, state for state: the row being
   played is the sunk dark the mines field goes when a round is staked, hover is the same
   breath of accent over it, a cleared tile lifts back to the resting grey with the lilac
   edge, and the mine keeps the sunk fill with the red line around it. The dim rows above
   are the one state mines has no equivalent for and they stay as they are. */
.sh_shelf[data-game="towers"] .dd_row_tiles .dd_tile {
  transition: border-color .12s ease, background .18s ease, color .18s ease;
}
.sh_shelf[data-game="towers"] .dd_row_tiles.current .dd_tile {
  background: color-mix(in srgb, #000 32%, var(--sh-tile));
}
.sh_shelf[data-game="towers"] .dd_row_tiles.current .dd_tile:hover:not(:disabled) {
  border-color: color-mix(in srgb, var(--secondary) 40%, var(--sh-tile-line));
  background: color-mix(
    in srgb,
    var(--secondary) 7%,
    color-mix(in srgb, #000 32%, var(--sh-tile))
  );
}
.sh_shelf[data-game="towers"] .dd_row_tiles .dd_tile.safe {
  background: var(--sh-tile);
  border-color: color-mix(in srgb, var(--secondary) 45%, var(--sh-tile-line));
}
.sh_shelf[data-game="towers"] .dd_row_tiles .dd_tile.mine {
  background: color-mix(in srgb, #000 32%, var(--sh-tile));
  border-color: var(--mine);
}

/* Keno is the one tall board left that gives up height in the window: towers now sizes
   itself against the height budget (see .towers_board above) so its tiles keep the mines
   proportions instead of being squashed to a fixed strip. */
.sh_shelf_body .dd_keno { gap: 5px; }

/* Once the stream is too narrow for a two-column window, the shelf is the stream's whole
   width again and the panel stacks over the board. */
@media (max-width: 900px) {
  .sh_shelf { left: calc(16px / var(--shelf-zoom)); right: calc(16px / var(--shelf-zoom)); width: auto; }
  .sh_shelf_body .dd_grid { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------- composer */

/* The box sits 24px off the bottom of the window, the way it does in the client this is
   shaped like. The hint strip is lifted out of the flow and floated in that 24px — the
   same trick the typing indicator plays there — so the strip appearing or clearing never
   moves the box. */
.sh_composer { padding: 0 16px 24px; flex: 0 0 auto; position: relative; }
.sh_box {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  background: var(--sh-raise);
  border-radius: 8px;
  padding: 0 14px;
  min-height: 44px;
}
.sh_input {
  flex: 1 1 auto;
  border: 0;
  background: transparent;
  color: var(--sh-text);
  font-size: 15px;
  line-height: 1.4;
  padding: 11px 0;
  resize: none;
  max-height: 200px;
  overflow-y: auto;
  font-family: inherit;
}
/* The box is already the only thing you can type into, and the caret says where you
   are — the UA's focus ring on top of that is a dotted rectangle round the composer. */
.sh_input:focus, .sh_input:focus-visible { outline: none; }
.sh_input::placeholder { color: var(--sh-mute); }
/* The composer is contenteditable, so the placeholder is ours to draw. */
.sh_input:empty::before {
  content: attr(data-placeholder);
  color: var(--sh-mute);
  pointer-events: none;
}
.sh_input { white-space: pre-wrap; word-wrap: break-word; overflow-wrap: anywhere; height: auto; }
/* Emoji you are typing, at the size of the text they sit in. */
.sh_emoji_typed { width: 20px; height: 20px; vertical-align: -4px; filter: none; }
.sh_box_acts { display: flex; gap: 2px; padding: 6px 0; flex: 0 0 auto; }
.sh_box_btn {
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--sh-dim);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
  transition: color .12s ease, background .12s ease, transform .12s ease;
}
.sh_box_btn:hover { color: var(--sh-text); background: rgba(255, 255, 255, .05); }
.sh_box_btn.on { color: var(--secondary); }
.sh_box_btn:active { transform: scale(.92); }

.sh_hint {
  position: absolute;
  left: 18px;
  right: 18px;
  bottom: 4px;
  height: 16px;
  line-height: 16px;
  font-size: 11px;
  color: var(--sh-mute);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  pointer-events: none;
}

/* -- popovers ------------------------------------------------------------ */

.sh_pop {
  position: absolute;
  bottom: 100%;
  right: 0;
  margin-bottom: 10px;
  background: #1c1d21;
  border: 1px solid #101114;
  border-radius: 8px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, .5);
  z-index: 40;
  animation: sh_pop_in .14s ease-out;
  overflow: hidden;
}
@keyframes sh_pop_in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.sh_pop[hidden] { display: none; }
.sh_pop_head {
  padding: 10px 12px 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--sh-mute);
}

.sh_emojis { width: 316px; max-height: 260px; overflow-y: auto; padding: 0 8px 10px; }
.sh_emoji_cat { font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--sh-mute); padding: 8px 4px 4px; }
.sh_emoji_grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 2px; }
.sh_emoji_btn {
  border: 0;
  background: transparent;
  font-size: 21px;
  line-height: 1;
  padding: 6px 0;
  border-radius: 4px;
  cursor: pointer;
}
.sh_emoji_btn:hover { background: #2a2b30; }
.sh_emoji_img { width: 24px; height: 24px; display: block; margin: 0 auto; }
.sh_emoji_btn .sh_emoji_img { transition: transform .1s ease; }
.sh_emoji_btn:hover .sh_emoji_img { transform: scale(1.12); }

/* The picker's handle. Greyed until hovered, so a resting composer stays quiet — the
   colour arriving on hover is the affordance, the way it is in the client we follow.
   The face itself is swapped on hover; see rollEmojiButton in app.js. */
.sh_box_emoji img { width: 22px; height: 22px; filter: grayscale(1) opacity(.65); transition: filter .12s ease, transform .12s ease; }
.sh_box_emoji:hover img,
.sh_box_emoji.on img { filter: none; transform: scale(1.08); }

/* The `:name` list is the `/` palette with an emoji where the game icon goes. */
.sh_emoji_ac .sh_slash_icon { display: flex; align-items: center; justify-content: center; }

/* -- the activity list --------------------------------------------------- */

/* Shaped as an app directory listing rather than as a menu: a stack of raised rows, each
   one an icon, a name and a description that runs until it runs out of room. The type
   scale and the three greys are the ones the client this shell is modelled on uses for
   the same list — a 16/600 name in the header white, a 14/400 description in the muted
   grey, and a row that lifts to the hover grey — so the popover reads as a directory the
   moment it opens rather than as a list of buttons that happen to have subtitles.

   What is not copied is the voice or the palette: the names stay lowercase like the rest
   of the site, and the marks are all one accent (see game_icons.js). */
/* The directory's own heading, in place of the 11px uppercase label the other popovers
   carry: a category header in a listing is a heading, and it is set like one. */
.sh_acts_pop .sh_pop_head {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0;
  text-transform: none;
  color: var(--sh-head);
  padding: 14px 16px 10px;
}
.sh_acts_pop {
  width: 336px;
  max-height: min(452px, 60vh);
  overflow-y: auto;
  padding-bottom: 8px;
  scrollbar-width: none; /* firefox */
  -ms-overflow-style: none; /* legacy edge */
}
.sh_acts_pop::-webkit-scrollbar { display: none; }
.sh_act_list { display: flex; flex-direction: column; gap: 2px; padding: 0 8px; }
.sh_act_loading { padding: 2px 16px 10px; font-size: 14px; color: var(--sh-mute); }

.sh_act {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  border: 0;
  border-radius: 8px;
  background: var(--sh-row);
  color: var(--sh-text);
  padding: 8px 10px;
  cursor: pointer;
  text-align: left;
  /* Rows are a fixed height whatever the description does, so eight of them make an even
     column instead of a ragged one. */
  min-height: 56px;
  transition: background .12s ease;
}
.sh_act:hover { background: var(--sh-row-hover); }
.sh_act:active { background: var(--sh-row-active); }
.sh_act_icon {
  width: 40px;
  height: 40px;
  flex: 0 0 40px;
  /* The mark fills the box and there is no tile behind it, so this element carries no
     background of its own — the icon is strokes on the popover, nothing more. */
  /* At rest the mark is the same white as the row's name, so a column of eight reads as
     one list of labels rather than eight coloured badges competing with the text. The
     accent is what the pointer does, not what the icon is. */
  color: var(--sh-head);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color .12s ease;
}
.sh_act:hover .sh_act_icon,
.sh_act:focus-visible .sh_act_icon,
.sh_act.on .sh_act_icon { color: var(--secondary); }
/* The row lifts, not the mark: inverting the tile on hover made the icon the thing that
   moved, and in a directory the row is what is being pointed at. */
.sh_act .sh_gicon { display: block; }

/* min-width:0 is what lets the description below actually clip — without it the flex
   item takes its content's width and the ellipsis never happens. */
.sh_act_body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sh_act_name { font-size: 16px; font-weight: 600; color: var(--sh-head); line-height: 1.25; }
.sh_act_sub {
  font-size: 14px;
  font-weight: 400;
  color: var(--sh-sub);
  line-height: 1.3;
  /* One line, cut with an ellipsis — the whole reason the blurbs are written long. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sh_act[disabled] { opacity: .45; cursor: not-allowed; }
.sh_act[disabled]:hover { background: var(--sh-row); }

/* -- the slash palette --------------------------------------------------- */

/* Full width rather than anchored to a button, because the thing it is completing is
   the text in the bar and it should sit over it. */
.sh_slash { left: 0; right: 0; max-height: 320px; overflow-y: auto; padding-bottom: 6px; }
.sh_pop_head .sh_slash_tip {
  float: right;
  font-weight: 500;
  letter-spacing: .03em;
  text-transform: none;
  color: var(--sh-mute);
}
.sh_slash_item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--sh-text);
  padding: 7px 12px;
  cursor: pointer;
  text-align: left;
  font-size: 14px;
}
.sh_slash_item.on, .sh_slash_item:hover { background: #2a2b30; }
/* The same drawn mark as the activity list, at the size a one-line row can carry, and
   with no plate behind it there either. */
.sh_slash_icon {
  width: 22px;
  height: 22px;
  flex: 0 0 22px;
  /* Same rest/hover rule as the activity list — two lists of the same eight games have to
     agree on colour as well as shape. `.on` is the keyboard-highlighted row. */
  color: var(--sh-head);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color .12s ease;
}
.sh_slash_item.on .sh_slash_icon,
.sh_slash_item:hover .sh_slash_icon { color: var(--secondary); }
.sh_slash_icon .sh_gicon { display: block; }
body.chat_app .sh_slash_name { font-family: var(--font); font-weight: 600; flex: 0 0 auto; }
/* What the query matched on. It is the reason this row is in the list, so it is the one
   thing on the row that takes the accent. */
.sh_slash_name mark { background: transparent; color: var(--secondary); font-weight: 600; }
.sh_slash_blurb { font-size: 12px; color: var(--sh-mute); flex: 1 1 auto; }
body.chat_app .sh_slash_args {
  font-family: var(--font);
  font-size: 11px;
  color: var(--sh-mute);
  opacity: .7;
  flex: 0 0 auto;
}
@media (max-width: 700px) { .sh_slash_args, .sh_slash_blurb { display: none; } }

/* -- the argument bar ---------------------------------------------------- */

/* Command mode replaces the textarea in place, so the message bar keeps its shape and
   the arguments read as part of the thing you are about to send. */
.sh_cmd {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 7px 0;
  min-height: 30px;
}
.sh_cmd[hidden] { display: none; }
body.chat_app .sh_cmd_name {
  font-family: var(--font);
  font-size: 13px;
  font-weight: 700;
  color: var(--secondary);
  background: rgba(185, 160, 242, .12);
  border-radius: 4px;
  padding: 4px 8px;
  flex: 0 0 auto;
}
.sh_cmd_arg { display: flex; align-items: center; gap: 6px; }
.sh_cmd_label { font-size: 11px; color: var(--sh-mute); letter-spacing: .03em; }
.sh_cmd_wrap {
  display: flex;
  align-items: center;
  gap: 2px;
  background: var(--sh-main);
  border: 1px solid var(--sh-line);
  border-radius: 4px;
  padding: 3px 7px;
}
.sh_cmd_wrap:focus-within { border-color: var(--secondary); }
body.chat_app .sh_cmd_fix { font-family: var(--font); font-style: normal; font-size: 12px; color: var(--sh-mute); }
body.chat_app .sh_cmd_in,
body.chat_app .sh_cmd_pick {
  font-family: var(--font);
  font-size: 13px;
  color: var(--sh-text);
  background: transparent;
  border: 0;
  padding: 0;
  outline: none;
}
body.chat_app .sh_cmd_pick {
  background: var(--sh-main);
  border: 1px solid var(--sh-line);
  border-radius: 4px;
  padding: 4px 6px;
  cursor: pointer;
}
.sh_cmd_pick:focus { border-color: var(--secondary); }
.sh_cmd_in::placeholder { color: var(--sh-mute); }
.sh_cmd_go, .sh_cmd_x {
  border: 0;
  border-radius: 4px;
  cursor: pointer;
  font-family: inherit;
  font-size: 12px;
  padding: 5px 10px;
}
.sh_cmd_go { margin-left: auto; background: var(--secondary); color: #17151d; font-weight: 600; }
.sh_cmd_go:hover { filter: brightness(1.08); }
.sh_cmd_x { background: transparent; color: var(--sh-mute); padding: 5px 8px; }
.sh_cmd_x:hover { color: var(--sh-text); background: rgba(255, 255, 255, .06); }

/* ---------------------------------------------------------------- members */

.sh_members {
  width: var(--sh-members-w);
  flex: 0 0 var(--sh-members-w);
  background: var(--sh-side);
  overflow-y: auto;
  padding: 16px 8px;
}
.sh_members[hidden] { display: none; }
.sh_members_head {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--sh-mute);
  padding: 0 8px 6px;
}
/* A member row is a button, not a paragraph: clicking it opens the person's card, so the
   name must not turn into a text selection under a double-click and the caret must stay a
   pointer across the whole row rather than becoming an I-beam over the name. */
.sh_member {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 8px;
  border-radius: 4px;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
}
.sh_member:hover, .sh_member.on { background: #26272c; }
.sh_member_avatar {
  width: 30px;
  height: 30px;
  flex: 0 0 30px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
  color: #17151d;
  position: relative;
}
.sh_member_avatar::after {
  content: "";
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #23a55a;
  border: 2.5px solid var(--sh-side);
}
.sh_member.off { opacity: .42; }
.sh_member.off .sh_member_avatar::after { background: #80848e; }
.sh_member_name { font-size: 14px; color: var(--sh-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sh_member_tag { margin-left: auto; font-size: 9px; letter-spacing: .08em; text-transform: uppercase; color: var(--sh-mute); }
.sh_member_kick {
  margin-left: auto;
  border: 0;
  background: transparent;
  color: var(--sh-mute);
  cursor: pointer;
  font-size: 12px;
  opacity: 0;
}
.sh_member:hover .sh_member_kick { opacity: 1; }
.sh_member_kick:hover { color: #e0706a; }

/* ---------------------------------------------------------------- profile card */

/* An avatar tile carrying a picture. The initials stay in the markup as the alt text of
   last resort and are simply not painted once there is an image over them. */
/* A face that opens a card is a control, not decoration. */
[data-profile] { cursor: pointer; }

.has_pic {
  background-size: cover !important;
  background-position: center !important;
  color: transparent !important;
}

.sh_card {
  position: fixed;
  width: 300px;
  max-width: calc(100vw - 24px);
  background: #111214;
  border-radius: 8px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, .6);
  overflow: hidden;
  z-index: 1300;
  animation: sh_pop_in .12s ease-out;
}
.sh_card[hidden] { display: none; }

/* Banner plus the picture that overlaps it. One relative box, because on your own card a
   single overlay covers both and the whole thing is the upload target. */
.sh_card_top { position: relative; }
.sh_card_banner {
  height: 72px;
  /* Replaced by the accent derived from the picture; this is the no-picture default. */
  background: var(--sh-raise);
}
.sh_card_face {
  position: absolute;
  left: 16px;
  bottom: -34px;
  width: 76px;
  height: 76px;
  border-radius: 50%;
  border: 6px solid #111214;
  background: var(--secondary);
  color: #17151d;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 26px;
  font-weight: 700;
}

/* The upload affordance: invisible until the pointer is over the banner, then a grey
   wash and one thin line of white type. Own card only. */
.sh_card_upload {
  position: absolute;
  inset: 0;
  border: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, .55);
  color: #fff;
  font-family: inherit;
  font-size: 12px;
  font-weight: 300;
  letter-spacing: .14em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0;
  transition: opacity .12s ease;
}
.sh_card_top:hover .sh_card_upload, .sh_card_upload:focus-visible { opacity: 1; }

.sh_card_body { padding: 44px 16px 16px; }
.sh_card_name { font-size: 19px; font-weight: 700; color: var(--sh-head); word-break: break-word; }
.sh_card_sub { font-size: 12px; color: var(--sh-mute); margin-top: 2px; }
.sh_card_panel {
  margin-top: 12px;
  background: #1b1c1f;
  border-radius: 8px;
  padding: 12px;
}
.sh_card_head {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--sh-mute);
  margin-bottom: 8px;
}
.sh_card_stat {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 4px 0;
  font-size: 13px;
}
.sh_card_stat_k { color: var(--sh-dim); }
.sh_card_stat_v {
  color: var(--sh-text);
  font-family: var(--sh-mono, "JetBrains Mono", monospace);
  font-weight: 500;
  text-align: right;
}
.sh_card_stat_v small { display: block; font-size: 10px; color: var(--sh-mute); font-weight: 400; }
.sh_card_note { margin-top: 10px; font-size: 11px; color: var(--sh-mute); }

/* ---------------------------------------------------------------- dialog */

.sh_modal_bg {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .72);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1200;
  animation: sh_pop_in .14s ease-out;
}
.sh_modal_bg[hidden] { display: none; }
.sh_modal {
  width: 420px;
  max-width: calc(100vw - 32px);
  background: #232428;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, .6);
}
.sh_modal h3 { font-size: 18px; margin: 0 0 6px; }
.sh_modal p { font-size: 13px; color: var(--sh-dim); margin: 0 0 14px; }
.sh_modal_row { display: flex; gap: 8px; }
.sh_field {
  flex: 1 1 auto;
  background: #16171a;
  border: 1px solid #101114;
  border-radius: 5px;
  color: var(--sh-text);
  padding: 9px 11px;
  font-size: 13px;
  font-family: inherit;
  min-width: 0;
}
.sh_btn {
  border: 0;
  border-radius: 5px;
  padding: 9px 16px;
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  background: var(--secondary);
  color: #17151d;
  flex: 0 0 auto;
}
.sh_btn:hover { filter: brightness(1.08); }
.sh_btn.ghost { background: transparent; color: var(--sh-dim); }
.sh_btn.ghost:hover { color: var(--sh-text); }
.sh_modal_acts { display: flex; justify-content: flex-end; gap: 6px; margin-top: 16px; }

/* ---------------------------------------------------------------- narrow */

/* Below the drawer breakpoint the four columns stop fitting side by side, so the two
   that are navigation slide over the channel instead of squeezing it. The channel
   itself never gets narrower than the viewport and never wider — it is the one column
   that is always the full width of whatever is left. */

.sh_nav_btn { display: none; margin-left: -6px; }

.sh_scrim {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .6);
  z-index: 55;
  animation: sh_pop_in .14s ease-out;
}
.sh_scrim[hidden] { display: none; }

/* One column tighter before anything has to move. */
@media (max-width: 1180px) { :root { --sh-members-w: 200px; } }

@media (max-width: 900px) {
  .sh_nav_btn { display: flex; }

  /* Taken out of flow so .sh_main inherits the whole width; the pair slides as one. */
  .sh_rail,
  .sh_side {
    position: fixed;
    top: 0;
    height: 100%;
    z-index: 60;
    transform: translateX(calc(-1 * (var(--sh-rail-w) + var(--sh-side-w))));
    transition: transform .22s cubic-bezier(.2, .8, .3, 1);
  }
  .sh_rail { left: 0; }
  .sh_side { left: var(--sh-rail-w); box-shadow: 6px 0 24px rgba(0, 0, 0, .45); }
  /* With the rail hidden the drawer is the channel list alone: it starts at the screen
     edge, and it only has its own width to slide out from under. */
  .sh_rail[hidden] + .sh_side {
    left: 0;
    transform: translateX(calc(-1 * var(--sh-side-w)));
  }
  body.chat_app.nav_open .sh_rail[hidden] + .sh_side { transform: none; }
  body.chat_app.nav_open .sh_rail,
  body.chat_app.nav_open .sh_side { transform: none; }

  .sh_members {
    position: fixed;
    top: 0;
    right: 0;
    height: 100%;
    width: min(260px, 78vw);
    flex-basis: auto;
    z-index: 60;
    box-shadow: -6px 0 24px rgba(0, 0, 0, .45);
  }

  /* The 460px cap is a reading measure for a wide column and is not wanted here, but the
     card still may not outgrow the row it sits in — it shrink-wraps its board now, so
     "no maximum" would let a fixed-width board push the card past the screen edge. */
  .sh_embed { max-width: 100%; }
  .sh_shelf { max-height: calc(54vh / var(--shelf-zoom)); }
}

@media (prefers-reduced-motion: reduce) {
  .sh_rail, .sh_side { transition: none; }
}

@media (max-width: 560px) {
  :root { --sh-side-w: 216px; --sh-rail-w: 60px; }
  .sh_msg { padding-left: 12px; padding-right: 12px; gap: 12px; }
  .sh_sys { padding-left: 64px; }
  .sh_head { padding: 0 10px; gap: 6px; }
  /* The topic is the first thing to go — the channel's name has to survive. */
  .sh_head_topic { display: none; }
  .sh_river_top { padding: 20px 12px 8px; }
  .sh_river_top h2 { font-size: 22px; }
  .sh_composer { padding: 0 10px 20px; }
  .sh_shelf { left: calc(10px / var(--shelf-zoom)); right: calc(10px / var(--shelf-zoom)); }
  .sh_shelf_body { padding: 10px; }
  .sh_shelf_body .dd_grid { grid-template-columns: 1fr; }
  .sh_pop { right: -8px; }
  .sh_emojis { width: min(316px, calc(100vw - 28px)); }
  .sh_acts_pop { width: min(336px, calc(100vw - 28px)); }
}

@media (max-width: 400px) {
  .sh_msg { gap: 10px; }
  .sh_avatar { width: 34px; height: 34px; flex-basis: 34px; font-size: 13px; }
  .sh_msg .sh_gutter { width: 34px; flex-basis: 34px; }
  .sh_sys { padding-left: 56px; }
  .sh_text, .sh_author { font-size: 15px; }
}

/* ================================================================
   the shared table — #slide

   A `mode: "game"` channel is not a channel with a game in it: the
   board is where the channel would be. So .sh_body is hidden outright
   (app.js sets .table_channel on the body) and this section owns the
   main column instead — no river, no composer, no member list.

   Two columns: what you are putting on the round, and the round. The
   board's ribbon of boxes is positioned arithmetically by slide.js
   from --sl-cell, so that number and the CELL constant in that file
   have to agree; everything else here is presentation.

   Colour: the site has one accent (--secondary, the amethyst) and the
   box tiers below are it at five strengths. A big multiplier is not a
   different hue, it is more of the same one — five competing colours
   on one strip would read as five kinds of thing rather than one thing
   getting bigger. Under 2x a box stays grey on purpose: if every box
   were tinted, no box would read as tinted.
   ================================================================ */

:root {
  /* Cell pitch: box width plus the gap after it (8.25px — trimmed a quarter from the old
     11px so the strip reads tighter). Mirrors CELL_FALLBACK in assets/games/slide.js. */
  --sl-cell: 118.25px;
  --sl-box: 110px;
  /* A participant row's full height — 24px face plus 6px padding either side, border-box.
     The list's cap is a multiple of it, so "ten bets" stays ten bets if the row changes. */
  --sl-prow: 36px;
  /* The five tiers, low to high. Each is the accent hue pushed further round and up. */
  --sl-t1: #8f7fc4;
  --sl-t2: #b9a0f2;
  --sl-t3: #c98bf5;
  --sl-t4: #e07ae0;
  --sl-t5: #ff6fd8;
}

/* The head keeps the channel name — a board with no header reads as a page that failed to
   load — but everything on it that acts on a chat surface is gone (app.js hides the
   member-list button, and a table has no invite). */
body.table_channel .sh_head_topic { color: var(--secondary); }

/* `display: flex` below beats the `hidden` attribute's UA `display: none`, so on a normal
   channel the empty table still claimed a flex share of the main column and pushed the
   composer up off the bottom. Restore it explicitly. */
.sh_table[hidden] { display: none; }

.sh_table {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  display: flex;
  padding: 18px;
}
.sh_table > div { flex: 1 1 auto; min-width: 0; display: flex; }

/* A Games-server original: the exact window the activity shelf opens — the same
   .sh_shelf/.sh_shelf_body rules paint its panels, size its bet column and pick its
   per-game width — parked on the table surface instead of popped over the composer.
   Only the popover-ness is overridden: it sits in flow, centred, with no entrance. */
.sh_table .sh_games_win {
  position: static;
  margin: auto;
  flex: 0 0 auto;
  animation: none;
}

/* On the table surface the window itself disappears: no popover chrome, no one box
   holding everything. What is left is the slide table's picture — the bet column and
   the board, each a card of its own (the .sl_panel treatment) sitting directly on the
   surface with clear air between them. The shelf popover keeps its window; this is
   the channel mount only. */
.sh_table .sh_games_win {
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
  overflow: visible;
  /* The height cap and the body scroll were the window's; the table surface itself
     scrolls (.sh_table is overflow: auto), so a tall board just runs on. */
  max-height: none;
}
.sh_table .sh_games_win .sh_shelf_body {
  padding: 0;
  overflow: visible;
}
/* Slide's gap between its cards, in place of the 10px the panels had inside one box. */
.sh_table .sh_games_win .dd_grid { gap: 16px; }
.sh_table .sh_games_win .dd_panel {
  background: var(--sh-raise);
  border: 1px solid var(--sh-line);
  border-radius: 10px;
  padding: 16px;
}
.sh_table .sh_games_win .roul_win { --roul-fade: var(--sh-raise); }
/* Mines strips its stage panel inside the popover — there the window is the card and a
   panel in it was a grey box in a grey box. On the table the window is gone, so the board
   gets its card back like every other game's (and like the slide board). */
.sh_table .sh_games_win[data-game="mines"] .dd_panel.stage_panel {
  background: var(--sh-raise);
  border-color: var(--sh-line);
  padding: 16px;
}

/* The recent-bets rail is absolutely positioned over the stage panel's top-right corner,
   so the panel is what has to leave room for it: 6px above the chips, the 19px chip, 6px
   under. dd.css reserves that on .stage_panel, but every rule above restates `padding`
   as a shorthand and so wipes it — mines worst of all, where padding goes to 0 and the
   first tile row is drawn straight under the figures. Restated once, last, for both
   places the rail is drawn. */
.sh_shelf_body .dd_panel.stage_panel,
.sh_shelf[data-game="mines"] .dd_panel.stage_panel,
.sh_table .sh_games_win .dd_panel.stage_panel,
.sh_table .sh_games_win[data-game="mines"] .dd_panel.stage_panel { padding-top: 31px; }

.sl_boot {
  margin: auto;
  color: var(--sh-mute);
  font-size: 13px;
  font-family: var(--sh-font);
}

.sl_wrap {
  margin: auto;
  display: grid;
  /* The board takes the room; the two panels are each the width their contents need and no
     more. The player list sits on the far side from the stake panel so the pair cannot stack,
     and the max-width carries the extra column plus its gap — the board is the same width here
     as it was when the list was stacked under the stake panel. */
  grid-template-columns: 296px minmax(0, 1fr) 268px;
  gap: 16px;
  width: 100%;
  max-width: 1464px;
  font-family: var(--sh-font);
  /* Each column is its own height. Stretched — the default — the columns were tied together at
     the tallest of them, so every bet that made the player list longer also made the board
     taller: a ten-row list dragged 200px of empty track down with it. */
  align-items: start;
}

.sl_side { display: flex; flex-direction: column; gap: 14px; min-width: 0; }

/* The player-list column, and the reason it is a wrapper rather than the panel itself.
   `align-items: start` already kept a long list from stretching the board, but the wrap is
   centred in the channel (`margin: auto`), so the list still set the wrap's *height* — and a
   taller wrap re-centres, which walked the whole table up the screen every time somebody
   joined the round. The column is held at zero height and the panel is taken out of flow
   inside it: it hangs from the top of the board and grows downwards, measuring nothing. The
   list's own ten-row cap keeps it inside the board's height. */
.sl_seats { position: relative; height: 0; min-width: 0; }
.sl_seats > .sl_players { position: absolute; top: 0; left: 0; right: 0; }

.sl_panel {
  background: var(--sh-raise);
  border: 1px solid var(--sh-line);
  border-radius: 10px;
  padding: 16px;
}

/* -- the stake panel ------------------------------------------------ */

.sl_field { margin-bottom: 16px; }
.sl_field:last-of-type { margin-bottom: 12px; }
/* Lowercase, and one step further back than the rest of the panel's grey: these two words
   name a field the player already knows the shape of, so they are a caption on the box rather
   than something to read. The figures they label carry the emphasis. */
.sl_field > label {
  display: block;
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: lowercase;
  color: var(--sh-mute);
  margin-bottom: 6px;
}

.sl_amount { position: relative; display: flex; align-items: center; }
.sl_input {
  flex: 1 1 auto;
  min-width: 0;
  background: var(--sh-field);
  border: 1px solid var(--sh-line);
  border-radius: 6px;
  color: var(--sh-text);
  font: 600 15px/1 var(--sh-mono);
  padding: 11px 12px;
  outline: none;
}
.sl_input:focus { border-color: var(--secondary); }
.sl_suffix {
  position: absolute;
  right: 12px;
  color: var(--sh-mute);
  font: 600 14px/1 var(--sh-mono);
  pointer-events: none;
}

.sl_quick { display: flex; gap: 6px; margin-top: 8px; }
.sl_chip {
  flex: 1 1 auto;
  background: var(--sh-row);
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--sh-sub);
  font: 600 12px/1 var(--sh-font);
  padding: 8px 4px;
  cursor: pointer;
  transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.sl_chip:hover { background: var(--sh-row-hover); color: var(--sh-head); }
.sl_chip:active { background: var(--sh-row-active); }

/* What a win returns, as the rule that closes the panel.
   Two tiles used to sit here — a win chance and a payout, boxed like a readout — and between
   them they spent a third of the panel restating the target the player had just typed. This is
   the same figure with the box taken away: a hairline across the panel, the word small enough
   to be a caption rather than a heading, and the amount sitting on the line with the rule
   broken around it. The label is the size and colour an embed's stat label is, because it is
   doing the same job. */
.sl_pays {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 2px 0 14px;
  /* Held at the height of the labelled state so an empty amount box does not walk the button
     up two pixels and back as the player types. */
  min-height: 18px;
  /* The rule itself: a line through the middle of the row, drawn behind the flex children and
     covered by their own background so the text sits in a gap in it. */
  position: relative;
}
.sl_pays::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: var(--sh-line);
}
.sl_pays_k,
.sl_pays_v {
  position: relative;
  background: var(--sh-raise);
  padding-right: 8px;
}
.sl_pays_k {
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--sh-mute);
}
.sl_pays_v {
  margin-left: auto;
  padding-right: 0;
  padding-left: 8px;
  font: 700 14px/1 var(--sh-mono);
  color: var(--sh-head);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Nothing typed: the rule stays — it is the divider above the button as much as it is the
   readout — but with no label to caption an absent figure. Nothing to style; the class is on
   the node so the state is readable from the DOM. */

/* Outlined, never filled — the same statement the bet button makes in every other game. */
.sl_play {
  width: 100%;
  background: transparent;
  border: 1px solid var(--secondary);
  border-radius: 6px;
  color: var(--secondary);
  font: 700 14px/1 var(--sh-font);
  padding: 14px;
  cursor: pointer;
  transition: background .12s ease, opacity .12s ease;
}
.sl_play:hover:not(:disabled) {
  background: color-mix(in srgb, var(--secondary) 16%, transparent);
}
/* A phase where nothing can be done reads as disabled. A round the player is already on
   does not — it says so instead, in the accent, because a staked bet is a state worth
   seeing rather than a control that stopped working. */
.sl_play:disabled { cursor: default; opacity: .45; }
/* Booked, not staked. The button is outlined either way; a queued stake carries a faint wash
   of the accent so the waiting state is visible without the button ever reading as filled. */
.sl_play_queue {
  background: color-mix(in srgb, var(--secondary) 10%, transparent);
}
.sl_play_queue:hover:not(:disabled) {
  background: color-mix(in srgb, var(--secondary) 20%, transparent);
}
/* The acknowledgement that a bet landed: one quick dip and straight back, and the button
   is ready for the next stake — several bets may go on one round, so "placed" is a beat,
   not a state. */
.sl_play_flash { animation: sl_play_dip .38s ease; }
@keyframes sl_play_dip {
  0% { filter: none; }
  35% { filter: brightness(.5); }
  100% { filter: none; }
}

/* -- the player list ----------------------------------------------- */

.sl_players {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 12px;
}
.sl_players_head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 2px 4px 10px;
  border-bottom: 1px solid var(--sh-line);
  margin-bottom: 6px;
}
.sl_players_count { font-size: 12px; font-weight: 600; color: var(--sh-dim); }
.sl_players_pot { font: 700 13px/1 var(--sh-mono); color: var(--sh-head); }
/* Ten bets, then scroll.
   A row is exactly `--sl-prow` tall — 24px face plus 6px of padding either side, border-box —
   and the rows stack with no gap between them, so ten of them is the cap outright. It was
   320px before, which is eight rows and a nine-tenths of a tenth: the list opened cut through
   the middle of a row, which reads as a rendering fault rather than as more to see. */
.sl_players_list {
  overflow-y: auto;
  min-height: 0;
  max-height: calc(var(--sl-prow) * 10);
}
/* The empty state stands in for a row, so it is built like one: same padding, same 24px
   face-height line box, same type size. The panel is then the same height whether or not
   anyone is on the round. */
.sl_players_empty {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--sl-prow);
  padding: 6px 4px;
  font-size: 13px;
  color: var(--sh-mute);
}

.sl_row {
  display: grid;
  /* face, name, stake, target, payout — the embed stat order, with the name where an embed
     does not need one. The three figure columns size to their content so the row cannot
     reflow as a payout counts up; the name gives up the space. */
  grid-template-columns: 24px minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: 8px;
  padding: 6px 4px;
  border-radius: 5px;
  /* A row's colour arrives when the round settles, and it travels rather than snaps — the
     same treatment the message cards give a settled figure. */
  transition: background .2s ease, opacity .2s ease;
}
/* Settled either way, and the two read as opposites: a win comes up in the lilac the whole
   board is tinted with, a loss goes red and steps back — the row dims as a whole, so the
   face dims with it and a glance down the list is a glance at who is left. */
.sl_row_win { background: rgba(185, 160, 242, .16); }
.sl_row_lose { opacity: .5; }
/* Nothing marks your own row while the round is still open. It used to carry the same lilac
   tint a win does, and on a board where most of the rows are yours that read as a list of
   results on a round that has not been spun yet — the tint means *paid*, so it is kept for the
   settled rows and the waiting ones are plain. */
.sl_row_me.sl_row_lose { background: rgba(185, 160, 242, .09); }
.sl_face {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 9px/1 var(--sh-font);
  color: #17131f;
  overflow: hidden;
}
.sl_face_img { width: 100%; height: 100%; object-fit: cover; }
.sl_row_name {
  font-size: 13px;
  color: var(--sh-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The stake. White and unchanging, whatever the round does to the row around it: it is the
   one figure on the row that was settled the moment the bet went on. */
.sl_row_bet {
  font: 600 12px/1 var(--sh-mono);
  color: var(--sh-head);
  font-variant-numeric: tabular-nums;
}
.sl_row_mult { font: 600 12px/1 var(--sh-mono); }
.sl_row_pay {
  display: inline-block;
  font: 600 12px/1 var(--sh-mono);
  color: var(--sh-mute);
  /* Tabular figures: the payout counts up in place, and proportional digits make the number
     jitter sideways while it climbs. */
  font-variant-numeric: tabular-nums;
  transition: color .2s ease;
}
.sl_row_win .sl_row_pay { color: var(--sl-t2); font-weight: 700; }
.sl_row_lose .sl_row_pay { color: var(--sh-lose); }
/* The count-up's own beat: the figure swells as it starts climbing and settles at its size.
   `animatePayouts` adds this at the moment the numbers start moving. */
.sl_amt_up { animation: sl_amt_up .62s cubic-bezier(.2, .8, .3, 1); }
@keyframes sl_amt_up {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.16); }
  100% { transform: scale(1); }
}

/* -- the board ----------------------------------------------------- */

.sl_board {
  background: var(--sh-raise);
  border: 1px solid var(--sh-line);
  border-radius: 10px;
  padding: 14px;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 420px;
}

.sl_history {
  display: flex;
  gap: 6px;
  /* Top right, where the wheel games put their last results — and where the eye already
     goes for "what has been happening". */
  justify-content: flex-end;
  flex-wrap: wrap;
  min-height: 26px;
}
/* Bare figures, not chips: no box, no border — the history rail is a row of coloured
   numbers. The padding is kept as the hover/click target. Hover moves nothing and draws
   nothing; the figure itself simply comes up brighter. */
.sl_hchip {
  background: none;
  border: 0;
  padding: 4px 6px;
  font: 600 11px/1 var(--sh-mono);
  color: var(--sh-sub);
  cursor: pointer;
  transition: filter .12s ease;
}
.sl_hchip:hover { filter: brightness(1.45); }
/* A fresh result arrives at the right end of the rail and the older chips shuffle left.
   drawHistory tags only the chip that just landed, so the redraw animates one chip and the
   rest simply take their new places. */
.sl_hchip_new { animation: sl_chip_in .38s cubic-bezier(.2, .8, .3, 1); }
@keyframes sl_chip_in {
  from { opacity: 0; transform: translateX(16px); }
  to { opacity: 1; transform: none; }
}

/* The ribbon.

   The track is the window; the rail inside it is the ribbon and is what moves. Only
   `transform` is animated — never `left` or a margin — so the slide is a compositor job
   rather than sixty layouts a second. */
.sl_track {
  position: relative;
  flex: 1 1 auto;
  min-height: 210px;
  margin: 14px 0;
  overflow: hidden;
  /* Its own paint layer, so boxes crossing it do not repaint the panel behind them. */
  contain: paint;
}
.sl_rail {
  position: absolute;
  top: 50%;
  left: 0;
  display: flex;
  gap: calc(var(--sl-cell) - var(--sl-box));
  transform: translate3d(0, 0, 0);
  will-change: transform;
  margin-top: calc(var(--sl-box) / -2);
}

.sl_cell {
  position: relative;
  flex: 0 0 var(--sl-box);
  width: var(--sl-box);
  height: var(--sl-box);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: var(--sh-field);
  border: 1px solid var(--sh-tile-line);
  color: var(--sh-dim);
  font: 700 17px/1 var(--sh-mono);
  /* The resting look of every box, travelling or not: pushed back a touch, so the one
     thing that ever comes up to full strength is the box the round selects. */
  filter: brightness(.82);
  transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease,
    filter .3s ease;
}
.sl_blank { opacity: .25; }
/* A recycled cell — a box from an earlier round, on the ribbon only to be travelled past the
   way a roulette marker crosses pockets that are not the result — is drawn exactly like every
   other box. It used to be pushed back to .32 opacity to mark out the round's own strip, and
   that read as the ribbon going see-through on one side of the landing: the run-up ahead of the
   strip and the pad after it are the same track, and a track that fades where the strip ends
   says the board has run out. The strip is told apart by the box that lights on arrival
   (.sl_landed), which is the only distinction the player needs. */

/* The tiers.

   Every box on the strip is the same dark grey — no wash, no tinted border, and the fill is
   the one thing that never varies. What a big number gets is the number itself in the tier's
   colour and, from t3 up, a glow thrown *outside* the box onto the boxes either side of it.
   Tinting the fill made the ribbon read as a row of coloured tiles; a glow reads as one box
   burning brighter than its neighbours, which is what the eye should catch as it goes past.
   No `background` or `border-color` belongs in this block. */
.sl_t0 { color: var(--sh-dim); }
.sl_t1 { color: var(--sl-t1); }
.sl_t2 { color: var(--sl-t2); }
.sl_t3 { color: var(--sl-t3); }
.sl_t4 { color: var(--sl-t4); }
.sl_t5 { color: var(--sl-t5); }
/* The glow belongs to the figure, never a box: a tier class on its own only colours.
   On the ribbon the blur radiates from the multiplier inside the flat dark tile; the
   history rail and the status line carry no glow at all. */
.sl_cell.sl_t3 b { text-shadow: 0 0 14px color-mix(in srgb, var(--sl-t3) 60%, transparent); }
.sl_cell.sl_t4 b { text-shadow: 0 0 18px color-mix(in srgb, var(--sl-t4) 75%, transparent); }
.sl_cell.sl_t5 b {
  text-shadow:
    0 0 12px color-mix(in srgb, var(--sl-t5) 85%, transparent),
    0 0 28px color-mix(in srgb, var(--sl-t5) 55%, transparent);
}

/* Where the ribbon came to rest.
   Emphasis here is a *tell*, so it exists only after the fact: `.sl_landed` is put on by
   `landed()` in slide.js when the ribbon has already stopped, never laid down with the
   strip. A box that grew while it was still travelling towards the marker announced the
   result several seconds before the board did.

   The emphasis itself is gated by `.sl_settled` on the rail rather than living on the box:
   the model remembers which box landed across redraws, but the *look* — the winner grown a
   touch, brought up to full strength, its outline gone white (tinted by its tier) — is the
   state of a board at rest. Every other box simply stays at the dark baseline .sl_cell
   already is; nothing brightens for the spin. slide.js drops the class as the next slide
   begins, so the winner eases back into the strip before the ribbon moves, and the .25s
   transitions on .sl_cell animate both directions; there is no pop keyframe. */
.sl_settled .sl_landed {
  transform: scale(1.08);
  filter: none;
  border-color: color-mix(in srgb, #fff 72%, currentColor);
}

.sl_marker {
  position: absolute;
  left: 50%;
  top: 8px;
  bottom: 8px;
  width: 2px;
  margin-left: -1px;
  /* Two thin lines and nothing else — no arrowheads. The line is broken over the box so
     the multiplier under the marker stays readable: one segment drops from the top, one
     rises from the bottom, and each fades out short of the box's edge — the segments stop
     6px clear of the tile rather than touching it. */
  background:
    linear-gradient(to bottom, var(--secondary) 40%, transparent)
      top center / 1.1px calc(50% - var(--sl-box) / 2 - 6px) no-repeat,
    linear-gradient(to top, var(--secondary) 40%, transparent)
      bottom center / 1.1px calc(50% - var(--sl-box) / 2 - 6px) no-repeat;
  pointer-events: none;
}

/* The ribbon runs off both edges rather than stopping at them: a strip that ends in a hard
   line reads as a list, and this has to read as something passing through. */
.sl_fade {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 84px;
  pointer-events: none;
}
.sl_fade_l { left: 0; background: linear-gradient(to right, var(--sh-raise), transparent); }
.sl_fade_r { right: 0; background: linear-gradient(to left, var(--sh-raise), transparent); }

.sl_status {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 0 2px 8px;
}
.sl_phase { font-size: 13px; color: var(--sh-sub); }
.sl_phase b { color: var(--sh-head); font-family: var(--sh-mono); }
.sl_round_no { font: 600 11px/1 var(--sh-mono); color: var(--sh-mute); }

.sl_bar { height: 3px; background: var(--sh-field); border-radius: 2px; overflow: hidden; }
.sl_bar_fill {
  height: 100%;
  width: 0;
  background: var(--secondary);
  /* Matches the 100ms tick in slide.js, so the bar moves continuously instead of stepping. */
  transition: width .1s linear;
}
.sl_bar_done { background: var(--sh-line); }

/* -- the round modal ----------------------------------------------- */

.sl_modal_bg {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .66);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 60;
  padding: 20px;
}
.sl_modal_bg.sl_open { display: flex; }
.sl_modal {
  background: var(--sh-main);
  border: 1px solid var(--sh-line);
  border-radius: 10px;
  width: min(560px, 100%);
  max-height: min(84vh, 760px);
  display: flex;
  flex-direction: column;
  font-family: var(--sh-font);
}
.sl_modal_head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 16px 12px;
  border-bottom: 1px solid var(--sh-line);
}
.sl_modal_title { font: 700 16px/1.2 var(--sh-font); color: var(--sh-head); }
.sl_modal_sub { font-size: 11px; color: var(--sh-mute); margin-top: 3px; }
.sl_modal_x {
  background: none;
  border: 0;
  color: var(--sh-mute);
  font-size: 15px;
  cursor: pointer;
  padding: 2px 4px;
}
.sl_modal_x:hover { color: var(--sh-head); }
.sl_modal_body { overflow-y: auto; padding: 14px 16px 16px; }

.sl_modal_result { display: flex; align-items: baseline; gap: 10px; margin-bottom: 14px; }
.sl_modal_big { font: 700 30px/1 var(--sh-mono); }
.sl_modal_box { font-size: 11px; color: var(--sh-mute); }

/* The whole strip the round dealt, so the result reads in the company it landed in. Scrolls
   in its own box: 24 boxes will not fit a 560px modal, and the page must not grow a
   horizontal scrollbar because of it. The bar itself is hidden — a wheel, a drag, or the
   snap points still move it, and a bare scrollbar under five bordered tiles read as a stray
   rule rather than a control.

   Five across the width and the rest behind the scroll, rather than a dozen shrunk to fit.
   A box is a multiplier, and a row of a dozen 10px ones was a bar of hex nobody could pick a
   figure out of — at a fifth of the width each they are the size the strip on the board draws
   them, which is the size at which the landing reads as one of a run.

   Padding runs top and bottom alike: `overflow-x: auto` here also switches the box's own
   overflow-y to auto (only one axis may stay `visible`), and with no top padding that clipped
   the landed tile's outline ring against the box's own edge — the mult read as cut off along
   its top rather than framed. Margins above and below match the result and the "players"
   heading's own margin so the strip sits at even distance from both, once the two collapse
   against each other the way adjacent block margins do. */
.sl_modal_strip {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding: 4px 2px;
  margin-bottom: 14px;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
}
.sl_modal_strip::-webkit-scrollbar { display: none; }
.sl_mini {
  /* five tiles and the four gaps between them, exactly the box's width */
  flex: 0 0 calc((100% - 4 * 6px) / 5);
  scroll-snap-align: start;
  display: grid;
  place-items: center;
  background: var(--sh-field);
  border: 1px solid var(--sh-tile-line);
  border-radius: 6px;
  padding: 9px 4px;
  font: 600 13px/1 var(--sh-mono);
}
.sl_mini_hit { outline: 1px solid currentColor; outline-offset: 1px; }

.sl_modal_sect {
  font-size: 10px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--sh-mute);
  margin: 14px 0 8px;
}
.sl_modal_players { display: flex; flex-direction: column; gap: 2px; }

/* The proof fields are the bet slip's — `.bet_field` / `.bet_well` / `.bet_copy` out of
   dd.css, built by `dd.fairFieldsMarkup`. All this has to do is retune the two colours
   the slip scopes to its own card: those are picked against Discord's modal grey (#313338),
   and this card is the near-black table, so used unchanged the label would be brighter here
   than the section heading above it. The well keeps its own depth, one step under the card,
   which is the same relationship it has on the slip. */
.sl_modal {
  --bet-label: var(--sh-mute);
  --bet-value-dim: var(--sh-dim);
}
.sl_modal .bet_fields { margin-top: 0; }
.sl_modal .bet_well { background: var(--sh-field); border: 1px solid var(--sh-line); }
.sl_modal .bet_copy:hover { color: var(--sh-head); }

/* Only a failed commitment reaches this line now — a pass is what "how this is verified"
   already promises, so it needed no confirmation line under a hash nobody could read anyway. */
.sl_verdict { margin-top: 10px; font-size: 11px; line-height: 1.5; color: var(--sh-mute); }
.sl_verdict_bad { color: var(--sh-lose); font-weight: 700; }

/* Stacked and centred, the bet slip's own order: the thing that opens something above the
   thing that only explains. */
.sl_modal_foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  margin-top: 16px;
  text-align: center;
}
.sl_modal_foot .sl_chip { flex: 0 0 auto; padding: 8px 16px; }
/* The slip's own "provably fair" link, out of `.bet_hint a` in dd.css, restated as a
   standalone class: this card has no hint sentence to hang the link off of. */
.bet_hint_link {
  font-size: 10.5px;
  color: #e0e1e5;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.bet_hint_link:hover { color: var(--secondary); }

/* The activity list's badge for a game that is a shared table, because picking that row does
   something no other row does: it leaves the channel you are in. */
.sh_act_live {
  display: inline-block;
  margin-left: 8px;
  vertical-align: middle;
  background: color-mix(in srgb, var(--secondary) 22%, transparent);
  color: var(--secondary);
  border-radius: 999px;
  padding: 2px 7px;
  font: 700 9px/1.4 var(--sh-font);
  letter-spacing: .05em;
  text-transform: uppercase;
}

@media (prefers-reduced-motion: reduce) {
  .sl_rail { transition: none; }
  .sl_hchip_new { animation: none; }
  .sl_play_flash { animation: none; }
  .sl_cell { transition: none; }
  /* The figure still lands on the payout — animatePayouts writes it straight in. */
  .sl_amt_up { animation: none; }
}

/* One column under about a tablet: the board keeps the width and the panel goes above it.
   The cells shrink with the viewport, and slide.js reads --sl-cell at slide time, so the
   two stay in step across a resize. */
@media (max-width: 1000px) {
  .sl_wrap { grid-template-columns: minmax(0, 1fr); }
  .sl_side { flex-direction: row; flex-wrap: wrap; }
  .sl_panel { flex: 1 1 260px; }
  /* One column, so the list is back in the flow under the board and measures again — there is
     no side column left for it to hang beside. */
  .sl_seats { height: auto; }
  .sl_seats > .sl_players { position: static; }
  /* Side by side with the stake panel on a tablet, so the list gets five rows rather than
     ten — a whole number of them, for the same reason the wide cap is. */
  .sl_players_list { max-height: calc(var(--sl-prow) * 5); }
}

@media (max-width: 560px) {
  /* Same quarter-trimmed gap as the desktop pitch: 6px instead of the old 8px. */
  :root { --sl-cell: 88px; --sl-box: 82px; }
  .sh_table { padding: 10px; }
  .sl_board { min-height: 340px; padding: 10px; }
  .sl_track { min-height: 150px; }
  .sl_cell { font-size: 14px; border-radius: 6px; }
  .sl_fade { width: 44px; }
  .sl_side { flex-direction: column; }
}
