/* ── FYM Craps ────────────────────────────────────────────────────────────
   The screen is two things stacked: the PIT (a 3D table where dice roll)
   and the FELT (a DOM betting layout). The prototype at Test Games/Dice
   fused them — you tapped the 3D felt through a raycast — and this file is
   where that decision was undone. See the header of js/craps.js for why.

   Portrait first. Nothing here may scroll sideways: every row is a grid
   that reflows, and the two places a fixed width could sneak in — the
   history strip and the field's number run — are clipped or wrapped rather
   than allowed to widen the page.

   Safe areas use --fym-safe-*, never env() alone. Telegram's iOS webview
   does not implement the CSS env() safe-area keywords and reports 0, which
   is documented in this repo at js/safe-area.js:31-38; --fym-safe-* is fed
   by that file and collapses to plain env() everywhere else.

   CRAPS IS IMMERSIVE. It was not when this file was written — the note here
   used to say so, and said the height budget below was sized for a screen
   sitting under the app's header and bottom nav. That stopped being true on
   2026-08-10, when `[data-screen="craps"]` went into game-chrome.css and
   both `#header` and `#bottom-nav` began resolving to `display: none` for
   this screen. The note did not move with it, and a stale note is worse than
   no note: it was read as fact twice before anyone checked, and the check is
   two lines in a browser —

       document.body.setAttribute('data-screen', 'craps');
       getComputedStyle(document.getElementById('header')).display  // 'none'

   The owner spent a day looking at a squeezed table anyway, because
   game-chrome.css's cache-bust token had not moved since 2026-07-31 and
   /static/*?v= is immutable for a year. See backend/tests/test_asset_tokens.py.  */

#craps-screen {
  /* The screen's own palette. The felt is not the app's surface colour and
     pretending otherwise makes a craps table look like a settings page. */
  --cr-felt-0: #187a46;
  --cr-felt-1: #10693a;
  --cr-felt-2: #0a5a30;
  --cr-line:   rgba(245, 240, 220, 0.82);
  --cr-line-soft: rgba(245, 240, 220, 0.48);
  --cr-gold:   #e8c15a;
  --cr-gold-dim: rgba(232, 193, 90, 0.34);
  --cr-ink:    #f4efe4;
  --cr-win:    #7dff9e;
  --cr-lose:   #ff8f8f;
  --cr-pit:    #0a0d0b;
  /* The height of the pill strip across the top of the pit. One value,
     because two things have to agree about it: the strip itself, and the
     callout, which is absolutely positioned over the same corner and used
     to land its 43 px total directly on top of the pills. Measured at 375
     px: the strip is 27 px, the total overlapped it by 27 of its 43.
     THE TALLEST THING IN THE STRIP IS NOT A PILL. `mountFairBadge` puts the
     fairness shield in `.craps-fairanchor`, and provably-fair.js sizes that
     button at 40 px — taller than the 26.5 px pills beside it, so the strip
     is 40 px whatever the pills do. A 28 px token therefore under-measured
     the strip by twelve and the call landed back on the readouts: measured
     with the shield mounted, strip bottom 310 against a total at 303, 7.4 px
     of overlap. The harness's stub mountFairBadge is a no-op, which is why
     this only shows once §K has loaded the real one — every real session has
     it from the first mount. */
  --cr-hud-h: 59px;

  padding-bottom: calc(var(--sp-md) + var(--fym-safe-bottom, 0px));

  /* THE LIT ROOM IS THE PAGE. This is the prototype's `body` background
     verbatim (index.html) — it used to live on `.craps-stage`, which meant
     the room ended where the pit did and everything under the chips was flat
     black. The owner's words: "mye svart i bunn". */
  background: radial-gradient(120% 90% at 50% 30%, #14211a 0%, #0a0d0b 70%);
}

/* Declared here rather than inherited from style.css's global reset. The
   felt's whole layout is percentage widths plus padding, so a screen that
   renders without that reset — a harness, a future page that loads this
   sheet on its own — is 16 px wider than its container and scrolls
   sideways. Measured in tools/js-tests/craps-browser.html at 391 px inside
   375 px before this rule existed. */
/* The ROOT is in this list too. style.css carries an app-wide
   `* { box-sizing: border-box }`, so #craps-screen inherits the right box
   model in production — but craps is a lazily-loaded screen with its own
   stylesheet, and a screen that is only square because a different file
   said so is one refactor away from being wrong. Owning it here also means
   a harness that loads craps.css alone measures the real thing. */
#craps-screen,
.craps,
.craps *,
.craps *::before,
.craps *::after {
  box-sizing: border-box;
}

.craps {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2xs);
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  padding: 0 var(--sp-2xs);
  /* THE CONTAINING BLOCK, and it has to be said out loud. Without it the
     felt is `static`, so every absolutely-positioned child resolves
     against `.screen` — which is 32 px wider. The HUD's
     `left/right: var(--sp-2xs)` then made it 357 wide inside a 341 felt,
     hanging 8 px off each side, and it took the history row with it.
     Measured after adding this: HUD 357 -> 325, history 341 -> 309, both
     inside the felt, and the other 22 absolutely-positioned descendants
     unmoved because each already had a positioned ancestor of its own. */
  position: relative;
  /* Belt and braces against a stray wide child. A craps layout that
     scrolls sideways on a phone is unusable, not merely untidy.
     NOTE this was also what hid the bug above for as long as it lived:
     clipping a mis-positioned element hides it rather than fixing it, and
     whether `scrollWidth` still reports the overflow varies with state,
     which is why the integration harness caught it on one width and not
     on the next. Keep the clip, but never treat it as the answer. */
  overflow-x: clip;
}

/* ══ The pit ═════════════════════════════════════════════════════════════ */

.craps-stage {
  position: relative;
  width: 100%;
  height: clamp(170px, 30vh, 300px);
  border-radius: 14px;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 25%, #16241b 0%, var(--cr-pit) 74%);
  border: 1px solid rgba(232, 193, 90, 0.18);
  isolation: isolate;
  touch-action: manipulation;
}

.craps-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* ── The living background (index.html:29-68) ────────────────────────────
 *
 * The prototype's `#bg-fx` is `position: fixed` over the whole window,
 * because there the canvas IS the window. Here the pit is a box in a
 * scrolling page, so the same layer is the box — and it shows through the
 * transparent canvas everywhere the table is not.
 *
 * `z-index: -1` rather than a z-index on the canvas: `.craps-stage` has
 * `isolation: isolate`, so a negative child is trapped inside it and paints
 * above the stage's own background and below every other child — including
 * `.craps-vignette` and `.craps-flat`, which have no z-index of their own
 * and would have gone behind a canvas that was given one.
 *
 * Sizes are in % of the stage where the prototype uses vmax of the window,
 * and the four drift durations (43 / 57 / 71 / 63 s) are its own — long,
 * mutually prime, so the pattern does not visibly repeat.
 */
/* Screen-level now, not stage-level — see the note where craps.js appends
   it. `z-index: -1` puts it behind every sibling and in front of
   #craps-screen's own gradient, which is exactly the prototype's stacking:
   `#bg-fx` fixed behind `#scene`. */
.craps-bgfx {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: -1;
}
.craps-blob {
  position: absolute;
  border-radius: 50%;
  mix-blend-mode: screen;
  will-change: transform;
}
.craps-blob.is-red {
  width: 190%; height: 190%; top: -75%; left: -65%;
  background: radial-gradient(circle, rgba(190, 24, 50, 0.34), transparent 62%);
  animation: craps-drift1 43s ease-in-out infinite alternate;
}
.craps-blob.is-gold {
  width: 170%; height: 170%; bottom: -64%; right: -53%;
  background: radial-gradient(circle, rgba(214, 158, 48, 0.30), transparent 62%);
  animation: craps-drift2 57s ease-in-out infinite alternate;
}
.craps-blob.is-purple {
  width: 158%; height: 158%; top: 20%; left: -74%;
  background: radial-gradient(circle, rgba(120, 52, 160, 0.26), transparent 60%);
  animation: craps-drift3 71s ease-in-out infinite alternate;
}
.craps-blob.is-amber {
  width: 127%; height: 127%; top: -42%; right: -42%;
  background: radial-gradient(circle, rgba(230, 120, 40, 0.22), transparent 60%);
  animation: craps-drift4 63s ease-in-out infinite alternate;
}
@keyframes craps-drift1 { to { transform: translate(23%, 15%) scale(1.18); } }
@keyframes craps-drift2 { to { transform: translate(-19%, -13%) scale(1.12); } }
@keyframes craps-drift3 { to { transform: translate(25%, -11%) scale(0.92); } }
@keyframes craps-drift4 { to { transform: translate(-15%, 17%) scale(1.2); } }

.craps-bokeh {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 216, 115, 0.9),
                              rgba(255, 216, 115, 0) 70%);
  opacity: 0;
  will-change: opacity, transform;
  animation: craps-twinkle var(--dur, 7s) ease-in-out var(--delay, 0s) infinite;
}
@keyframes craps-twinkle {
  0%, 100% { opacity: 0; transform: translateY(0) scale(0.7); }
  45% { opacity: var(--peak, 0.5); transform: translateY(-14px) scale(1); }
  70% { opacity: calc(var(--peak, 0.5) * 0.45); }
}

/* ── Which surface takes the bets ─────────────────────────────────────────
 *
 * The felt is the betting surface, so when there is a live context and the
 * player has not asked for the list, the DOM layout gets out of the way and
 * the pit takes the height it was paying rent on. `paintLayout()` owns the
 * two classes.
 *
 * `display: none` and not `remove()`: the DOM felt is what the screen
 * degrades TO. No WebGL, a lost context, a failed three.js import or
 * prefers-reduced-motion all put it straight back, and it is also the only
 * keyboard-and-screen-reader path onto the layout — the LIST button.
 */
.craps.is-table .craps-felt { display: none; }

/* THE TABLE TAKES WHAT IS LEFT, because what is left is what the place row's
 * tap targets are made of. The back row is six boxes across the far end of a
 * table drawn in perspective: at a 350 px stage on a 320 px phone each one is
 * 20 CSS pixels of paint, and every pixel of stage height goes straight into
 * that number. Measured, 320x568: a 352 px stage gives a 22.6 px target and a
 * 400 px stage gives 27.
 *
 * The budget is the MEASURED viewport (--fym-gc-vh, written by
 * FYM.GameChrome, which craps is now on) less the chrome bar and less what
 * the rest of the column costs. The HUD is absolutely positioned inside the
 * stage and costs nothing. Not `vh`: Telegram's webview lies about vh, which
 * is the whole reason --fym-gc-vh exists.
 *
 * THE RESERVE IS MEASURED, NOT ITEMISED, and the reason is that the old note
 * here itemised it — "32 px of .screen padding, ~106 px of chip rail and
 * buttons, 16 px of gaps, and 16 px of slack" — and its own arithmetic came
 * to 170 while the value underneath said 150. The list was the honest one;
 * the number had drifted from it, and the drift was a real 10 px of bar
 * hanging below the fold that nobody had looked for because the itemisation
 * read as if it had been checked.
 *
 * So: swept in a real browser under the PRODUCTION condition — with
 * body[data-screen="craps"] set, which is what game-chrome.css:183 keys
 * --fym-gc-top off, and which is exactly what a hand-mounted screen does not
 * have. Reading the slack between the root's bottom edge and the viewport's,
 * at 375x812:
 *
 *      reserve   stage    slack
 *        200      572       +4
 *        210      562      +14
 *        216      556      +20      <- was here, before the bar grew
 *        220      552      +24
 *
 * THE BAR GREW on 2026-08-11, when the visual-parity pass put the prototype's
 * sizes back: the throw button 44 -> 58, the chips 48 -> 54, the two ghosts
 * 44 -> 41. Net +20 or so, and the slack measured 3 px at 216 — technically
 * fitting and one font-metric away from not. 236 puts it back near 20.
 *
 * and the same 216 leaves +24 at 320x568, which is the tighter of the two
 * because the chrome bar there is 34 px rather than 40.
 *
 * The last 16 of the 216 has a name: --fym-gc-top measures the chrome bar
 * and the safe area, but the root ALSO starts 16 px lower than that —
 * `.screen`'s own padding-top, which the subtraction has never known about.
 * Measured at 375x812: --fym-gc-top computes 40 px and the root's top edge
 * is at 56.
 *
 * Re-sweep this table when the bar changes; do not re-derive the number from
 * a list of parts, and do not measure it on a screen mounted by hand.
 *
 * The floor is what a table stops being readable below; the ceiling is where
 * a desktop browser would otherwise hand the felt half a metre of green. */
.craps.is-table .craps-stage {
  /* THE TABLE IS THE SCREEN, and this is the difference every other fix was
     dancing around. Measured in the prototype at 375x812:

         #scene        0, 0, 375, 812        <- the canvas IS the viewport
         #hud-top      0, 0, 375, 56         <- floats over it
         #hud-bottom   0, 667.5, 375, 144.5  <- floats over it

     The port was a flex COLUMN — strip, then a 556 px stage, then a bar
     underneath taking the rest — so the table could never be more than 68%
     of the screen whatever the reserve said, and everything below it was a
     second, separate region. The owner asked for exactly this: the controls
     may sit OVER the game, but the whole table has to be visible, as in the
     prototype.
     The stage fills what is left of the root and the bar goes ON TOP of it.
     The reserve arithmetic that used to size this box is gone with it —
     there is nothing left to reserve for. */
  /* THE CANVAS IS THE VIEWPORT, and now it is safe for it to be.
     The prototype's `#scene` is 0,0,375,812 with both HUDs floating over it,
     and the TABLE inside that canvas is framed with lit room above and below
     — `fitCamera` keeps widening until the near edge of the felt clears the
     bottom HUD by 8 px (main.js:1346-1348). I made this box full-height once
     before WITHOUT porting that rule, and the chips landed on top of PASS
     LINE. The rule is in craps.js now, so the float is what the prototype
     does rather than what broke it. */
  flex: 1 1 auto;
  height: auto;
  min-height: 0;
  /* NO FRAME AND NO CORNERS. The prototype's `#scene` is
     `position: fixed; inset: 0` with no border and no radius — its felt runs
     to the screen edge. The 1px gold hairline and the 14px radius here drew
     a lit card against the page: measured, the border composited to
     rgb(56,57,33) against a page of rgb(4,3,2), a contrast ratio of 1.74
     where the pit-to-page step is only 1.17 — the frame was the BRIGHTEST
     thing at that boundary. The radius also clipped the WebGL canvas.
     The background goes too: the room is on #craps-screen now and the pit
     lets it through. */
  border: 0;
  border-radius: 0;
  background: transparent;
  /* AND IT BLEEDS TO THE EDGES, because the fit is width-bound and the
     padding is coming straight out of the place row.
     Measured at 320x568: rail corners 2 px from the left and right edges of
     the canvas with 97 px of unused stage above the table and 25 below — the
     camera cannot use the height, so every pixel of horizontal padding is a
     pixel off all twelve zones. `.screen` spends 16 a side and `.craps`
     spends another 8; giving both back is 270 px of canvas instead of 320,
     which is 18% on every target on the felt.
     `50% - 50vw` is the full-bleed idiom and needs to know neither number.
     Floored at -24px so the 560 px desktop column does not turn into a
     1280 px one.
     `width: auto` is load-bearing: the base rule says `width: 100%`, which
     resolves against the padded column and cancels the negative margins
     exactly. Auto lets the flex column stretch it, and stretch subtracts
     margins — including negative ones. */
  margin-inline: max(calc(50% - 50vw), -24px);
  width: auto;
}

/* The column clips horizontally on purpose (a craps layout that scrolls
   sideways on a phone is unusable) and that clip would take the bleed
   straight back off. The stage is exactly the viewport wide and starts at
   its left edge, so it cannot produce a scrollbar; the browser guard
   asserts scrollWidth at 320 and 375 rather than taking that on trust. */
.craps.is-table { overflow-x: visible; }

/* The pit is the table now, so a tap on it must not be treated as a scroll
   gesture or double-tap-to-zoom, and a long press must not raise a callout
   over the felt. `manipulation` alone leaves the selection behaviour. */
.craps.is-table .craps-canvas {
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

/* ── What is riding on each zone ─────────────────────────────────────────
 * A stack of discs says "there is money here"; only a number says how much.
 * The layer is positioned by updateBetLabels() through the same projection
 * the hit rectangles are measured with, and is off entirely while the dice
 * are in the air — there is nothing to read mid-throw, and a label chasing
 * the ceremony camera reads as a bug.
 */
.craps-labels {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
  opacity: 0;
  transition: opacity var(--motion-small) var(--easing);
}
.craps-labels.is-on { opacity: 1; }

.craps-betlabel {
  position: absolute;
  top: 0;
  left: 0;
  display: none;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(8, 12, 10, 0.82);
  border: 1px solid var(--cr-gold-dim);
  color: var(--cr-gold);
  font-size: 11px;
  font-weight: 900;
  line-height: 1.45;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
}
.craps-betlabel.is-on { display: block; }

/* ── The drink and the thump ──────────────────────────────────────────────
 *
 * The prototype's two circles (index.html:100-128), moved from `position:
 * fixed` on a full-screen canvas to the bottom-left corner of the stage —
 * the same corner of the same table, because the stage IS the canvas here.
 * They cost the outer corner of the pass line, which is a full-width band
 * and the largest target on the felt; nothing else on the layout reaches
 * that patch of cloth.
 *
 * 44 px rather than the prototype's 54, which is the number every other
 * control on this screen is built to (`.craps-btn { min-height: 44px }`) and
 * the number `MIN_TAP_PX` pads the felt's own zones out to. The whole port
 * is a size down from the prototype — chips went 54 -> 48 for the same
 * reason — and 54 px circles here would take a visibly bigger bite out of
 * the felt than they do there.
 *
 * `hidden` is set by paintTheatre() whenever there is no pit, and hidden
 * beats every rule below.
 */
.craps-theatre {
  position: absolute;
  left: var(--sp-2xs);
  /* Clear of the history strip, which lives at the bottom of the HUD, and
     clear of the stage's own rounded corner. */
  bottom: var(--sp-2xs);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3xs);
  z-index: 4;
  pointer-events: none;
}
.craps-theatre[hidden] { display: none; }

/* The port's own controls — Take down, List, Rules — floating on the right
   edge the way the drink and the fist float on the left. They are OUTSIDE
   the bottom HUD on purpose: fitCamera pays for every pixel of that HUD in
   table size, and these three cost 60. */
/* Mirrors .craps-theatre: same bottom anchor, opposite edge, same 44 px
   circular buttons (the .craps-tbtn class IS the drink/fist button). At this
   anchor they sit over the bar's right end and the room, grazing the table
   corner at most — the first cut sat 160 px higher and covered YO-11. */
.craps-siderail {
  position: absolute;
  right: var(--sp-2xs);
  /* Above the bar, not beside Clear: at bottom 8 the circles sat exactly on
     the Clear button (measured x 307-351 against Clear's 291-353 in the same
     band). 152 clears the 148 px bar; as 44 px circles they reach the wood at
     most — the zone-overlap probe reads NONE. */
  bottom: calc(var(--sp-2xs) + 152px);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--sp-3xs);
  z-index: 4;
}
.craps.is-drinking .craps-siderail {
  opacity: 0;
  pointer-events: none;
}
.craps.is-drinking .craps-siderail * { pointer-events: none; }

.craps-tbtn {
  appearance: none;
  -webkit-appearance: none;
  pointer-events: auto;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--cr-gold-dim);
  background: rgba(8, 12, 10, 0.75);
  font-family: inherit;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4);
  transition: opacity var(--motion-small) var(--easing),
              transform var(--motion-micro) var(--easing);
  -webkit-tap-highlight-color: transparent;
}
.craps-tbtn:active { transform: scale(0.9); }
.craps-tbtn:disabled { opacity: 0.35; cursor: default; }

/* THE HUD GETS OUT OF THE WAY WHILE THE GLASS IS AT THE MOUTH.
 * The prototype fades `#hud-top`, `#hud-bottom`, `#history` and both buttons
 * to nothing and kills their pointers (index.html:131-136). Same here, minus
 * the part where there is no way back: a tap on the pit ends the sequence —
 * see onPitUp(). The rule blocks pointers on the bar's children as well as
 * on the bar, because a 0-opacity Roll button that still takes taps is worse
 * than a visible one. */
.craps-hud,
.craps-bar,
.craps-theatre {
  transition: opacity 0.35s var(--easing);
}
.craps.is-drinking .craps-hud,
.craps.is-drinking .craps-bar,
.craps.is-drinking .craps-theatre {
  opacity: 0;
  pointer-events: none;
}
/* THE CHILDREN, NOT JUST THE CONTAINERS. `pointer-events: none` on a parent
   does not survive a child that says `auto`, and three of them do: the
   fairness shield in the HUD, and both theatre buttons. Every one of them
   would be an invisible tap target for the whole nine seconds. */
.craps.is-drinking .craps-hud *,
.craps.is-drinking .craps-bar *,
.craps.is-drinking .craps-tbtn { pointer-events: none; }

.craps-vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--motion-small) var(--easing);
  background: radial-gradient(ellipse at 50% 55%, transparent 42%,
                              rgba(0, 0, 0, 0.6) 100%);
}
.craps-stage.is-slowmo .craps-vignette { opacity: 1; }

/* The prototype names the effect (#slowmo-tag, index.html:334). Without the
   words the vignette alone reads as the screen dimming, which on a 300 px
   stage is what a lost focus looks like. Letter-spacing, weight, the 0.3 s
   fade and the 0.9 s pulse are the prototype's; the size steps down with the
   rest of the port. */
.craps-slowmo {
  position: absolute;
  top: 18%;
  left: 50%;
  transform: translateX(-50%);
  pointer-events: none;
  z-index: 3;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.8);
  opacity: 0;
  transition: opacity 0.3s var(--easing);
}
.craps-stage.is-slowmo .craps-slowmo {
  opacity: 1;
  animation: craps-slowpulse 0.9s ease-in-out infinite;
}
@keyframes craps-slowpulse { 50% { opacity: 0.45; } }

/* ── What each zone just won or lost, over its own chips ─────────────────
 *
 * `floatText` (main.js:1457) positions these through the HOME camera and
 * lets the animation remove them. 1.6 s, rise-and-fade, green up / red down
 * — the prototype's keyframes, its two colours and its 900 weight.
 */
.craps-floats {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 3;
}
.craps-float {
  position: absolute;
  transform: translate(-50%, -50%);
  font-size: 18px;
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.85);
  animation: craps-floatup 1.6s var(--easing) forwards;
}
.craps-float.is-win { color: #7dff9e; }
.craps-float.is-lose { color: #ff8080; }
@keyframes craps-floatup {
  0% { opacity: 0; transform: translate(-50%, -30%) scale(0.7); }
  15% { opacity: 1; transform: translate(-50%, -60%) scale(1.15); }
  30% { transform: translate(-50%, -70%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -180%) scale(1); }
}

/* ── The flat dice: no WebGL, a lost context, or reduced motion ───────── */

.craps-flat {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  gap: clamp(14px, 5vw, 30px);
}
.craps-stage.is-flat .craps-flat { display: flex; }

/* NOT `padding: 14%` ON THE DIE, AND NOT `1fr` TRACKS.
 *
 * A percentage padding resolves against the containing block, which here is
 * `.craps-flat` — `position: absolute; inset: 0`, i.e. the whole stage — and
 * not against the 52-78 px die. With `box-sizing: border-box` (style.css:5,
 * and again at the top of this file) a padding wider than the specified
 * width does not overflow: it floors the CONTENT box to zero. Measured on a
 * 375 px phone: stage 323 px, padding 45.2 px a side against a 90.4 px die,
 * `grid-template-columns` computing to `0px 0px 0px`, every pip a 0x0 border
 * box. Twelve pips carrying `is-on` and nothing painted — two featureless
 * red squares, on the ONLY presentation a reduced-motion player ever sees.
 *
 * So the inset is expressed as track sizes instead. Percentage grid tracks
 * and gaps resolve against the grid container's own content box, which with
 * no padding IS the die: 3 x 21.1% + 2 x 4.35% = 72%, centred by
 * `place-content`, leaving exactly the 14% margin the padding was reaching
 * for. Every number here is a fraction of the die and of nothing else.
 *
 * The browser harness asserts a painted pip's width is > 0 at every phone
 * width, because `dataset.value` was right the whole time this was broken. */
.craps-flatdie {
  width: clamp(52px, 16vw, 78px);
  aspect-ratio: 1;
  border-radius: 16%;
  background: radial-gradient(circle at 34% 28%, #d81f30, #9c0e1c 78%);
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.55),
              inset 0 2px 6px rgba(255, 255, 255, 0.22);
  display: grid;
  grid-template-columns: repeat(3, 21.1%);
  grid-template-rows: repeat(3, 21.1%);
  place-content: center;
  padding: 0;
  gap: 4.35%;
}

.craps-pip {
  border-radius: 50%;
  background: transparent;
}
.craps-pip.is-on {
  background: radial-gradient(circle at 32% 30%, #ffffff, #e8e0d0 85%);
  box-shadow: inset 0 -1px 2px rgba(0, 0, 0, 0.3);
}

.craps-flat.is-shaking .craps-flatdie { animation: craps-rattle 0.38s linear infinite; }
.craps-flatdie[data-die="2"] { animation-delay: 0.09s; }
.craps-flat.is-landed .craps-flatdie { animation: craps-land 0.4s var(--easing) both; }

@keyframes craps-rattle {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(-6deg); }
  50%      { transform: translate3d(0, -5px, 0) rotate(7deg); }
}
@keyframes craps-land {
  0%   { transform: scale(1.18) rotate(-9deg); }
  100% { transform: scale(1) rotate(0deg); }
}

/* ── HUD over the pit ─────────────────────────────────────────────────── */

/* SPACE-BETWEEN, because the prototype's strip has three items and spreads
   them: measured at 14 / 158.3 / 361 across a 375 px row. The port packed two
   pills at 24 / 108.3 and left the rest of the row empty, which is why the
   top of the screen reads as unbalanced next to the mockup. */
.craps-hud {
  position: absolute;
  top: var(--sp-2xs);
  left: var(--sp-2xs);
  right: var(--sp-2xs);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3xs);
  min-height: var(--cr-hud-h);
  pointer-events: none;
  z-index: 3;
}

/* 7px 14px at 15px, which is the prototype's pill. This shipped 4px 10px at
   --fs-caption (11px) and measured 23 px tall against the prototype's 41 —
   about two thirds, on the two readouts that say what phase the hand is in
   and what is at risk. */
.craps-pill {
  background: rgba(8, 12, 10, 0.72);
  border: 1px solid rgba(232, 193, 90, 0.3);
  border-radius: 999px;
  padding: 7px 14px;
  font-size: 15px;
  font-weight: 700;
  /* 0.02em and 1.55, both the prototype's. The padding and size above were
     already right — the pill still measured 35.5 against 41 because the app's
     global line-height is tighter than the prototype's page, and the tracking
     was 0.06 where `.pill` says 0.02. Declared values matching is not the
     same as rendering matching, which is why this was measured rather than
     read. */
  line-height: 1.55;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--cr-ink);
  white-space: nowrap;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.craps-pill--phase.is-point { border-color: #ff9d3c; color: #ffb95c; }
/* The phase pill is the smaller of the two in the prototype: 13 against 15. */
.craps-pill--phase { font-size: 13px; }
.craps-pill--risk { font-variant-numeric: tabular-nums; color: var(--muted); }

/* THE ROUND INFO BUTTON, which the port did not have. The prototype's
   `#info-btn` is a 38 px gold circle carrying an italic Georgia "i", sitting
   in the middle of the top strip (index.html:382, :90-98). The port had moved
   its job to a "Rules" text button in the bottom utility row — and that
   button may stay, because the owner allows extra controls; what was not
   allowed is the prototype's own element being absent.
   `pointer-events: auto` because the strip itself is `none`. */
/* THREE CLASSES, and the count is the whole point. obsidian-vault.css sets
   an app-wide 44 px floor on every button, and its selector list is not just
   `body.ov button` — it also carries `body.ov [type="button"]`, which these
   are. ATTRIBUTE SELECTORS COUNT IN THE CLASS COLUMN, so that rule is
   (0,2,1), and my first attempt at `.craps .craps-pill--info` was (0,2,0):
   equal on classes, beaten on elements. It measured 44 x 44 with the 38s
   ignored, twice, before I stopped reasoning and asked the browser which
   rule was winning. Three classes is (0,3,0) and clears it.
   (There IS a documented opt-out — `.ov-btn-inline` and friends at
   obsidian-vault.css:7328 — but it is for small inline icon buttons inside
   chips and rows, which a throw button is not.) */
.craps .craps-hud .craps-pill--info {
  width: 38px;
  height: 38px;
  min-width: 38px;
  min-height: 38px;
  padding: 0;
  border-radius: 999px;
  font-family: Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-size: 19px;
  font-weight: 800;
  letter-spacing: 0;
  text-transform: none;
  color: var(--cr-gold);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  cursor: pointer;
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
}
.craps .craps-hud .craps-pill--info:active { transform: scale(0.94); }
.craps-pill--risk.is-live { color: var(--cr-gold); border-color: var(--cr-gold-dim); }

/* No `margin-left: auto` any more. The strip is `space-between` with three
   children now — the left group, the info button, this — so the shield ends
   up on the right by the layout rather than by a crutch, and the info button
   genuinely centres instead of being pushed. */
/* A width even before the shield mounts. `mountFairBadge` is lazy, so on the
   first frames this box is 0 wide and `space-between` pushed the info button
   hard right instead of leaving it mid-row. 40 px is what provably-fair.js
   sizes the shield at, so the row does not shift when it lands either. */
.craps-fairanchor {
  pointer-events: auto;
  display: inline-flex;
  min-width: 40px;
  justify-content: flex-end;
}

/* The two readouts travel together as the left item of the three. */
.craps-hud__left {
  display: flex;
  align-items: center;
  gap: var(--sp-3xs);
  min-width: 0;
}

/* ANCHORED FROM THE TOP, BELOW THE STRIP. This used to sit inside
   `.craps-hud` and take `bottom: var(--sp-2xs)` from it, which drew the dots
   straight over the pills — 21.5 px of vertical and 174.1 px of horizontal
   overlap, measured. It is a sibling of the strip now, so `bottom` would
   anchor it to the foot of the whole screen; `top` past the strip's own
   height is the prototype's arrangement (`top: 54px`, dots clear underneath
   at y 54-78 with zero overlap). */
.craps-history {
  position: absolute;
  left: var(--sp-2xs);
  right: var(--sp-2xs);
  top: calc(var(--sp-2xs) + var(--cr-hud-h) + 6px);
  display: flex;
  gap: 4px;
  justify-content: center;
  pointer-events: none;
  overflow: hidden;
  z-index: 2;
}
.craps-histdot {
  flex: 0 0 auto;
  min-width: 22px;
  height: 22px;
  padding: 0 4px;
  border-radius: 6px;
  background: rgba(8, 12, 10, 0.66);
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: #cfe8d8;
  font-size: 11px;
  font-weight: 800;
  display: flex;
  align-items: center;
  justify-content: center;
  font-variant-numeric: tabular-nums;
}
.craps-histdot.is-seven { color: var(--cr-lose); border-color: rgba(255, 122, 122, 0.5); }

/* ── The call ─────────────────────────────────────────────────────────── */

/* Pinned to the top of the pit, not centred in it. The settle pushes the
   camera to within half a metre of the dice, so the middle of the stage is
   exactly where the two things the player wants to read at that moment are
   — the number on the felt and the number in the call — and centring put
   one on top of the other. */
.craps-callout {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 2px;
  text-align: center;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.86);
  transition: opacity var(--motion-small) var(--easing),
              transform var(--motion-small) var(--easing);
  z-index: 4;
  /* The top padding clears the pill strip. The callout is inset:0 over the
     whole pit and starts at the top, which is exactly where the phase pill
     and the at-risk pill live — so the moment the number was called, the
     two readouts that say what the number MEANT went behind it. Measured at
     375 px: 43 px of total on a 27 px strip, 27 px of overlap. */
  padding: calc(var(--sp-2xs) + var(--cr-hud-h) + 4px)
           var(--sp-sm) var(--sp-sm);
}
.craps-callout.is-shown { opacity: 1; transform: scale(1); }

/* A roll another session already threw and was already paid for. The number
 * is real and belongs on screen; the celebration is not this client's to
 * make, so the whole call is muted and the settlement lines lose their
 * win/lose colour. See roll()'s `res.replay` branch. */
.craps-callout.is-replay .craps-callout__total,
.craps-callout.is-replay .craps-callout__say { color: #b9b3a4; text-shadow: none; }
.craps-callout.is-replay .craps-line { opacity: 0.72; }

.craps-callout__total {
  /* THE NUMBER YOU ROLLED, at the size the prototype says it is.
     `clamp(34px, 10vw, 58px)` resolves to 37.3 px on a 375 px phone against
     the prototype's flat 88 px (index.html:291-313, measured on both) — the
     one moment the whole screen is built around, at 42% of the size. The
     ceiling was also never reachable: 58px needs a 580 px viewport, and
     this screen is sized for 375.
     23.5vw is 88 px at 375 and keeps shrinking below that on a 320, where a
     flat 88 would crowd the callout's sub-line off the stage. */
  font-size: clamp(58px, 23.5vw, 96px);
  font-weight: 900;
  line-height: 1;
  color: #fff;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 26px rgba(232, 193, 90, 0.5), 0 4px 14px rgba(0, 0, 0, 0.8);
}
.craps-callout__say {
  font-size: var(--fs-body-lg);
  font-weight: 800;
  color: var(--cr-gold);
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.8);
}
.craps-callout__lines {
  margin-top: 4px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 40%;
  overflow: hidden;
}
.craps-line {
  display: flex;
  align-items: baseline;
  gap: 6px;
  justify-content: center;
  font-size: var(--fs-small);
  font-weight: 700;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.85);
}
.craps-line__zone { color: var(--cr-ink); opacity: 0.85; }
.craps-line__v { font-variant-numeric: tabular-nums; }
.craps-line.is-win  .craps-line__v { color: var(--cr-win); }
.craps-line.is-lose .craps-line__v { color: var(--cr-lose); }
.craps-line.is-push .craps-line__v { color: #cfd4cf; }
.craps-line__tag {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--cr-line-soft);
}

/* ══ The felt ════════════════════════════════════════════════════════════ */

.craps-felt {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 7px;
  border-radius: 14px;
  background:
    radial-gradient(130% 100% at 50% 0%, var(--cr-felt-0), var(--cr-felt-1) 55%,
                    var(--cr-felt-2) 100%);
  border: 1px solid rgba(245, 240, 220, 0.16);
  box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.35);
}

.craps-places {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 4px;
}

.craps-props {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
}

/* ── A zone ───────────────────────────────────────────────────────────── */

.craps-zone {
  position: relative;
  appearance: none;
  -webkit-appearance: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  color: var(--cr-ink);
  background: rgba(4, 30, 18, 0.24);
  border: 1.5px solid var(--cr-line-soft);
  border-radius: 9px;
  padding: 6px 8px 7px;
  min-height: 44px;              /* the tap target, not a decoration */
  display: flex;
  flex-direction: column;
  gap: 1px;
  transition: transform var(--motion-micro) var(--easing),
              border-color var(--motion-micro) var(--easing),
              background-color var(--motion-micro) var(--easing);
  -webkit-tap-highlight-color: transparent;
}
.craps-zone:active { transform: scale(0.975); }

.craps-zone__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px;
}
.craps-zone__name {
  font-weight: 900;
  font-size: var(--fs-small);
  letter-spacing: 0.04em;
}
.craps-zone__pay {
  font-size: 10px;
  font-weight: 700;
  color: var(--cr-line-soft);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.craps-zone__note {
  font-size: 10px;
  line-height: 1.25;
  color: var(--cr-line-soft);
}
.craps-zone__min {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: var(--cr-line-soft);
  text-transform: uppercase;
}
/* The step is only shouted when the CURRENT chip cannot meet it. That is
   the whole of the "5 chip on the 6" problem: the server answers 400
   naming the step, and the player should never have to find that out by
   tapping six times. */
.craps-zone__min.is-blocking { color: #ffc98a; }

.craps-zone__stake {
  margin-top: auto;
  align-self: flex-start;
  font-size: var(--fs-small);
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  color: var(--cr-gold);
  min-height: 1em;
}

.craps-zone.is-live {
  border-color: var(--cr-gold);
  background: rgba(232, 193, 90, 0.1);
}

.craps-zone.is-blocked {
  opacity: 0.42;
  border-style: dashed;
}
/* A bet you already HAVE must stay legible even when it cannot be raised.
   The pass line with the point on is the case: it is locked, it is also
   100 $FYM of yours, and dimming it to 42% made the money you have riding
   look like the one thing on the felt you are not allowed to see. */
.craps-zone.is-blocked.is-live {
  opacity: 0.85;
  border-style: solid;
}
.craps-zone.is-blocked.is-live .craps-zone__stake { opacity: 1; }
/* Deliberately NOT `disabled`. A disabled button swallows the tap and the
   player learns nothing; this one is visibly unavailable and still
   answers with the reason. */
.craps-zone.is-blocked:active { transform: none; }

.craps-zone.is-takedown {
  border-color: #ff9d3c;
  background: rgba(255, 157, 60, 0.12);
}
.craps-zone.is-takedown .craps-zone__stake::after {
  content: ' ↺';
  color: #ffb95c;
}

.craps-zone.is-flash { animation: craps-flash 0.6s var(--easing); }
@keyframes craps-flash {
  0%   { box-shadow: 0 0 0 0 rgba(232, 193, 90, 0.7); }
  100% { box-shadow: 0 0 0 14px rgba(232, 193, 90, 0); }
}

/* ── Place boxes ──────────────────────────────────────────────────────── */

.craps-zone--place {
  align-items: center;
  text-align: center;
  padding: 5px 2px 6px;
  gap: 0;
}
.craps-zone--place .craps-zone__head {
  flex-direction: column;
  align-items: center;
  gap: 0;
}
.craps-zone--place .craps-zone__name {
  font-size: clamp(13px, 4.2vw, 20px);
  line-height: 1.05;
}
/* SIX and NINE are spelled out the way a real layout spells them, so they
   cannot be read upside down as each other. They are also four characters
   in a box sized for one, hence the shrink. */
.craps-zone--place[data-zone="place6"] .craps-zone__name,
.craps-zone--place[data-zone="place9"] .craps-zone__name {
  font-size: clamp(10px, 3vw, 13px);
  letter-spacing: 0.02em;
}
.craps-zone--place .craps-zone__stake { align-self: center; }
.craps-zone--place .craps-zone__min { font-size: 9px; }

.craps-zone__puck {
  position: absolute;
  top: -7px;
  left: 50%;
  transform: translateX(-50%) scale(0.6);
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: #f2ead2;
  color: #141414;
  border: 3px solid #141414;
  font-size: 9px;
  font-weight: 900;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-small) var(--easing),
              transform var(--motion-small) var(--easing);
}
.craps-zone__puck.is-on { opacity: 1; transform: translateX(-50%) scale(1); }

/* ── Field ────────────────────────────────────────────────────────────── */

.craps-zone--field .craps-zone__strip {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  margin: 2px 0 1px;
}
.craps-zone__num {
  font-size: var(--fs-small);
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  color: var(--cr-line);
  min-width: 20px;
  text-align: center;
}
/* 2 and 12 are the entire reason the field is a 2.78% bet rather than a
   5.56% one — flatten both to 2:1 and the edge doubles. The ring is how a
   real layout says so. */
.craps-zone__num.is-double {
  color: var(--cr-gold);
  border: 1.5px solid var(--cr-gold);
  border-radius: 50%;
  min-width: 22px;
  line-height: 19px;
}

/* ── Lines ────────────────────────────────────────────────────────────── */

.craps-zone--line { min-height: 52px; }
.craps-zone--line .craps-zone__name { font-size: var(--fs-body); letter-spacing: 0.1em; }
.craps-zone--pass {
  border-color: var(--cr-gold);
  border-width: 2px;
}
.craps-zone--pass .craps-zone__name { color: var(--cr-gold); }

/* ══ The bar ═════════════════════════════════════════════════════════════ */

/* A SCRIM, NOT A SLAB. The prototype's `#hud-bottom` is
   `linear-gradient(to top, rgba(5,8,6,0.88) 55%, rgba(5,8,6,0))` — the felt
   fades out UNDER the chips instead of being cut off by an opaque edge, so
   the table reads as continuous all the way down. The port had `none` over a
   flat page, which is the other half of "mye svart i bunn": nothing under the
   pit belonged to the same room. */
/* Over the table, which is what `#hud-bottom` does — and `fitCamera` reads
   this element's height to decide how far back the camera has to sit, so the
   felt is never behind it. */
.craps.is-table .craps-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 4;
  padding-left: var(--sp-2xs);
  padding-right: var(--sp-2xs);
  padding-bottom: calc(12px + var(--fym-safe-bottom, 0px));
}
.craps.is-table {
  height: calc(var(--fym-gc-vh, 100dvh) - var(--fym-gc-top, 40px));
  min-height: 0;
}

.craps-bar {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2xs);
  align-items: center;
  padding-top: var(--sp-2xs);
  background: linear-gradient(to top, rgba(5, 8, 6, 0.88) 55%, rgba(5, 8, 6, 0));
}

/* 10px, not var(--sp-2xs)'s 8. `#chip-row { gap: 10px }` in the prototype
   (index.html), and with four 54px chips the row is 246 of a 320px screen —
   it fits, and the extra 2px a side is visible next to a 54px disc. */
.craps-chips {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}

/* EVERY NUMBER HERE IS THE PROTOTYPE'S, and five of them had been shrunk.
   `.chip-btn` in the prototype's index.html is 54x54 with a 4px dashed rim
   and a 3px outline; this shipped 48x48 / 3 / 2, which is 11% smaller in
   diameter and a visibly thinner rim on the most-tapped control in the game.
   The four radial gradients below were already byte-identical, which is what
   made the difference read as "the chips look smaller" rather than "the
   chips look wrong". */
.craps-chip {
  appearance: none;
  -webkit-appearance: none;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  border: 4px dashed rgba(255, 255, 255, 0.85);
  outline: 3px solid rgba(0, 0, 0, 0.4);
  color: #fff;
  font-family: inherit;
  font-size: var(--fs-small);
  font-weight: 900;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.55),
              inset 0 2px 4px rgba(255, 255, 255, 0.25);
  transition: transform var(--motion-micro) var(--easing),
              box-shadow var(--motion-micro) var(--easing);
  -webkit-tap-highlight-color: transparent;
}
.craps-chip[data-chip="5"]   { background: radial-gradient(circle at 35% 30%, #e4574f, #a31f1f 75%); }
.craps-chip[data-chip="25"]  { background: radial-gradient(circle at 35% 30%, #3fae66, #14713a 75%); }
.craps-chip[data-chip="100"] { background: radial-gradient(circle at 35% 30%, #4a4a52, #17171c 75%); }
.craps-chip[data-chip="500"] { background: radial-gradient(circle at 35% 30%, #9a5fd0, #5a2b8c 75%); }
/* -8px and 1.1, the prototype's `.chip-btn.selected`. The selected chip is
   how the felt tells you what a tap will cost; a smaller lift is a quieter
   answer to the most important question on the screen. */
.craps-chip.is-selected {
  transform: translateY(-8px) scale(1.1);
  box-shadow: 0 10px 18px rgba(0, 0, 0, 0.6),
              0 0 0 3px var(--cr-gold),
              inset 0 2px 4px rgba(255, 255, 255, 0.3);
}

.craps-actions {
  display: flex;
  gap: var(--sp-2xs);
  align-items: center;
  width: 100%;
  justify-content: center;
}
/* The utility row pays for its own height. Its three controls are secondary
   — a single-zone take down, the list fallback and the rules sheet — but
   Take down moves money, so 44 px of tap target is NOT what gets given up
   here. The row gives up type size and top margin instead. */
.craps-actions--util {
  margin-top: var(--sp-2xs);
}
.craps-actions--util .craps-btn {
  flex: 1 1 0;
  font-size: var(--fs-tiny, var(--fs-small));
}
/* NATURAL WIDTHS AND A 10 px GAP, which is what the prototype's `#action-row`
   is — `display: flex; gap: 10px; align-items: center`, every button at the
   width its own padding gives it. Measured at 375: undo 74, throw 175.3,
   clear 61.9, spanning 22..353.1 with two 10 px gaps. I had derived a 1:2:1
   flex from the ratio; it is not a flex at all, and the ratio was a
   coincidence of those three labels' lengths. */
.craps-actions--main {
  gap: 10px;
}
.craps-actions--main .craps-btn--ghost {
  flex: 0 0 auto;
}

.craps-btn {
  appearance: none;
  -webkit-appearance: none;
  font-family: inherit;
  font-weight: 800;
  border-radius: 12px;
  cursor: pointer;
  min-height: 44px;
  padding: 0 var(--sp-sm);
  color: var(--cr-ink);
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.08);
  transition: transform var(--motion-micro) var(--easing),
              background-color var(--motion-micro) var(--easing);
  -webkit-tap-highlight-color: transparent;
}
.craps-btn:active { transform: scale(0.97); }
.craps-btn:disabled { opacity: 0.35; cursor: default; }
.craps-btn--ghost {
  font-size: var(--fs-small);
  /* Paying for the roll button's floor — see .craps-btn--roll. Narrower
     side padding, and shrink before it does. 44 px of height is untouched,
     so every one of these is still a full tap target. */
  padding: 0 var(--sp-2xs);
  flex: 0 1 auto;
  min-width: 0;
}
.craps-btn--ghost.is-active {
  background: rgba(255, 157, 60, 0.2);
  border-color: #ff9d3c;
  color: #ffc98a;
}
.craps-btn--roll {
  /* THE HERO CONTROL HAS TO LOOK LIKE ONE, and now it can be. This row is
     the prototype's three — Undo · ROLL · Clear — since the port's own
     Take down / List / Rules moved to the utility row below.
     What the old four-in-a-row cost is worth keeping written down: with a
     fourth control in here `flex: 1 1 auto` had no leftover space to grow
     out of, so every button sat at its text width and ROLL measured 82.5 px
     — NARROWER than TAKE DOWN's 89.3. The hero was the second-smallest
     thing in its own bar.
     The prototype's throw button is 175.3 px of a 373 px row, twice its
     neighbours (measured on both). Three buttons and two gaps is the same
     arithmetic, so that ratio is reachable here rather than approximated. */
  flex: 0 0 auto;
  padding: 15px 44px;
  /* THE HERO IS TALLER THAN ITS NEIGHBOURS, which is most of what makes it
     read as the hero. The prototype's `#roll-btn` is `padding: 15px 44px` at
     `font-size: 20px` and measures 58.5 px against its 41 px neighbours. The
     port inherited the shared 44 px floor and sat flush with them — a flat
     row of three equal pills. */
  /* Declared again under `.craps` below — the app-wide
     `body.ov button { min-height: 44px }` outranks a single class here. */
  font-size: 20px;
  /* 44 px min-height is the tap floor on the two ghosts; this is the floor
     that stops ROLL collapsing to its text if the ratio above ever cannot be
     honoured. It stays UNDER the 1:2:1 share at every supported width —
     otherwise it would win over the ratio and take the row back to the
     63.6% it was measured at. Three buttons and two gaps have to fit 318 px
     at the narrowest; the old note records that four of them did not,
     overflowing by 3 px at a flat 104. */
  min-width: clamp(88px, 26vw, 130px);
  max-width: 220px;
  font-size: var(--fs-body-lg);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #fff;
  background: linear-gradient(180deg, #e0453a, #9c1420);
  border-color: rgba(255, 141, 126, 0.35);
  box-shadow: 0 5px 16px rgba(200, 16, 46, 0.4),
              inset 0 1px 0 rgba(255, 255, 255, 0.28);
}
/* A SOLID DARK SLAB, not a faint pink-rimmed ghost. The prototype's
   `#roll-btn:disabled` is `background: #3a3f3c; color: #8b948e;
   box-shadow: none; border-color: transparent`. The port left the red rim on
   and dropped the whole button to `.craps-btn:disabled`'s `opacity: 0.35`,
   so the most important control on the screen spent every pre-bet moment as
   a translucent red outline. `opacity: 1` is required here to undo that. */
/* The 58 px hero, at a specificity that survives obsidian-vault.css's
   app-wide 44 px button floor — see the note on the info pill for why three
   classes rather than two. Measured before: ROLL 44, Undo 44, a flat row of
   equal pills. The prototype is 58.5 against 41. */
.craps .craps-actions .craps-btn--roll { min-height: 58px; }
.craps .craps-actions .craps-btn--ghost { min-height: 41px; }

.craps-btn--roll:disabled {
  background: #3a3f3c;
  color: #8b948e;
  border-color: transparent;
  box-shadow: none;
  opacity: 1;
}

/* ══ The receipt ═════════════════════════════════════════════════════════
   Three values and a label. `fair` absent renders as "not on record" —
   never as a failure, and never rebuilt from whatever seed pair happens to
   be live right now. A fabricated receipt is what a cheating casino
   produces. */

.craps-receipt {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px var(--sp-2xs);
  align-items: baseline;
  padding: var(--sp-2xs) var(--sp-xs);
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--surface);
  font-size: var(--fs-caption);
  min-height: 0;
}
.craps-receipt:empty { display: none; }
.craps-receipt.is-absent { border-style: dashed; opacity: 0.8; }

.craps-receipt__k {
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  white-space: nowrap;
}
.craps-receipt__v {
  color: var(--text);
  overflow-wrap: anywhere;
}
.craps-receipt__v.is-mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.craps-receipt__note {
  grid-column: 1 / -1;
  margin-top: 4px;
  color: var(--faint);
  line-height: 1.4;
}

/* ══ Toast ═══════════════════════════════════════════════════════════════ */

.craps-toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--nav-height, 64px) + var(--fym-safe-bottom, 0px) + 12px);
  transform: translateX(-50%) translateY(8px);
  max-width: min(92vw, 460px);
  padding: 10px 16px;
  border-radius: 12px;
  background: rgba(8, 12, 10, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.2);
  color: var(--cr-ink);
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1.35;
  text-align: center;
  z-index: 60;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-small) var(--easing),
              transform var(--motion-small) var(--easing);
}
.craps-toast.is-shown { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ══ Rules sheet ═════════════════════════════════════════════════════════ */

.craps-sheet {
  position: fixed;
  inset: 0;
  z-index: 70;
  background: rgba(5, 8, 6, 0.9);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: calc(var(--fym-safe-top, 0px) + var(--sp-md)) var(--sp-sm)
           calc(var(--fym-safe-bottom, 0px) + var(--sp-md));
}
.craps-sheet[hidden] { display: none; }

.craps-sheet__panel {
  position: relative;
  width: 100%;
  max-width: 460px;
  max-height: 100%;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: linear-gradient(180deg, #101a13, #0b120d);
  border: 1px solid var(--cr-gold-dim);
  border-radius: 16px;
  padding: var(--sp-sm);
  color: var(--cr-ink);
}
.craps-sheet__close {
  position: sticky;
  top: 0;
  float: right;
  appearance: none;
  -webkit-appearance: none;
  font-family: inherit;
  font-size: var(--fs-caption);
  font-weight: 700;
  color: var(--muted);
  background: rgba(8, 12, 10, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 999px;
  min-height: 34px;
  padding: 0 14px;
  cursor: pointer;
  margin-left: var(--sp-2xs);
}
.craps-sheet__title { color: var(--cr-gold); font-size: var(--fs-h3); margin: 0 0 4px; }
.craps-sheet__lead { color: #d9d3c4; font-size: var(--fs-small); line-height: 1.5; }
.craps-sheet__panel h3 {
  color: var(--cr-gold);
  font-size: var(--fs-small);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  margin: var(--sp-sm) 0 var(--sp-3xs);
  border-bottom: 1px solid var(--cr-gold-dim);
  padding-bottom: 4px;
}
.craps-sheet__list { padding-left: 18px; margin: 0; }
.craps-sheet__list li {
  font-size: var(--fs-small);
  line-height: 1.5;
  color: #d9d3c4;
  margin-bottom: 6px;
}
.craps-sheet__table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-caption);
  margin-top: 4px;
}
.craps-sheet__table th {
  text-align: left;
  color: var(--cr-gold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 4px 6px 4px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.craps-sheet__table td {
  padding: 5px 6px 5px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  color: #d9d3c4;
  vertical-align: top;
}
.craps-sheet__table td:first-child { font-weight: 700; color: var(--cr-ink); white-space: nowrap; }
.craps-sheet__edge { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ══ Reduced motion ══════════════════════════════════════════════════════
   tokens.css already collapses every duration to 0.01ms. What is left to
   do here is the thing a duration cannot express: the rattle and the
   landing pop are motion for its own sake, so they are removed rather
   than shortened. The pit itself is never built when this is set — see
   ensurePit() — so the stage is the flat dice and there is nothing
   spinning behind this rule. */
@media (prefers-reduced-motion: reduce) {
  .craps-flat.is-shaking .craps-flatdie,
  .craps-flat.is-landed .craps-flatdie,
  .craps-zone.is-flash {
    animation: none;
  }
  .craps-callout { transform: none; }
  .craps-callout.is-shown { transform: none; }
  /* The drink and the thump are motion for its own sake and their buttons
     are hidden outright when this is set — ensurePit() never builds the pit,
     so there is no glass and no hand — but the transitions are killed here
     too, because a rule that only holds because of a JS branch is a rule
     waiting to be wrong. */
  .craps-tbtn,
  .craps-hud,
  .craps-bar,
  .craps-theatre {
    transition: none;
  }
  /* THE ROOM STOPS DRIFTING. These four blobs and sixteen bokeh points are
     ambience and nothing else — they carry no state and say nothing — so
     under reduced motion they hold still rather than run fast. tokens.css
     collapses the duration, which for an `infinite alternate` animation is
     not stillness but a strobe. `animation: none` is.

     The SLOW MOTION tag keeps its words and loses its pulse: the words are
     information, the throb is decoration. */
  .craps-blob,
  .craps-bokeh,
  .craps-stage.is-slowmo .craps-slowmo {
    animation: none;
  }
  /* Defensive, and deliberately so. Under reduced motion `ensurePit()` never
     builds a context, `floatText()` returns on the null `gl`, and no float is
     ever placed — so this rule should be unreachable. It is here because the
     one thing worse than a float that bounces is a float that appears, never
     animates, and therefore never fires the `animationend` that removes it. */
  .craps-float { animation: craps-floatstill 1.6s linear forwards; }
}
@keyframes craps-floatstill {
  0% { opacity: 0; }
  6% { opacity: 1; }
  80% { opacity: 1; }
  100% { opacity: 0; }
}

/* ══ Short viewports ═════════════════════════════════════════════════════
   An SE-class phone under the app's header and nav has roughly 480 px of
   content. The pit is what gives, because the felt is the part you have to
   be able to hit. */
@media (max-height: 700px) {
  #craps-screen { --cr-hud-h: 36px; }
  .craps-stage { height: clamp(140px, 24vh, 200px); }
  /* THE CHIPS DO NOT SHRINK HERE ANY MORE. This used to take them to 42 px
     on any viewport 700 px tall or shorter — a 375x667 phone — where the
     prototype's are 54 px at every size. They are the dominant object in the
     lower third and the one control the owner flagged by eye as too small;
     the height comes out of the stage clamp above, which is a budget and can
     give it. */
  /* The two theatre buttons do NOT shrink with the chips. 44 px is the
     target-size floor, and a control that is optional is not a reason to go
     under it — if the corner is too tight the answer is to sit them closer
     together, not to make them smaller. */
  .craps-theatre { gap: 4px; }
  .craps-zone { min-height: 40px; padding: 4px 6px 5px; }
  .craps-zone--line { min-height: 46px; }
  /* No table override here. `.craps.is-table .craps-stage` is already a
     budget rather than a fraction — it shrinks with the viewport on its own,
     and it outranks the rule above whatever the media order. What this block
     still does for the table is shrink the chip rail, which is the only
     other thing on the screen that can give. */
}

/* Wide enough to sit side by side — a tablet held in landscape, or a
   desktop browser. The felt keeps its column so the tap targets stay the
   size a thumb needs. */
@media (min-width: 720px) and (min-height: 560px) {
  .craps-stage { height: clamp(240px, 38vh, 380px); }
}
