/* donutdice — components added on top of the prototype theme.
   One accent colour, sharp edges, no gradients: same language as theme.css. */

/* The top padding clears the fixed balance/profile boxes (16px inset + 54px tall),
   so nothing in the column ever slides under them. */
.dd_wrap { max-width: 1080px; margin: 0 auto; padding: 96px 20px 80px; position: relative; }
/* below 500px the topbar drops to the bottom of the screen, so the gap is not needed */
@media (max-width: 500px) { .dd_wrap { padding-top: 24px; padding-bottom: 110px; } }

/* The falling-money ascii field: a fixed layer filling everything right of the
   sidebar, so it covers the page rather than just the content column and re-fits
   itself on any viewport, zoom or sidebar change (the shader re-measures every
   frame). z-index -1 keeps it under the panels while staying above the page
   background — no ancestor may become a stacking context or this vanishes. */
.ascii_field {
  position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: -1; margin: 0;
  font-size: 11px; line-height: 1.25; letter-spacing: 0;
  white-space: pre; overflow: hidden;
  opacity: .42; user-select: none; pointer-events: none;
}
body.sidebar-open .ascii_field { left: 210px; }

/* A panel with no card around it: used for the bet feed, which reads better
   sitting straight on the page with the rain showing through. */
.dd_panel.bare { background: transparent; border: none; padding: 0; }

.dd_panel {
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 20px;
  margin-bottom: 16px;
}
.dd_panel h2 {
  font-size: 12px; letter-spacing: .24em; text-transform: lowercase;
  color: var(--text-muted); margin: 0 0 16px; font-weight: 500;
}

.dd_grid { display: grid; grid-template-columns: 320px 1fr; gap: 16px; align-items: start; }
@media (max-width: 860px) { .dd_grid { grid-template-columns: 1fr; } }

.dd_field { margin-bottom: 14px; }
.dd_field label {
  display: block; font-size: 10px; letter-spacing: .18em; text-transform: lowercase;
  color: var(--text-muted); margin-bottom: 6px;
}
.dd_input, .dd_select {
  width: 100%; background: var(--bg); border: 1px solid var(--border);
  color: var(--text); font-family: var(--font); font-size: 14px; padding: 10px 12px;
  outline: none;
}
.dd_input:focus, .dd_select:focus { border-color: var(--secondary); }
.dd_input.readonly { color: var(--text-muted); }

/* ================================================================
   the coin badge in an amount box, and the list it opens
   ----------------------------------------------------------------
   An amount box asks two things at once — how much, and out of which balance — so the
   coin lives inside the box rather than in a control beside it. It is parked at the
   trailing end and the input is given room for it, so the caret starts where it always
   did, the digits run left to right into empty box, and nothing is ever typed underneath
   the badge.

   Everything below is coin-agnostic on purpose: no rule names a currency, the rows are
   whatever the server said is enabled, and the mark is looked up per id with a fallback.
   A new coin needs nothing here. See dd.coinPicker.
   ================================================================ */
.dd_amount { position: relative; display: block; }
/* Room for the badge. The box is 100% wide and the badge is absolutely placed inside it,
   so this padding is the only thing keeping the two apart: at 44px the longest stake a
   player can type runs right up against the disc, and the extra four are the gap. */
.dd_amount .dd_input { padding-right: 48px; }
.coin_pick {
  position: absolute; right: 1px; top: 1px; bottom: 1px;
  display: flex; align-items: center;
  padding: 0 9px 0 7px;
}
.coin_pick_btn {
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  border: 0; background: transparent; cursor: pointer;
  border-radius: 50%;
  opacity: .92; transition: opacity .12s ease, background-color .12s ease;
}
.coin_pick_btn:hover, .coin_pick:focus-within .coin_pick_btn {
  opacity: 1; background: rgba(255, 255, 255, .06);
}
.coin_pick_btn .coin_mark { display: block; }

/* The list. Hung under the badge and right-aligned to it, so it opens into the panel
   rather than over the label above. Rounded and unruled — the site is square everywhere
   else, and this is a floating sheet rather than a panel in the layout, which is the one
   thing that reads better with a radius on it.

   Positioned `fixed`, with the coordinates written by dd.coinPicker from the badge's own
   rect. Absolute placement put the sheet inside whatever box the amount input lives in, and
   in the game shelf that box scrolls and clips: the list came out sliced at the panel edge.
   Fixed takes it out of every ancestor's overflow and out of their stacking contexts, so it
   draws over the whole page and the shelf can clip whatever it likes. */
.coin_pick_menu {
  position: fixed; top: 0; left: 0;
  min-width: 200px;
  padding: 5px;
  background: var(--surface, #202126);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 10px 26px rgba(0, 0, 0, .45);
  z-index: 4000;
  display: flex; flex-direction: column; gap: 2px;
  /* Shut by default, and shut to the layout as well: `visibility` keeps it out of the tab
     order and off the hit-testing while it is down, which `opacity: 0` alone would not. */
  opacity: 0; visibility: hidden;
  transform: translateY(-4px);
  transition: opacity .12s ease, transform .12s ease, visibility 0s linear .12s;
}
/* The gap the sheet is offset by is a hole in the hover target: the pointer leaving the
   badge crosses six pixels of nothing on its way down, and on a slow drag that is enough
   for :hover to drop and the list to shut under the cursor. This pad hangs off the sheet
   and fills the gap, so badge → pad → list is one unbroken surface. It only exists while
   the sheet is up: `visibility: hidden` on the parent takes it out of hit-testing too. */
.coin_pick_menu::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: -10px; height: 10px;
}
/* Hover opens it, and focus-within keeps it open for a keyboard. The whole .coin_pick is
   the hover target — badge and menu together, the menu still being a child of it however
   it is positioned — so the pointer can travel from one to the other without the list
   closing under it. */
.coin_pick:hover .coin_pick_menu,
.coin_pick:focus-within .coin_pick_menu,
.coin_pick_menu:hover {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity .12s ease, transform .12s ease, visibility 0s;
}
.coin_pick_row {
  display: flex; align-items: center; gap: 9px;
  width: 100%; padding: 7px 9px;
  border: 0; border-radius: 7px;
  background: transparent; color: var(--text);
  font-family: var(--font); font-size: 13px; text-align: left; cursor: pointer;
  transition: background-color .1s ease;
}
.coin_pick_row:hover:not(:disabled) { background: rgba(255, 255, 255, .07); }
/* The coin already in use. Highlighted and disabled together — it is the answer to the
   question the list is asking, so it is marked as chosen and there is nothing to press. */
.coin_pick_row.on {
  background: color-mix(in srgb, var(--secondary) 16%, transparent);
  color: var(--secondary);
  cursor: default;
}
.coin_pick_mark { display: flex; flex: 0 0 auto; }
.coin_pick_id { font-weight: 600; letter-spacing: .06em; }
/* The balance is the figure the eye goes to when comparing rows, so it holds the right
   edge and a tabular column rather than sitting wherever the ticker leaves it. */
.coin_pick_bal {
  margin-left: auto; color: var(--text-muted);
  font-variant-numeric: tabular-nums; font-size: 12px;
}
.coin_pick_row.on .coin_pick_bal { color: inherit; opacity: .85; }
@media (prefers-reduced-motion: reduce) {
  .coin_pick_menu, .coin_pick_row, .coin_pick_btn { transition: none; }
}

.dd_row { display: flex; gap: 8px; }
.dd_row > * { flex: 1; }

.dd_btn {
  width: 100%; padding: 14px; border: 1px solid var(--secondary);
  background: transparent; color: var(--secondary); font-family: var(--font);
  font-size: 13px; letter-spacing: .18em; text-transform: lowercase; cursor: pointer;
  transition: background .12s, color .12s;
}
.dd_btn:hover:not(:disabled) { background: var(--secondary); color: var(--bg); }
.dd_btn:disabled { opacity: .4; cursor: not-allowed; }
.dd_btn.secondary { border-color: var(--border); color: var(--text-muted); }
.dd_btn.secondary:hover:not(:disabled) { background: var(--border); color: var(--text); }
/* The play button while a round is open: grey rather than accented, since taking the
   money off the table is not the same invitation as starting a round.
   It is still the one live control on the panel while a stake is on the table, so it is
   drawn heavier than the rest — a two-pixel edge in the brightest grey the theme has,
   and the text at full strength. The old hairline in --border-hover put the one button
   that matters mid-round below the quick-stake chips in weight.
   The weight is an inset ring rather than a thicker border, so the button does not grow
   by two pixels — and push the panel down — the moment a round opens. */
.dd_btn.cashout {
  border-color: var(--text-dim); color: var(--text);
  box-shadow: inset 0 0 0 1px var(--text-dim);
}
.dd_btn.cashout:hover:not(:disabled) {
  border-color: var(--text); box-shadow: inset 0 0 0 1px var(--text);
  background: var(--border-hover); color: var(--text);
}

.dd_chip {
  display: inline-block; padding: 4px 8px; border: 1px solid var(--border);
  font-size: 11px; color: var(--text-muted); margin: 0 6px 6px 0; cursor: pointer;
  background: transparent; font-family: var(--font);
}
.dd_chip.on { border-color: var(--secondary); color: var(--secondary); }

.dd_result { text-align: center; padding: 28px 12px; min-height: 150px; }
.dd_result .big { font-size: 40px; letter-spacing: .04em; margin-bottom: 6px; }
.dd_result .win { color: var(--secondary); }
.dd_result .lose { color: var(--text-muted); }
.dd_result .sub { font-size: 12px; color: var(--text-muted); letter-spacing: .1em; }

.dd_meta {
  display: flex; flex-wrap: wrap; gap: 18px; font-size: 11px;
  color: var(--text-muted); border-top: 1px solid var(--border);
  padding-top: 12px; margin-top: 16px;
}
.dd_meta b { color: var(--text); font-weight: 400; }

.dd_table { width: 100%; border-collapse: collapse; font-size: 12px; }
.dd_table th, .dd_table td {
  text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border);
}
.dd_table th {
  font-size: 10px; letter-spacing: .16em; color: var(--text-muted);
  text-transform: lowercase; font-weight: 500;
}
.dd_table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.dd_mono { font-family: var(--font); word-break: break-all; }

.dd_note { font-size: 11px; color: var(--text-muted); line-height: 1.7; margin-top: 10px; }
.dd_note code { color: var(--text); }

/* Inline form error: sits inside the panel it belongs to, under the fields, so a
   missed requirement is read where it was caused. Hidden until there is something
   to say, and it does not reserve space while empty. */
.dd_error {
  display: none;
  border-left: 2px solid var(--danger);
  background: rgba(207, 147, 224, .07);
  color: var(--danger);
  font-size: 11.5px; line-height: 1.6; letter-spacing: .02em;
  padding: 9px 12px; margin: 0 0 14px;
}
.dd_error.show { display: block; }

/* The toast slides down out of the viewport and is then gone.
   `translateY(120%)` was not enough travel to clear `bottom: 24px` — 120% of the toast's
   own height is a couple of dozen pixels short of the gap plus the box — so a dismissed
   message left a sliver of itself parked along the bottom edge for the rest of the page's
   life. It now travels its full height *plus* that gap, and fades and goes
   `visibility: hidden` at the end of the slide, so it cannot be seen or clicked whatever
   the box ends up measuring. */
.dd_toast {
  position: fixed; bottom: 24px; left: 50%;
  transform: translateX(-50%) translateY(calc(100% + 24px));
  background: var(--surface); border: 1px solid var(--border); color: var(--text);
  padding: 12px 20px; font-size: 12px; z-index: 9999;
  max-width: 90vw;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: transform .18s, opacity .18s, visibility 0s linear .18s;
}
.dd_toast.show {
  transform: translateX(-50%) translateY(0);
  opacity: 1; visibility: visible;
  transition: transform .18s, opacity .18s, visibility 0s;
}
.dd_toast.error { border-color: var(--danger); color: var(--danger); }
.dd_toast.win { border-color: var(--secondary); color: var(--secondary); }

/* mines / towers boards. The cap is what keeps the tiles square and legible in a wide
   panel; it is deliberately generous, because a 5x5 board at 380px reads as a grid of
   thumbnails on a desktop-width stage. */
.dd_board { display: grid; gap: 8px; margin: 0 auto; max-width: 480px; }
.dd_board.mines { grid-template-columns: repeat(5, 1fr); }
.dd_tile {
  aspect-ratio: 1; border: 1px solid var(--border); background: var(--bg);
  color: var(--text-muted); font-family: var(--font); font-size: 18px; cursor: pointer;
  display: flex; align-items: center; justify-content: center; transition: border-color .1s;
}
.dd_tile:hover:not(:disabled) { border-color: var(--secondary); }
.dd_tile:disabled { cursor: default; }
/* Tiles are glyphless, so a cleared tile needs the fill as well as the border to read as
   revealed rather than merely hovered. */
.dd_tile.safe {
  border-color: var(--secondary); color: var(--secondary);
}
/* the only red on the board — see the --mine note in theme.css */
.dd_tile.mine {
  border-color: var(--mine); color: var(--mine);
  background: color-mix(in srgb, var(--mine) 14%, var(--bg));
}
.dd_tile.dim { opacity: .35; }
/* A mine on a row the climb never reached, after it ended on one. It is red but it is not
   .mine: the tile the player actually clicked has to stay the loudest thing on the board, so
   this is a wash and a tinted border at roughly half that strength, read through the dim the
   row already carries. Enough to see the tower that was waiting, not enough to compete with
   what happened. */
.dd_tile.mine_hint {
  border-color: color-mix(in srgb, var(--mine) 60%, var(--border));
  background: color-mix(in srgb, var(--mine) 32%, var(--bg));
}
/* A towers tile on a row the player has already climbed past and did not stand on. Solid,
   at full strength, and a shade above the resting tile: the rows behind the player are the
   floor of the board, and it is against them that the rows above read as untouched. */
.dd_tile.spent {
  opacity: 1;
  background: color-mix(in srgb, var(--text) 12%, var(--bg));
  border-color: color-mix(in srgb, var(--text) 20%, var(--border));
}

/* -- towers ---------------------------------------------------------------- */
/* The stage holds the board alone — the mode buttons are in the stake panel with the
   stake. The board takes the room it is given up to a cap: a four-wide tower with tiles
   any wider reads as a wall. Nothing here may state a width the stage cannot shrink
   below, or the column stops fitting the page and the whole layout scrolls sideways. */
.towers_stage {
  display: flex; align-items: flex-start; justify-content: center;
  max-width: 900px; margin: 0 auto;
}
.towers_board { flex: 1 1 240px; min-width: 0; max-width: 424px; }

/* Same gap as the mines board, so the two grids read as one system. */
.dd_row_tiles { display: flex; gap: 8px; justify-content: center; margin-bottom: 8px; }
/* A towers tile is a mines tile, a little wider: at these caps a 4-wide tower row and a
   5-wide mines board land tiles of the same height (~90px), with the tower's extra width
   carrying the money figure. The figure is small and pushed back to a quiet grey on
   purpose — the tile is the thing being pointed at, and a bright number that filled it
   would read as a button label. */
.dd_row_tiles .dd_tile {
  flex: 1 1 0; min-width: 0; width: auto; aspect-ratio: 10 / 9; height: auto;
  font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums;
  letter-spacing: -.01em; padding: 0 2px; overflow: hidden;
  color: color-mix(in srgb, var(--text-muted) 55%, var(--bg));
}
/* The row being played carries no lightening of its own: it is simply the one row not
   dimmed, exactly as an unrevealed mines tile is simply the one not yet turned, and its
   figures stay the quiet grey above. A turned tile takes the mines colours whole — lilac
   or red, border and text alike; restated here only because the row's grey figure rule
   above would otherwise win on file order. */
.dd_row_tiles .dd_tile.safe { color: var(--secondary); }
.dd_row_tiles .dd_tile.mine { color: var(--mine); }

/* ---- the segmented control -------------------------------------------------------------
   One group, one named choice per row, and a highlight that slides between them: towers'
   mode, plinko's and keno's risk, roulette's colour and parity. See segment() in
   assets/game.js — it sets --seg-n (how many) and --seg-i (which is lit), and every bit of
   the geometry below is arithmetic on those two, so the highlight is never measured and is
   in the right place on the first frame even though the shelf builds the panel detached.

   Sized in percentages of the track, so the options must share the group evenly — that is
   what makes `translate: i * 100%` land exactly on option i. */
.dd_seg {
  --seg-pad: 4px;
  --seg-i: 0;
  position: relative;
  display: flex;
  flex-direction: row;
  padding: var(--seg-pad);
  border: 1px solid var(--border);
  background: var(--bg);
  border-radius: 8px;
}
.dd_seg_v { flex-direction: column; }

.dd_seg_hl {
  position: absolute;
  top: var(--seg-pad);
  left: var(--seg-pad);
  width: calc((100% - var(--seg-pad) * 2) / var(--seg-n));
  height: calc(100% - var(--seg-pad) * 2);
  border: 1px solid var(--secondary);
  background: rgba(185, 160, 242, .06);
  border-radius: 6px;
  pointer-events: none;
  /* Nothing picked: no frame. It swells into existence on the first press. */
  opacity: 0;
  scale: .88;
  translate: calc(var(--seg-i) * 100%) 0;
  /* `translate` and `scale` rather than one `transform`, so the slide and the appearance
     are separate properties: suppressing the slide for a frame (.dd_seg_jump) must not
     also suppress the swell that follows it. */
  transition: translate .24s cubic-bezier(.2, .8, .3, 1), opacity .16s ease, scale .16s ease;
}
.dd_seg_v .dd_seg_hl {
  width: calc(100% - var(--seg-pad) * 2);
  height: calc((100% - var(--seg-pad) * 2) / var(--seg-n));
  translate: 0 calc(var(--seg-i) * 100%);
}
.dd_seg.picked .dd_seg_hl { opacity: 1; scale: 1; }
.dd_seg_hl.dd_seg_jump { transition: none; }

.dd_seg_opt {
  position: relative;
  z-index: 1;
  flex: 1 1 0;
  min-width: 0;
  padding: 11px 7px;
  border: 0;
  background: none;
  text-align: center;
  cursor: pointer;
  color: var(--text);
  font: 500 14px/1 var(--font);
  letter-spacing: .04em;
  transition: color .16s ease;
}
.dd_seg_opt:hover:not(:disabled):not(.on) { color: var(--secondary); }
.dd_seg_opt.on { color: var(--secondary); }
.dd_seg.off { opacity: .5; }
.dd_seg.off .dd_seg_opt { cursor: default; }

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

@media (max-width: 720px) {
  .towers_board { max-width: none; }
}

/* -- the climb ------------------------------------------------------------- */
/* A tile that has just been turned over, and one step of a climb: the row being left runs
   down while the row being arrived at runs up, over the same 180ms, so the ladder moves
   once instead of blinking twice. See markMovedRow() in games/towers.js for why these are
   `from`-only keyframes — the board is rebuilt on every response, so there is no previous
   value to transition from, and each animation instead runs from a stated start into
   whatever the tile's own resting style is. The tile that was actually clicked is excluded
   from the dim: it has its own pop, and it is turning its colour rather than going out. */
.dd_tile.pop { animation: dd_tile_pop .26s cubic-bezier(.2, .9, .3, 1.4); }
@keyframes dd_tile_pop { from { transform: scale(.72); } to { transform: scale(1); } }
.dd_row_tiles.dimming .dd_tile:not(.safe):not(.mine) {
  animation: dd_row_dim .18s cubic-bezier(.4, 0, .6, 1);
}
@keyframes dd_row_dim {
  /* the row being left, as it rested while current: dark border, quiet grey figure */
  from { opacity: 1; border-color: var(--border); color: color-mix(in srgb, var(--text-muted) 55%, var(--bg)); }
}
.dd_row_tiles.lighting .dd_tile { animation: dd_row_lit .18s cubic-bezier(.4, 0, .6, 1); }
@keyframes dd_row_lit {
  from { opacity: .35; border-color: var(--border); color: var(--text-muted); }
}
@media (prefers-reduced-motion: reduce) {
  .dd_tile.pop,
  .dd_row_tiles.dimming .dd_tile,
  .dd_row_tiles.lighting .dd_tile { animation: none; }
}

/* plinko buckets, keno grid, roulette wheel */
.dd_buckets { display: flex; gap: 3px; justify-content: center; flex-wrap: wrap; }
.dd_bucket {
  min-width: 42px; padding: 6px 4px; border: 1px solid var(--border);
  font-size: 10px; text-align: center; color: var(--text-muted);
}
.dd_bucket.hit { border-color: var(--secondary); color: var(--secondary); }

/* Still capped, but far higher than before: an 8-column grid has to stay square-ish, and
   the cap is what stops 40 numbers turning into 40 postage stamps on an ultrawide. The
   tiles carry the growth, not the gaps. The risk tier is in the stake panel with the
   stake, on the shared segmented control (.dd_seg above). */
.dd_keno {
  display: grid; grid-template-columns: repeat(8, 1fr); gap: 6px;
  max-width: 920px; margin: 0 auto;
}
/* The board rests white — the same border and figure as a live towers row — so forty
   legible numbers read as one object rather than a dark field. Against that floor the
   states subtract, and the picked numbers are the only lilac on the board. */
.dd_keno .dd_tile {
  font-size: clamp(15px, 1.5vw, 22px);
  border-color: color-mix(in srgb, var(--text) 28%, var(--border));
  color: var(--text);
}

.dd_tile.picked { border-color: var(--secondary); color: var(--secondary); }

/* A drawn number the player was not on: it loses the white and is pushed back further
   than the resting board, and left that way for the rest of the round, so the ones they
   were on are the only readable things on the board once the draw has run. */
.dd_keno .dd_tile.drawn {
  border-color: var(--border); color: var(--text-muted);
  background: var(--surface-light, var(--surface));
  opacity: .16;
  animation: keno_drawn .25s steps(3);
}
/* a hit — their number, and it came out. This is the one tile on the board that fills,
   and it fills as it lands: the outline floods with its own colour. */
.dd_keno .dd_tile.picked.drawn {
  background: var(--secondary); border-color: var(--secondary); color: var(--bg);
  opacity: 1;
  animation: keno_fill .32s cubic-bezier(.2, .9, .3, 1.2);
}
@keyframes keno_drawn { from { opacity: 1 } to { opacity: .16 } }
@keyframes keno_fill {
  0%   { background: var(--bg); color: var(--secondary); transform: scale(1); }
  45%  { background: var(--secondary); color: var(--bg); transform: scale(1.14); }
  100% { background: var(--secondary); color: var(--bg); transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .dd_keno .dd_tile.drawn, .dd_keno .dd_tile.picked.drawn { animation: none; }
}

/* The prize boxes, under the divider: what each hit count pays at the current pick
   count. The box for the number of hits landed so far lights as the draw runs. */
.round_strip.keno_pays {
  gap: 6px; letter-spacing: normal;
  max-width: 560px; margin-left: auto; margin-right: auto;
}
.keno_pay {
  flex: 1 1 74px; min-width: 74px; padding: 7px 8px; text-align: center;
  border: 1px solid var(--border); background: var(--bg);
  transition: border-color .15s, background .15s;
}
.keno_pay span { display: block; font-size: 10px; color: var(--text-muted); }
/* qualified past `.round_strip b`, which the strip sets for progress figures */
.round_strip .keno_pay b {
  display: block; margin-top: 3px; font-size: 13px; color: var(--secondary);
  font-variant-numeric: tabular-nums;
}
.keno_pay.on { border-color: var(--secondary); background: rgba(185, 160, 242, .08); }
.keno_pay.on span { color: var(--secondary); }
.keno_pay.empty { opacity: .45; }
.round_strip .keno_pay.empty b { color: var(--text-muted); }

.dd_reels { display: flex; gap: 10px; justify-content: center; }
.dd_reel {
  width: 92px; height: 92px; border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center; font-size: 15px;
  color: var(--text);
}
.dd_reel.win { border-color: var(--secondary); color: var(--secondary); }

/* ================================================================
   roulette — the wheel is gone: pockets ride a strip of square tiles that slides under
   a fixed marker, the same movement the slide table makes. Tiles alternate white and
   black, each carrying a real pocket number of that colour; zero is the accent and only
   ever appears as the tile the strip lands on. The strip is the whole board — the bet
   boxes that used to sit under it are gone, and what a bet pays is in the panel's pays
   row where every other game states it.
   ================================================================ */
/* The board is centred on the tiles, and on the tiles alone.
   The stage becomes a flex box (the class is put on by buildStage) so the strip sits in
   the middle of whatever height the panel has, and the marker hangs off the frame's top
   edge — outside its box, contributing no height — so the triangle is drawn above the row
   without being one of the things the centring has to account for. */
.dd_result.roul_stage { display: flex; align-items: center; justify-content: center; }
/* The rail is 24 tiles long — some 3200px — and although `.roul_win` clips it, that width
   is still what the column reports as the narrowest it can be. Left alone the board column
   is blown out to it, the panel grows wider than the window holding it, and the part of the
   strip you can actually see is off-centre with the marker near its right edge. The column
   is told it may be as narrow as it likes; the strip then takes the room there is and the
   tiles run off both edges under the fades, which is what they are meant to do. */
.dd_grid > div:has(.roul_stage) { min-width: 0; }
.roul_strip { position: relative; width: 100%; max-width: 560px; }
.roul_frame { position: relative; width: 100%; }
/* The pointer the tiles pass under. `bottom: 100%` hangs it entirely outside the frame:
   it sits just above the row and contributes no height, so what the stage centres is the
   row of squares and the triangle rides along wherever that lands. */
.roul_marker {
  position: absolute; bottom: 100%; left: 50%; margin-bottom: 3px;
  transform: translateX(-50%); z-index: 2;
  width: 0; height: 0;
  border-left: 6px solid transparent; border-right: 6px solid transparent;
  border-top: 11px solid var(--secondary);
}
.roul_win { position: relative; overflow: hidden; padding: 4px 0; }
.roul_rail { display: flex; gap: 6px; will-change: transform; }
/* tile + gap = 64px of pitch; roulette.js positions the rail arithmetically off the
   same numbers, so neither may change without the other */
.roul_tile {
  flex: 0 0 58px; width: 58px; height: 58px;
  display: flex; align-items: center; justify-content: center;
  font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums;
  transition: transform .25s ease, border-color .25s ease;
}
/* No fills: a tile is its number and a border in the same colour — white on white,
   black on black — so the strip reads as a row of stamps rather than a row of blocks. */
.roul_tile.white { background: transparent; border: 1px solid var(--text); color: var(--text); }
.roul_tile.black { background: transparent; border: 1px solid #000; color: #000; }
.roul_tile.green {
  background: transparent; border: 1px solid var(--secondary);
  color: var(--secondary);
}
/* Where the strip came to rest, marked the way slide marks its landing: the outline goes
   accent and the tile grows a touch. A transform is drawn and not laid out, so the tiles
   either side of the winner do not move — and `z-index` keeps the grown edges over its
   neighbours rather than under them. */
.roul_tile.hit {
  border-color: var(--secondary);
  transform: scale(1.1);
  position: relative; z-index: 1;
}
/* The strip runs off both edges rather than stopping at them, the way slide's ribbon
   does: the panel's own grey at the edge, thinning to nothing by the time it reaches the
   tiles. It is the surface closing over them, not a hole cut in it — see --roul-fade,
   which each context sets to the colour of the panel actually behind the strip. */
.roul_fade { position: absolute; top: 0; bottom: 0; width: 72px; pointer-events: none; z-index: 1; }
.roul_fade.l {
  left: 0;
  background: linear-gradient(90deg, var(--roul-fade, var(--surface)), transparent);
}
.roul_fade.r {
  right: 0;
  background: linear-gradient(270deg, var(--roul-fade, var(--surface)), transparent);
}

/* The four even-money bets are two segmented controls now — white/black over even/odd,
   built by games/roulette.js — so they take .dd_seg above and need nothing here. */

.dd_empty { color: var(--text-muted); font-size: 12px; padding: 20px 0; text-align: center; }

/* ================================================================
   the bet column — the bet panel and the round box, stacked. A rail of the last four
   settled rounds used to sit between them; it is gone (see the note in game.js), and with
   it the odd 7px margin it carried. The two boxes are now one gap apart, and it is the
   *same* gap the column keeps from the board beside it: the grid's own, inherited, so the
   space around the round box is even on every side by construction rather than by a number
   picked to look right next to another number.
   ================================================================ */
.bet_col > .dd_panel { margin-bottom: 0; }
.bet_col { display: flex; flex-direction: column; gap: inherit; }

/* The rail is absolutely positioned over this panel, so the panel is what has to leave it
   room: 6px above the chips, the 19px chip, 6px under them. Without this the top of the
   board (mines' first tile row, which starts at the panel's own padding) is drawn under
   the figures. Reserved on the panel rather than on each board so every game gets it. */
.stage_panel { position: relative; padding-top: 31px; }

/* ================================================================
   the strip under the board — nothing in the shell writes here any more (the round's own
   figures are in the round box, in the bet column). It stays for the games that draw
   their own content under the board: keno's prize boxes. Empty, it is not there at all —
   no rule, no height — so a game that never uses it has no line under its board.
   ================================================================ */
.round_strip:empty { display: none; }
.round_strip {
  display: flex; flex-wrap: wrap; gap: 18px; justify-content: center;
  min-height: 34px; padding-top: 14px; margin-top: 4px;
  border-top: 1px solid var(--border);
  font-size: 11px; letter-spacing: .1em; color: var(--text-muted);
}

/* ================================================================
   the round box — under the bet column: what the round is worth right now and what it
   would pay. Drawn as the channel embed draws the same two figures (.sh_embed_stats /
   .sh_stat in app.css), because it is the same statement, and a player who has learnt to
   read one should not have to learn the other.
   ================================================================ */
/* Tighter than a panel of controls: two figures need the height of two figures, and every
   pixel here is one the board above does not get. */
.round_box {
  display: flex; flex-wrap: wrap; gap: 4px 18px;
  padding: 9px 11px;
}
.round_stat { display: flex; flex-direction: column; gap: 1px; }
/* Labels are furniture and never take the result's colour — only the figures move. */
.round_stat span { font-size: 10px; letter-spacing: .12em; color: var(--text-muted); }
.round_stat b {
  font-size: 13px; font-weight: 600; color: var(--text);
  font-variant-numeric: tabular-nums;
  transition: color .35s ease;
}
.round_stat b.pos { color: var(--secondary); }
/* 0.00x, and a round that has not paid anything: the figure it is, in the grey it is
   worth, so it changes colour when it changes meaning and not before. */
.round_stat b.muted { color: var(--text-muted); }
/* JS parks a figure here for one frame while it sets where a tone change starts from —
   see animateStatTone() in game.js, which mirrors armTone() in app.js for the embed. */
.round_stat b.tone_hold { transition: none; }
@media (prefers-reduced-motion: reduce) { .round_stat b { transition: none; } }
.round_strip b { color: var(--text); font-weight: 400; font-variant-numeric: tabular-nums; }
.round_strip b.pos { color: var(--secondary); }
.round_strip b.muted { color: var(--text-muted); }

/* The bet panel's result line carries no rule of its own: it is the last thing in the
   panel now, and a divider above a line that is empty between rounds is just a line. */
.bet_meta { border-top: none; padding-top: 0; margin-top: 12px; }

/* ================================================================
   recent bets — the rail in the stage panel's top-right corner, the same read slide's
   round rail gives: bare multiplier figures, coloured off the shared tier ramp, newest
   at the right. Absolutely positioned over the panel so it takes part in no layout —
   nothing under it moves whether it holds nine chips or none. Each chip is a button
   that opens that bet's slip.

   The rail is bounded by the panel it sits on — `left` as well as `right`, so its width is
   the stage's width and not however wide eleven chips happen to be. What does not fit is not
   drawn (drawRecent measures and drops the oldest chips), so the rail never reaches back
   over the bet column beside it; `overflow: hidden` is only the backstop for the frame
   between a resize and the redraw.
   ================================================================ */
.game_recent {
  position: absolute; top: 6px; left: 8px; right: 8px; z-index: 4;
  display: flex; gap: 2px; justify-content: flex-end;
  overflow: hidden;
  pointer-events: none;
}
/* Bare figures, not boxes — the colour ramp is the whole design, as on slide. The
   padding is kept as the hover/click target; hover just brings the figure up brighter.
   `flex: none` matters beyond taste: the chips are measured to decide how many fit, and a
   chip the flex line has squeezed measures narrower than it will draw. */
.game_recent_chip {
  pointer-events: auto; flex: none; white-space: nowrap;
  background: none; border: 0; padding: 4px 6px;
  font: 600 11px/1 var(--font);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: filter .12s ease;
}
.game_recent_chip:hover { filter: brightness(1.45); }
/* A fresh result arrives at the right end and the older chips shuffle left; only the
   chip that just landed animates in. */
.game_recent_new { animation: game_recent_in .38s cubic-bezier(.2, .8, .3, 1); }
@keyframes game_recent_in {
  from { opacity: 0; transform: translateX(16px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .game_recent_new { animation: none; }
}

/* ================================================================
   bet slip — opened from a chip, or from a /#bet=<id> link. Shares the sign-up
   card's rounded language; those two are the only rounded things on the site.
   ================================================================ */
/* No backdrop-filter.
 *
 * The slip opens over the chat client, which is a full-viewport stack of scrollers,
 * avatars and animated cards. Blurring that costs a full-page offscreen composite per
 * frame, and *transitioning* the blur radius asks for a fresh one every frame for the
 * whole animation — which is why the slip used to crawl on the way in. A flat scrim is
 * one solid layer and opens instantly. */
.modal_overlay {
  position: fixed; inset: 0; z-index: 10001;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, .72);
  opacity: 0; visibility: hidden;
  transition: opacity .12s ease, visibility 0s linear .12s;
}
.modal_overlay.open {
  opacity: 1; visibility: visible;
  transition: opacity .12s ease, visibility 0s;
}
/* Discord's own modal grey — a step lighter than the surface behind it, so the slip
   reads as a card lifted off the client rather than a hole cut in it. */
.bet_modal {
  /* The card sits on #313338, not the near-black page, so the site's --text-muted and
     --text-dim have far too little contrast against it. The slip uses its own pair. */
  --bet-label: #a8abb3;
  --bet-value-dim: #cbcdd3;
  width: min(420px, 94vw); position: relative;
  background: #313338; border: none;
  border-radius: 8px; overflow: hidden;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
  padding: 20px 20px 18px;
  /* Only opacity and transform animate, so the whole open is one composited step with
     no layout or paint behind it. */
  opacity: 0; transform: scale(.97);
  transition: opacity .14s ease, transform .14s cubic-bezier(.2, .8, .3, 1);
  will-change: opacity, transform;
}
.modal_overlay.open .bet_modal { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .modal_overlay, .bet_modal { transition-duration: .01s; }
  .bet_modal { transform: none; }
}

.bet_head {
  display: flex; align-items: center; gap: 12px;
  padding-right: 28px; margin-bottom: 18px;
}
.bet_game {
  font-size: 14px; letter-spacing: .2em; text-transform: lowercase;
  color: var(--text); font-weight: 700;
}
/* the id is the copy control: clicking it puts the bet's link on the clipboard.
   No box around it — it reads as part of the heading, not as a button. */
.bet_id {
  display: inline-flex; align-items: center; gap: 8px;
  background: none; border: none; padding: 0;
  font-family: var(--font); font-size: 11px; color: var(--bet-label);
  cursor: pointer; transition: var(--transition);
}
.bet_id:hover { color: var(--text); }
.bet_id svg { color: var(--bet-label); flex: 0 0 auto; }
.bet_id:hover svg { color: var(--secondary); }
.bet_id.copied { color: var(--secondary); }
.bet_id.copied svg { color: var(--secondary); }

/* The round's figures: one column each, label over value, centred in its own share of
   the width. Nothing is ruled between them — see the note in buildBetModal. */
.bet_grid {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  /* Top-aligned: a money column is two lines tall and a multiplier one, and hanging them
     from the same top keeps every label on one line across the card. */
  align-items: start;
  gap: 4px;
  background: #2b2d31;
  border-radius: 6px;
  padding: 14px 8px;
}
.bet_cell[hidden] { display: none; }
.bet_cell {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  min-width: 0; text-align: center;
  font-size: 10px; letter-spacing: .08em;
  color: var(--bet-label);
}
/* The label is lowercased, not the cell: "LTC" is a ticker and has to keep its case. */
.bet_cell > span { text-transform: lowercase; }
.bet_cell b {
  color: var(--text); font-weight: 600; font-size: 14px;
  font-variant-numeric: tabular-nums;
  /* A coin amount is long and must not push the columns out of even thirds. */
  max-width: 100%; overflow-wrap: anywhere; line-height: 1.25;
}
.bet_cell b .bet_unit {
  display: block; font-style: normal; font-weight: 500;
  font-size: 9px; letter-spacing: .1em; color: var(--bet-label); margin-top: 3px;
}
.bet_cell.payout.win b { color: var(--secondary); }
/* The unit stays furniture even when the figure above it turns lilac on a win. */
.bet_cell.payout.win b .bet_unit { color: var(--bet-label); }

/* The proof material, one field per value: the name of the thing, and under it the value
   in a well sunk below the card with a copy button at its end.

   A 64-character hex seed is not something anyone reads — it is checked against something
   else — so the field's job is to hand it over intact, which is what the button is for. It
   was set as a right-aligned value on the label's own line before, where the only way to
   fit it was to let it wrap into three or four lines of hex that dominated the card and
   still could not be used for anything without selecting them by hand. Truncated to the
   width of the well it reads as a value, and the copy button gives you all of it. */
.bet_fields { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }
.bet_field[hidden] { display: none; }
.bet_field > label {
  display: block; margin-bottom: 5px;
  font-size: 10px; letter-spacing: .1em; text-transform: lowercase;
  color: var(--bet-label);
}
/* Darker than the figures box above it, which is itself darker than the card: three
   depths, and the deepest is the one holding the material you take away with you. */
.bet_well {
  display: flex; align-items: center; gap: 8px; min-height: 34px;
  background: #1e1f22; border-radius: 4px; padding: 9px 10px;
}
.bet_well_text {
  flex: 1; min-width: 0;
  font-size: 11.5px; color: var(--bet-value-dim);
  font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Nothing to hand over yet. Said in the label's grey rather than the value's, so the
   field reads as empty rather than as holding a short value. */
.bet_well.pending .bet_well_text { color: var(--bet-label); font-style: italic; }
.bet_copy {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 22px; height: 22px; padding: 0;
  background: none; border: none; border-radius: 3px;
  color: var(--bet-label); cursor: pointer;
  transition: color .12s ease, background .12s ease;
}
.bet_copy:hover { color: var(--text); background: rgba(255, 255, 255, .06); }
.bet_copy.copied { color: var(--secondary); }

/* Rotate: the one thing on the slip that does anything, and the answer to the only
   question the card leaves open — where the server seed is. Drawn the way the channel
   card's cash-out is, a word in a colour with nothing filled behind it, but ringed,
   because it is standing on its own at the foot of a card rather than under a board that
   has already explained itself. Lilac rather than that button's green: green is the
   colour of walking away with money, and this is not that. */
.bet_rotate {
  width: 100%; display: grid; place-items: center;
  height: 36px; margin-top: 16px;
  background: transparent; border: 1px solid var(--secondary); border-radius: 4px;
  font-family: inherit; font-size: 11px; font-weight: 500; letter-spacing: .07em;
  color: var(--secondary); cursor: pointer;
  transition: background .14s ease, color .14s ease;
}
.bet_rotate[hidden] { display: none; }
.bet_rotate:hover:not(:disabled) { background: var(--secondary); color: #1e1f22; }
.bet_rotate:disabled { opacity: .45; cursor: default; }

/* The one rule on the card, and it earns its keep: above it is what the round did,
   below it is the proof that it was not rigged. */
.bet_fair {
  margin-top: 16px; border-top: 1px solid #3f4147; padding-top: 14px;
  container-type: inline-size;
}
.bet_fair_head {
  font-size: 10px; letter-spacing: .24em; text-transform: lowercase;
  color: var(--bet-label); margin-bottom: 4px;
}
.bet_hint {
  font-size: 10.5px; line-height: 1.7; color: var(--bet-value-dim);
  margin-top: 10px; text-align: center;
}
/* The sentence sits on one line whatever the modal's width, with the link centred
   under it; the type shrinks with the container rather than wrapping. */
.bet_hint_line {
  display: block; white-space: nowrap;
  font-size: min(10.5px, 2.65cqw);
}
/* The link is the one thing on the card a player is meant to click, so it is the
   brightest of the small type, not the faintest. */
.bet_hint a {
  display: inline-block; margin-top: 6px;
  color: #e0e1e5; text-decoration: underline; text-underline-offset: 3px;
}
.bet_hint a:hover { color: var(--secondary); }

/* ================================================================
   signup modal — opens when a signed-out visitor clicks balance/profile.
   deliberately the one rounded thing on the site.
   ================================================================ */
.signup_overlay {
  position: fixed; inset: 0; z-index: 10001;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0);
  -webkit-backdrop-filter: blur(0px); backdrop-filter: blur(0px);
  visibility: hidden;
  transition: background .5s ease, backdrop-filter .5s ease,
    -webkit-backdrop-filter .5s ease, visibility 0s linear .5s;
}
.signup_overlay.open {
  visibility: visible;
  background: rgba(0, 0, 0, .55);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  transition: background .5s ease, backdrop-filter .5s ease,
    -webkit-backdrop-filter .5s ease, visibility 0s;
}
.signup_modal {
  width: min(920px, 94vw); min-height: 540px;
  display: flex; position: relative;
  background: var(--surface);
  border: none;
  border-radius: 16px; overflow: hidden;
  opacity: 0; transform: scale(.96) translateY(10px);
  transition: opacity .3s ease .12s, transform .3s ease .12s;
}
.signup_overlay.open .signup_modal { opacity: 1; transform: none; }
.signup_close {
  position: absolute; top: 12px; right: 14px; z-index: 3;
  background: none; border: none; color: var(--text-dim);
  font-size: 18px; cursor: pointer; padding: 6px; line-height: 1;
}
.signup_close:hover { color: var(--text); }

.signup_left {
  position: relative; flex: 0 0 40%;
  background: #000; overflow: hidden;
  display: flex; flex-direction: column; align-items: center;
  padding: 30px 20px 22px;
  border-right: 1px solid var(--border);
}
/* the card's field carries its own per-shape tints now, so it is dimmed as a whole
   rather than being painted one flat grey */
.signup_bg {
  position: absolute; inset: 0; margin: 0; z-index: 0;
  font-size: 10px; line-height: 1; letter-spacing: 0;
  white-space: pre; overflow: hidden; opacity: .42;
  user-select: none; pointer-events: none;
}
/* The wordmark is the sidebar's — six short lines rather than the tall block that
   used to be here, so it is set larger to hold the same weight in the panel. */
.signup_logo {
  position: relative; z-index: 1;
  font-size: clamp(9px, 2.4vw, 13px); line-height: 1.15; color: var(--text);
  white-space: pre; margin: 14px 0 0; text-align: left;
  text-shadow: 0 0 8px #000, 0 0 3px #000;
}
.signup_disclaimer {
  position: relative; z-index: 1; margin-top: auto;
  font-size: 9.5px; line-height: 1.75; color: var(--text-dim);
  text-align: center; max-width: 270px;
  text-shadow: 0 0 8px #000, 0 0 4px #000, 0 0 2px #000;
}
.signup_disclaimer a { color: var(--text); }

.signup_right {
  flex: 1; padding: 40px 44px 26px;
  display: flex; flex-direction: column;
}
.signup_title {
  font-size: 16px; letter-spacing: .2em; text-transform: lowercase;
  color: var(--text); text-align: center; margin: 0 0 26px; font-weight: 700;
}
.signup_rules {
  font-size: 10.5px; line-height: 1.6; color: var(--text-muted);
  margin: -4px 0 14px;
}
.signup_or {
  display: flex; align-items: center; gap: 12px; margin: 15px 0 18px;
  font-size: 10px; letter-spacing: .24em; color: var(--text-muted);
  text-transform: lowercase;
}
.signup_or::before, .signup_or::after {
  content: ""; flex: 1; height: 1px; background: var(--border-hover);
}
.signup_alt {
  width: 100%; padding: 13px; margin-bottom: 10px;
  background: var(--surface-light); border: 1px solid var(--border-hover);
  color: var(--text); font-family: var(--font); font-size: 13px;
  letter-spacing: .1em; text-transform: lowercase; cursor: pointer;
  display: flex; align-items: center; justify-content: center; gap: 10px;
  text-decoration: none; transition: var(--transition);
}
.signup_alt:hover { border-color: var(--secondary); }
.signup_alt i { color: var(--secondary); }
.signup_switch {
  background: none; border: none; color: var(--text-muted);
  font-family: var(--font); font-size: 11px; cursor: pointer;
  margin: 21px auto 0; display: block; line-height: 1;
}
.signup_switch:hover { color: var(--text); }
.signup_links {
  margin-top: auto; padding-top: 18px; text-align: center;
  font-size: 11px; color: var(--text-muted);
}
.signup_links a { color: var(--text-dim); text-decoration: none; margin: 0 8px; }
.signup_links a:hover { color: var(--text); }

@media (max-width: 720px) {
  .signup_modal { flex-direction: column; min-height: 0; }
  .signup_left { flex: 0 0 auto; min-height: 200px; border-right: none; border-bottom: 1px solid var(--border); }
  .signup_right { padding: 26px 22px 18px; }
}
