/* ============================================================
   THE COIN ORIGINALS SHEET — one dialog container, every game.

   Taken from the flappy-crash build, which had the cleanest version of
   it, and generalised so a game only has to bring its own content:

     <dialog class="cpsheet" id="…" aria-labelledby="…">
       <div class="cpsheet__head">
         <h2 class="cpsheet__title" id="…">Provably fair</h2>
         <div class="cpsheet__tools">
           <button class="cpsheet__x" data-cpclose aria-label="Close">✕</button>
         </div>
       </div>
       <div class="cpsheet__body"> … </div>
       <div class="cpsheet__foot"><button class="cpbtn" data-cpclose>Close</button></div>
     </dialog>

   WHY A SHARED FILE AND NOT A COPY PER GAME. Eight house games each
   re-inventing a modal is eight chances to disagree about the same
   thing, and the one panel that is IDENTICAL in every game — Provably
   Fair — is the one a player is most likely to compare across them. A
   different border radius on the fairness dialog is a small thing that
   reads as two different operators.

   It is plain CSS with its own fallbacks, so it works in a game that
   has never heard of the prototype's tokens, and defers to them where
   they exist. Classic <link>, no build step, fine from file://.

   `<dialog>` is used natively but WITHOUT showModal() — a game runs in
   an iframe and the top layer is the iframe's, not the app's, so a
   modal dialog would trap focus inside a frame the player can tab out
   of anyway. Open it with [open] plus .is-on and put the scrim inside
   the game's own frame. See games/Plinko for the wiring.
   ============================================================ */

/* The tokens are declared on BOTH entry points, not on the dialog alone.
   A game that mounts the fairness panel into a dialog of its own — which
   BirdCrash does, into markup that predates this folder — would otherwise
   get a panel whose every var() is undefined, and an undefined custom
   property does not fall back, it computes to the property's INITIAL value:
   transparent wells, and borders that resolve to currentColor. The panel
   came out as unstyled text on a card and looked like a stylesheet that had
   failed to load, when in fact it had loaded and had nothing to read. A
   component that only works inside one container is a trap for the second
   caller, so both carry the block. */
.cpsheet,
.cpfair,
.cpset,
.cpscroll {
  --cps-ink:      #E7E9F0;
  --cps-ink-mid:  #A7ACBD;
  --cps-ink-dim:  #767C90;
  --cps-ink-faint:#565C6E;
  --cps-hair:     #24273180;
  --cps-hair-hi:  #363A49;
  --cps-red:      var(--cp-red, #C0180C);
  --cps-red-hi:   #E0392B;
  --cps-green:    #038B68;
  --cps-well:     #07080B;
}

.cpsheet {
  position: absolute; left: 50%; top: 50%;
  /* COUNTER-SCALED, so one dialog is one size in every game. Plinko and
     BirdCrash scale their whole cabinet and a sheet inside one inherits it:
     the same 640px card measured 549 in BirdCrash and 650 in Plinko.
     --cpinv is 1/scale, published on the cabinet by _shared/chrome.js. */
  transform: translate(-50%, -50%) scale(calc(var(--cpinv, 1) * .97));
  margin: 0; padding: 0;
  /* Wide, because the values inside are 64 hexadecimal characters and a
     narrow card spends its height scrolling past truncated strings. The
     panel is short and wide by nature; making it narrow and tall was
     fighting the content. */
  /* THE DEFAULT IS THE MIDDLE OF THREE. A panel that is a TOOL (seeds, a
     switch list) wants ~380 and a panel that is a DOCUMENT wants ~900;
     both are modifiers below. 640 stays the default only because every
     existing caller already sits at it. */
  width: 640px;
  /* the frame, not the viewport: this sits inside a game window that is
     itself a fraction of the page */
  /* THE CAP IS IN RENDERED PIXELS. 100% is the parent's CSS width, which in a
     scaled cabinet is not what the player sees: Plinko's 620px portrait stage
     capped the card at 514 where Dice's 740px frame gave 640. Dividing by
     --cpinv converts the parent to screen px first, so all four cap together. */
  max-width: calc(100% * var(--cpk, 1) - 56px);
  /* HEIGHT HUGS CONTENT. It was a fixed cap on every panel, so an empty
     list held a full-height box open. Spribe's empty Bet History collapses
     to 99px; this is the same rule, with the cap kept as a ceiling only. */
  max-height: calc(100% - 56px);
  height: auto;
  display: none;
  flex-direction: column;
  z-index: 31;
  border: 1px solid var(--cp-border, #35394A);
  border-radius: 12px;
  background: var(--cps-card, #1A1D26);
  color: var(--cps-ink);
  /* TIER 1 of two. The 60px red glow that stood here had no light source in
     the cabinet to justify it and distinguished this surface from nothing --
     unmotivated elevation is the tell, not elevation. .cpnest below is tier
     2: same fill, half the radius, a third the shadow. */
  box-shadow: var(--cps-elev-1, 0 16px 48px rgba(0, 0, 0, .34)),
              inset 0 1px 0 rgba(223, 226, 238, .06);
  opacity: 0;
  transition: opacity .2s cubic-bezier(.2, .7, .3, 1),
              transform .2s cubic-bezier(.2, .7, .3, 1);
  overflow: hidden;
}
/* ---- THE TWO SIZES, because a tool and a document are not the same object.
   A seed panel is a working surface: narrow, dense, every row an action. A
   rules panel is a document: wide, prose-measure, read once. Trying to be
   both is what makes a panel 640px of neither -- Hacksaw's single
   undifferentiated surface runs 3+ full screens for exactly this reason. ---- */
.cpsheet--tool { width: 380px; }
.cpsheet--doc  { width: 900px; }

.cpsheet[open] { display: flex; }
.cpsheet.is-on { opacity: 1; transform: translate(-50%, -50%) scale(var(--cpinv, 1)); }

/* Some games already centre the panel inside their own scrim and drive
   the open state from the scrim. They get the same skin with none of the
   positioning — which is the whole point of separating the two: sharing
   a container must not mean adopting somebody else's open/close code. */
.cpsheet--inline {
  /* IT MUST NOT SHRINK. The inline variant is a flex ITEM in the game's own
     scrim, so a 640px card inside a 620px authored stage was shrunk to fit and
     rendered 618 where its siblings rendered 640. max-width already governs the
     cap; flex-shrink was quietly applying a second, narrower one.
     flex:none.  */
  flex: none;
  position: static;
  /* the inline variant is centred by its own scrim, so it needs the
     counter-scale without the centring translate */
  transform: scale(var(--cpinv, 1));
  margin: auto;
  display: flex;
  opacity: 1;
}

/* the scrim belongs to the GAME's frame, not the page */
.cpscrim {
  position: absolute; inset: 0; z-index: 30;
  /* THE GAME HAS NOT STOPPED, SO THE SCRIM MUST NOT SAY IT HAS. Both studio
     references keep the round running behind an open panel -- verified, not
     assumed: Hacksaw's balance moved with GAME INFO open, and a Spribe round
     resolved behind How to Play. Spribe dims to .15; that is measured against
     a lighter board than ours, and on a near-black cabinet .15 reads as
     nothing at all (the same effect the Roobet capture showed). .55 with no
     blur is the value that dims without erasing -- the pegs, the multiplier
     row and the balance all stay legible. Rendered, not reasoned. */
  background: rgba(6, 6, 9, .55);
  border-radius: inherit;
  opacity: 0;
  transition: opacity .2s cubic-bezier(.2, .7, .3, 1);
}
.cpscrim[hidden] { display: none; }
.cpscrim.is-on { opacity: 1; }

.cpsheet__head {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--cps-hair);
  /* THE RED WASH IS GONE. Red was doing five jobs on this panel -- the glow,
     this wash, the shield, the rotate button and the close button -- and a
     colour that means five things means none. It keeps the one that is an
     ACTION. Hacksaw rations its green to exactly two jobs; this is that rule. */
  flex: none;
}
.cpsheet__title { margin: 0; font-size: 17px; font-weight: 700; letter-spacing: -.01em; }
.cpsheet__tools { display: flex; align-items: center; gap: 12px; }
/* RETIRED FROM THE LIVE GAMES (owner, 2026-08-26): no game prints a round
   tag in the head any more. The rule stays because the frozen zz-*.html
   snapshots link this file and still carry the span; do not add the tag
   to a new game. */
.cpsheet__tag {
  font-size: 10px; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--cps-ink-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.cpsheet__x {
  width: 30px; height: 30px; flex: none;
  display: grid; place-items: center;
  border-radius: 8px;
  border: 1px solid var(--cps-hair-hi);
  background: none;
  color: var(--cps-ink-dim);
  font-size: 16px; line-height: 1;
  cursor: pointer;
  transition: color .16s, border-color .16s;
}
@media (hover:hover){.cpsheet__x:hover{ color: var(--cps-ink); border-color: var(--cps-red-hi); }}

.cpsheet__body {
  padding: 16px 18px;
  display: grid; gap: 12px;
  /* minmax(0,1fr), not 1fr. A grid item's automatic minimum size is its
     CONTENT, so a 64-character seed hash widens the track past the card
     and pushes the copy buttons out through the right edge — the ellipsis
     on the value never gets a chance to apply because the column grew
     instead. This is the single most likely thing to break when a game
     puts a long value in a shared panel. */
  grid-template-columns: minmax(0, 1fr);
  overflow: auto;
  /* the only part that scrolls, so a long panel never pushes the close
     control off the bottom of a short game window */
  min-height: 0;
}
.cpsheet__body p { margin: 0; font-size: 12.5px; color: var(--cps-ink-mid); line-height: 1.65; }
.cpsheet__body p b { color: var(--cps-ink); font-weight: 600; }
.cpsheet__body h3 {
  margin: 4px 0 0;
  font-size: 11px; font-weight: 700;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--cps-ink-dim);
}

/* [hidden] MUST WIN. The attribute is display:none by default, but the rule
   below sets display:flex and a class beats a UA default -- so a foot hidden
   by a game stayed on screen. Dice hides its foot on every door but autobet
   and the button kept showing until this existed. */
.cpsheet__foot[hidden] { display: none !important; }
.cpsheet__foot {
  padding: 12px 18px 16px;
  display: flex; justify-content: flex-end; gap: 10px;
  border-top: 1px solid var(--cps-hair);
  flex: none;
}

/* ---- RETIRED PAIRS: a receipt drawer, not a second panel. It appears only
   after a rotation, so it must not change the shape of the thing above it --
   it is capped and scrolls instead of growing, and every pair is three tight
   lines rather than two labelled wells. ---- */
.cpfair__retired { margin-top: 0; }
.cpret__h {
  display: flex; align-items: baseline; justify-content: space-between;
  padding-bottom: 7px; margin-bottom: 2px;
  border-bottom: 1px solid var(--cps-hair);
}
.cpret__h span {
  font-weight: 600; font-size: 9.5px; line-height: 1;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--cps-ink-dim);
}
.cpret__h b { font-weight: 600; font-size: 11px; color: var(--cps-ink-mid); }

/* CAPPED. Two rotations used to add 300px to a panel that had already been
   sized for its content; past three pairs this scrolls and the panel does not
   move. */
.cpret__list {
  list-style: none; margin: 0; padding: 0;
  /* NO INNER SCROLL. The body is the only thing on this sheet that scrolls;
     a capped list inside it was a scroller within a scroller. In its own tab
     the list can simply be as long as it is. */
}
.cpret__i { padding: 9px 0 8px; }
.cpret__i + .cpret__i { border-top: 1px solid var(--cps-hair); }
.cpret__top { display: flex; align-items: baseline; gap: 8px; margin-bottom: 5px; }
.cpret__top b { font-weight: 600; font-size: 11.5px; color: var(--cps-ink); }
.cpret__top span { font-size: 10.5px; color: var(--cps-ink-dim); }

/* one line a seed: a tiny role label, the value, and the only thing anyone
   came here to do. No well -- a value you copy and leave does not need a box. */
/* ASKED FOR, NOT DUMPED. A revealed server seed is a secret that has done its
   job -- it still has to be reachable or a past round cannot be checked, but it
   does not have to be the first thing on the row. */
.cpret__d > summary {
  cursor: pointer; list-style: none;
  font-weight: 600; font-size: 10px; line-height: 1;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--cps-ink-dim);
  padding: 3px 0;
}
.cpret__d > summary::-webkit-details-marker { display: none; }
.cpret__d > summary::after { content: " +"; }
.cpret__d[open] > summary::after { content: " −"; }
@media (hover:hover){.cpret__d > summary:hover{ color: var(--cps-ink); }}
.cpret__d[open] > summary { margin-bottom: 5px; }

/* A CLOSED <details> HIDES ITS CHILDREN VIA A UA RULE, AND AN EXPLICIT display
   BEATS IT. .cpret__v sets display:grid, so the seeds kept rendering inside a
   details that reported open=false -- the same shape of bug as .cpsheet__foot
   losing to display:flex earlier in this pass. State it outright. */
.cpret__d:not([open]) .cpret__v { display: none; }

.cpret__v {
  display: grid; grid-template-columns: 42px minmax(0, 1fr) auto;
  align-items: center; gap: 8px;
}
.cpret__v + .cpret__v { margin-top: 3px; }
.cpret__v > span {
  font-weight: 600; font-size: 9px; line-height: 1;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--cps-ink-dim);
}
.cpret__v code {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 10.5px; color: var(--cps-ink-mid);
}

/* ---- the document half of the fairness panel: three questions, one sentence
   each. A heading in WEIGHT, an answer in the body colour -- hierarchy from
   weight and colour rather than from size, which is how Spribe runs ~90% of
   its system at 12-14px. ---- */
.cpfair__q { display: grid; gap: 3px; }
.cpfair__q + .cpfair__q { margin-top: 12px; }
.cpfair__q b {
  font-weight: 600; font-size: 12px; line-height: 1;
  color: var(--cps-ink);
}
.cpfair__q p {
  margin: 0; font-size: 12px; line-height: 1.6;
  color: var(--cps-ink-mid);
}
.cpfair__q em { font-style: normal; color: var(--cps-ink); }

/* ---- THE ACTION BLOCK. Everything above it on this panel is a value to read;
   this is the one thing that CHANGES something, so it sits on its own ground
   instead of wearing the same well as the readings. No border -- a raised fill
   and the space around it are enough, which is the rule the rest of this sheet
   follows. ---- */
.cpfair__rot {
  display: grid; gap: 8px;
  margin-top: 6px;
  padding: 13px 14px 12px;
  border-radius: 10px;
  background: var(--cps-raise, rgba(223, 226, 238, .035));
}
.cpfair__rothead {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
/* OFFERED, NOT DEMANDED. "Type a seed" is a blank stare for most players, and
   any seed is as good as any other -- what matters is that it is theirs. */
.cpfair__dice {
  border: 0; background: none; cursor: pointer; padding: 0;
  font-weight: 600; font-size: 10.5px; line-height: 1;
  letter-spacing: .04em;
  color: var(--cps-ink-dim);
  text-decoration: underline; text-underline-offset: 3px;
  text-decoration-color: var(--cps-hair-hi);
}
@media (hover:hover){.cpfair__dice:hover{ color: var(--cps-ink); text-decoration-color: currentColor; }}
.cpfair__dice:focus-visible { outline: 2px solid var(--cps-red-hi); outline-offset: 3px; border-radius: 4px; }

.cpfair__rotrow { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
.cpfair__rotrow > * { min-width: 0; }
/* THE FIELD ANNOUNCES THAT IT IS ONE. It was styled exactly like the read-only
   wells above, so nothing said it could be typed into; it is lighter than they
   are, and it takes a real focus ring. */
.cpfair__rotrow input {
  padding: 10px 11px;
  background: var(--cps-well);
  border: 0; border-radius: 8px;
  box-shadow: inset 0 0 0 1px var(--cps-hair-hi);
  color: var(--cps-ink);
  font-weight: 400; font-size: 12px; line-height: 1.2;
  font-family: var(--cps-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
}
.cpfair__rotrow input::placeholder { color: var(--cps-ink-dim); }
.cpfair__rotrow input:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 1px var(--cps-red-hi);
}

/* THE ONE RED OBJECT ON THE PANEL, and it says what it does. It was a bare
   rectangle reading "Rotate" -- a verb with no object, on a control whose whole
   point is that it swaps two named things. The glyph is the same arc-and-arrow
   the rest of the house uses for a cycle. */
.cpfair__go {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 0 15px; height: 38px; flex: none;
  border: 0; border-radius: 8px; cursor: pointer;
  background: var(--cps-red); color: #fff;
  font-weight: 600; font-size: 12.5px; line-height: 1;
  white-space: nowrap;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .16), 0 1px 0 rgba(0, 0, 0, .5);
  transition: background .14s, transform .08s, box-shadow .14s;
}
.cpfair__go svg {
  width: 15px; height: 15px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
@media (hover:hover){.cpfair__go:hover{ background: var(--cps-red-hi); }}
/* it presses. A CTA that does not move under the finger is the tell that it was
   drawn rather than built. */
.cpfair__go:active { transform: translateY(1px); box-shadow: inset 0 2px 5px rgba(0, 0, 0, .45); }
.cpfair__go:focus-visible { outline: 2px solid var(--cps-red-hi); outline-offset: 2px; }

/* the consequence, tied to the button that causes it */
.cpfair__commit {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 11px; color: var(--cps-ink-dim);
}
.cpfair__commit code {
  min-width: 0; flex: 1 1 120px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 10.5px; color: var(--cps-ink-mid);
}

/* ---- ONE LINE OF FACTS. Limits, RTP, edge: the numbers a player checks
   once and does not read. Hairlines BETWEEN, never around; values hug their
   own width; nothing here is filled, because a fill makes a number look like
   a control. ---- */
.cpfacts {
  display: flex; flex-wrap: wrap; align-items: baseline;
  margin: 0 0 14px;
  font-variant-numeric: tabular-nums;
}
.cpfacts > div {
  display: flex; align-items: baseline; gap: 6px;
  padding: 2px 11px;
  border-left: 1px solid var(--cps-hair);
}
/* none above the first, none below the last, none around the group */
.cpfacts > div:first-child { padding-left: 0; border-left: 0; }
.cpfacts span {
  /* separate properties, not the shorthand: font:...var(--x, inherit) is an
     INVALID declaration -- inherit is not a legal component value there -- so
     the browser dropped the size with it and these rendered at the inherited
     14px. Measured, not guessed. */
  font-weight: 600; font-size: 9px; line-height: 1;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--cps-ink-dim);
  white-space: nowrap;
}
/* 13px, not 18. The figure is a fact, not the subject of the panel. */
.cpfacts b {
  font-weight: 600; font-size: 12.5px; line-height: 1;
  color: var(--cps-ink);
  white-space: nowrap;
}

/* ---- TABS INSIDE A PANEL. The split Spribe gets from two separate
   surfaces, in one card -- which is what suits a panel opened from a game
   window rather than from a site menu. ---- */
.cpsheet__tabs {
  display: flex; gap: 2px;
  margin: 0 0 14px;
  border-bottom: 1px solid var(--cps-hair);
}
.cpsheet__tabs button {
  padding: 8px 13px; border: 0; background: none; cursor: pointer;
  font-weight: 600; font-size: 12px; line-height: 1;
  color: var(--cps-ink-dim);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color .14s, border-color .14s;
}
@media (hover:hover){.cpsheet__tabs button:hover{ color: var(--cps-ink); }}
/* THE SELECTED TAB IS MARKED BY WEIGHT AND A RULE, not by a filled pill.
   A filled pill in a panel that also contains filled value wells makes two
   different things look like the same thing. */
.cpsheet__tabs button[aria-selected="true"] {
  color: var(--cps-ink);
  border-bottom-color: var(--cps-red-hi);
}
.cpsheet__tabs button:focus-visible {
  outline: 2px solid var(--cps-red-hi); outline-offset: -2px; border-radius: 6px;
}
/* the panel is a grid like .cpsheet__body itself: its children were inheriting
   no gap because a plain div broke the body grid, so two paragraphs ran
   together with no air between them. */
.cptabpanel { display: grid; gap: 12px; align-content: start; }
.cptabpanel[hidden] { display: none !important; }

/* ---- a labelled value with a copy button ---- */
/* minmax(0,1fr) again, one level down — the row is itself a grid, and a
   64-character hash inside it will widen this track too if it is allowed to */
.cpseed { display: grid; grid-template-columns: minmax(0, 1fr); gap: 5px; }
.cpseed > * { min-width: 0; }
.cpseed__lbl {
  font-size: 10px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--cps-ink-dim);
}
.cpseed__val {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 11px;
  /* SEPARATED BY FILL AND SPACE, NOT BY OUTLINE. A hairline around every one
     of five identical boxes is what flattens the hierarchy -- everything is
     equally framed, so nothing is primary. Spribe outlines nothing and uses
     hairlines only BETWEEN rows; Hacksaw's rules document has zero borders in
     it at all. The well already reads as a field because it is darker than
     the card; the border was saying it twice. */
  background: var(--cps-well);
  border: 0;
  border-radius: 8px;
}
.cpseed__val code,
.cpseed__val input {
  flex: 1; min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px;
  color: var(--cps-ink);
  background: none; border: 0; padding: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cpseed__val input:focus { outline: none; color: #fff; }
/* not yet knowable — greyed rather than hidden, because the fact that it
   exists and is withheld until the reveal IS the mechanism */
.cpseed__val[data-locked="true"] code { color: var(--cps-ink-faint); }

.cpcopy {
  position: relative; flex: none;
  height: 26px; padding: 0 9px;
  border: 1px solid var(--cps-hair-hi); border-radius: 5px;
  background: none;
  font-size: 9px; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--cps-ink-dim);
  cursor: pointer;
  transition: color .14s, border-color .14s;
}
@media (hover:hover){.cpcopy:hover{ color: var(--cps-ink); border-color: var(--cps-red-hi); }}
.cpcopy[data-copied="true"] { color: #17C79A; border-color: var(--cps-green); }

/* same reason as the body: two columns that refuse to shrink below their
   content are two columns that will not fit a narrow game window */
.cpgrid2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
.cpgrid2 > * { min-width: 0; }

.cpbtn {
  height: 38px; padding: 0 20px;
  border-radius: 9px;
  border: 1px solid var(--cps-hair-hi);
  background: none;
  color: var(--cps-ink);
  font-weight: 700; font-size: 12px;
  letter-spacing: .1em; text-transform: uppercase;
  cursor: pointer;
  transition: color .14s, border-color .14s, background-color .14s;
}
@media (hover:hover){.cpbtn:hover{ border-color: var(--cps-red-hi); }}
.cpbtn--primary {
  background: var(--cps-red); border-color: var(--cps-red); color: #fff;
}
@media (hover:hover){.cpbtn--primary:hover{ background: var(--cp-red-hover, #A3140A); }}

/* ---- the scroller, on every panel that scrolls -------------------------
   A panel is the one place in a casino game where a player reads rather
   than plays, and it is also the only place a scrollbar appears — so the
   default one is the single most off-brand pixel in the product: a
   light-grey Windows bar down the side of a black card.

   Branded, not decorated: the track is the well the fields already sit in,
   the thumb is the house red at rest rather than on hover, because a
   scrollbar that only shows its colour when you touch it has told you
   nothing at the moment you needed to know there was more. Overlay width,
   so the content does not reflow when a panel grows past its height.

   `.cpscroll` is the same treatment for a game whose panels are not
   .cpsheet__body yet — one class to add rather than a copy of this block. */
.cpsheet__body,
.cpscroll {
  scrollbar-width: thin;
  scrollbar-color: var(--cps-red, #C0180C) transparent;
  overscroll-behavior: contain;
}
.cpsheet__body::-webkit-scrollbar,
.cpscroll::-webkit-scrollbar { width: 10px; height: 10px; }
.cpsheet__body::-webkit-scrollbar-track,
.cpscroll::-webkit-scrollbar-track {
  background: transparent;
  border-left: 1px solid var(--cps-hair, #24273180);
}
.cpsheet__body::-webkit-scrollbar-thumb,
.cpscroll::-webkit-scrollbar-thumb {
  border-radius: 99px;
  /* inset by its own transparent border so the thumb reads as 6px of red
     floating in the gutter rather than a 10px bar welded to the edge */
  border: 2px solid transparent;
  background-clip: padding-box;
  background-color: var(--cps-red, #C0180C);
}
@media (hover:hover){.cpsheet__body::-webkit-scrollbar-thumb:hover, .cpscroll::-webkit-scrollbar-thumb:hover{
  background-color: var(--cps-red-hi, #E0392B);
}}
.cpsheet__body::-webkit-scrollbar-corner,
.cpscroll::-webkit-scrollbar-corner { background: transparent; }

/* ---- and the edge that says there IS more ----
   THE SCROLLBAR IS NOT THE AFFORDANCE. The thumb above is already the house
   red at rest, for the reason written there — and Coin Dice, the first game
   with a panel long enough to overflow this box, was still read by three
   separate reviewers as a panel that simply ENDS where it was cut: the
   fairness disclosure stopping on "…this is ours, exactly:" with the
   construction below the fold, Help cut mid-heading, the ledger sliced through
   two tiles, the autobet inputs halved. A 6px bar in a gutter answers "can I
   scroll"; it does not answer "is there more", which is the question.

   It is Plinko's idiom, not a new one: `#dock[data-scroll] .dkMid` masks a zone
   only while that zone is ACTUALLY scrolling, and its own note says why —
   "masking a caption that is not going anywhere reads broken". So the fade is
   keyed on an attribute, the attribute is written by measurement, and a panel
   that fits has no mask at all. games/_shared/sheet.js does the measuring and
   installs itself; a game adds one <script> tag and writes no code.

   A GRADIENT MASK, NEVER A url(). A mask image is origin-restricted where a
   background-image is not, so over file:// a url() mask fails silently AND
   masks its element out entirely — correct box, nothing painted, nothing
   logged. tokens.css inlines its woff2 for the same reason. There is no URL
   here and there must never be one.

   AND IT FADES A WHOLE ROW, TO FAINT RATHER THAN TO NOTHING. The first cut was
   a 30px fade to transparent and it was INVISIBLE in the render — dark content
   dissolving on a dark panel is no signal at all, and the shot showed exactly
   the hard cut it was meant to remove. Stopping short of 0 is what makes it
   read as CONTINUING rather than as ending: content going to nothing looks
   deliberate, content going faint looks like there is more of it. Checked in
   the picture, in Mines and in Dice, not in the computed style.
   64px AND .05, UP FROM 44px AND .12 — casino-manager, Dice round: at
   380x300 the sheet body is 144px tall, and the 44px band left the last
   sliced row at enough opacity that the cut still read hard against the foot
   bar. The deeper band starts the dissolve a full row earlier and lands
   fainter, so the slice is unmistakably "more below", not "cut off".
   Re-rendered in Dice, Mines AND Plinko at 380x300 before it was kept —
   the file is shared, and Plinko's boxed frame never scrolls this body at
   that size, so only the two panels that show the cue changed. */
.cpsheet__body[data-cpscroll="bottom"],
.cpscroll[data-cpscroll="bottom"] {
  -webkit-mask-image: linear-gradient(180deg, #000 calc(100% - 64px), rgba(0,0,0,.05) 100%);
  mask-image: linear-gradient(180deg, #000 calc(100% - 64px), rgba(0,0,0,.05) 100%);
}
.cpsheet__body[data-cpscroll="top"],
.cpscroll[data-cpscroll="top"] {
  -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,.12) 0, #000 30px);
  mask-image: linear-gradient(180deg, rgba(0,0,0,.12) 0, #000 30px);
}
.cpsheet__body[data-cpscroll="both"],
.cpscroll[data-cpscroll="both"] {
  -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,.12) 0, #000 30px,
                                      #000 calc(100% - 44px), rgba(0,0,0,.12) 100%);
  mask-image: linear-gradient(180deg, rgba(0,0,0,.12) 0, #000 30px,
                              #000 calc(100% - 44px), rgba(0,0,0,.12) 100%);
}
/* the fade must not eat the last line: the body's own bottom padding is what
   the mask is spending, so a scrolling panel gets a little more of it */
.cpsheet__body[data-cpscroll] { padding-bottom: 26px; }

/* ---- the provably-fair panel ----
   The panel is mounted into a plain div inside .cpsheet__body, so the body's
   own 14px grid gap applies to that ONE div and not to anything inside it —
   which is why the rotate button sat flush against the field above it and the
   rule read as a stray line under the nonce. The mount point is a grid too. */
.cpfair{ display:grid; grid-template-columns:minmax(0,1fr); gap:12px; }

/* ---- the provably-fair panel's own three parts ---- */
/* the active triple and the pre-committed pair are two groups, so they are
   spaced as groups rather than as five equal rows */
.cpfair__now,
.cpfair__next { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
/* room on both sides, or it reads as a stray line rather than as the break
   between what is in play and what is committed next */
.cpfair__rule {
  border: 0; border-top: 1px solid var(--cps-hair-hi);
  margin: 2px 0;
  align-self: center;
}
/* the only button in the panel, so it takes the width rather than hiding in
   a corner of a foot bar */
.cpfair__rotate { width: 100%; }
.cpfair__link {
  justify-self: start;
  background: none; border: 0; padding: 0;
  font-size: 11px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--cps-ink-dim); cursor: pointer;
  transition: color .14s;
}
@media (hover:hover){.cpfair__link:hover{ color: var(--cps-ink); }}

/* ---- the settings panel ----
   Rows, not an icon list: a switch needs a name, and a name that is only an
   icon is a name the player has to already know. The note under each label is
   what the setting actually costs them — "Sound" alone does not say whether it
   is the chime or the music. */
.cpset { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
.cpset__row {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 11px 12px;
  background: var(--cps-well);
  border: 1px solid var(--cps-hair-hi);
  border-radius: 10px;
  min-width: 0;
}
.cpset__t { display: grid; gap: 2px; min-width: 0; }
.cpset__t strong { font-size: 13px; font-weight: 600; color: var(--cps-ink); }
.cpset__t span { font-size: 11.5px; color: var(--cps-ink-dim); }
/* the switch reads its own state as a word, because a coloured pill with no
   label is a state you have to remember the convention for */
.cpset__sw {
  flex: none; min-width: 58px; height: 30px; padding: 0 12px;
  border-radius: 99px;
  border: 1px solid var(--cps-hair-hi);
  background: none;
  color: var(--cps-ink-dim);
  font: inherit; font-size: 11px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
  cursor: pointer;
  transition: color .16s, border-color .16s, background-color .16s;
}
.cpset__sw[aria-checked="true"] {
  background: var(--cps-red);
  border-color: var(--cps-red);
  color: #fff;
}
@media (hover:hover){.cpset__sw:hover{ border-color: var(--cps-red-hi); }}

/* a range row stacks its label above the slider instead of sitting beside a
   switch — a slider narrower than the row is a slider the player fumbles for
   on a 380px docked window, so it always gets the row's full width */
.cpset__row[data-type="range"] { flex-direction: column; align-items: stretch; gap: 8px; }
.cpset__rangewrap { display: flex; align-items: center; gap: 10px; min-width: 0; }
.cpset__range {
  flex: 1; min-width: 0; height: 4px;
  accent-color: var(--cps-red, #C0180C);
  background: none; cursor: pointer;
}
.cpset__rangeval {
  flex: none; min-width: 2.4em; text-align: right;
  font: 700 12px/1 var(--cps-fig, ui-monospace, monospace);
  font-variant-numeric: tabular-nums;
  color: var(--cps-ink);
}

/* ---- narrow frames ---- */
@media (max-width: 640px) {
  /* SCALE-AWARE LIKE THE BASE RULE. This overrode it and did NOT multiply by
     --cpk, so on a 375px phone BirdCrash computed calc(100% - 24px) against a
     720px authored parent, kept the full 640px card and rendered it 640px wide
     on a 375px screen -- overflowing far enough to drag the cabinet 123px
     off-canvas. A narrower rule that forgets what the wider one accounts for is
     worse than no rule. */
  .cpsheet {
    max-width: calc(100% * var(--cpk, 1) - 24px);
    max-height: calc(100% * var(--cpk, 1) - 24px);
  }
  .cpsheet__body { padding: 14px; gap: 12px; }
  .cpgrid2 { grid-template-columns: 1fr; }
}

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

/* ---- the retired seed pairs (CPFair.set({ retired })) -------------------
   Section head + per-pair caption; the value rows are the panel's own
   .cpseed wells, so they need nothing here. */
.cpfair__rettitle{margin:18px 0 6px;font:700 10px/1 var(--cps-font-ui,inherit);letter-spacing:.14em;
  text-transform:uppercase;color:var(--cps-ink-dim)}
.cpfair__retnote{margin:0 0 10px;font-size:12px;color:var(--cps-ink-dim);line-height:1.55}
.cpfair__pair{display:flex;align-items:baseline;gap:8px;margin:10px 0 4px;font-size:12.5px;color:var(--cps-ink)}
.cpfair__pair span{color:var(--cps-ink-dim);font-size:11px}

/* ---- touch targets, COARSE POINTERS ONLY (Dice mobile round, 2026-08-27) -
   The settings switch is a 58x30 pill and the volume range a 4px strip —
   both under the 44px touch floor on the phones the sheet now serves.
   Shared file, so the fix is strictly additive and strictly gated:
   @media (pointer: coarse) never matches a desktop mouse, so every
   fine-pointer render of Mines, Plinko, BirdCrash and Dice is
   byte-identical to before this block existed (shot before/after at
   380x300 and 1280x760 — work/dice/shots/mobile/land/sheet-*).
   The switch keeps its exact 58x30 PAINT — the hit box alone grows to
   ~70x46 through an invisible pseudo-element (a pseudo is never the event
   target, so the delegation in settings.js still sees the button). The
   range grows its real box: a native slider's hit area IS its element,
   and a 4px element cannot be gripped by a finger at any paint size. */
@media (pointer: coarse){
  .cpset__sw{position:relative}
  .cpset__sw::after{content:"";position:absolute;left:-6px;right:-6px;top:-8px;bottom:-8px}
  .cpset__range{height:44px}
}


/* the pair says what it covered and where its seed now lives; it does not
   print the seed. See provably-fair.js for why. */
.cpret__hint{ font-size:11px; line-height:1.5; color:var(--cps-ink-dim); }
.cpret__hint b{ color:var(--cps-ink-mid); font-weight:600; }
/* THE LEDGER KEEPS THE SEED ONLY WHERE NOTHING ELSE CAN HOLD IT -- a game with
   no round list has no bet to hang the reveal on, so the pair entry carries it
   rather than pointing at a panel that does not exist. Same row shape as the
   seed line on a round, so the two read as one idea in two places. */
.cpret__seed{ display:flex; align-items:center; gap:8px; min-width:0; margin-top:6px; }
.cpret__lbl{ font-size:10px; letter-spacing:.02em; color:var(--cps-ink-dim); white-space:nowrap; }
.cpret__seed code{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:10px; }


/* THE SEED, ON THE ROUND IT DECIDED. It heads an opened breakdown because it
   belongs to the whole round rather than to any single result in it, and it is
   reachable only from a round -- which is the point of moving it off the pair
   ledger. The dim variant is the honest state while the pair is still live. */
.seedline .sl, .seedline .b{
  display:flex; align-items:center; gap:8px; min-width:0;
  font-size:10.5px; color:var(--cps-ink-mid, #9AA1B4);
}
.seedline .dim{ color:var(--cps-ink-dim, #6A7183); }
.seedline code{
  min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  font-size:10px;
}
