Files
JoshHeaps.Net/JoshHeaps.Net/wwwroot/css/casino/quicksplit.css
T
jheapsandClaude Opus 5 6fa4532781 Fix the contrast the audit could finally measure
Contrast is the one check I've got wrong twice, both times by being clever about
backgrounds. The first version read only background-color, so text on any of the
gradient-painted sheets was measured against transparency and the dealer puck's
dark "D" was reported at 1.2:1 against nothing. The second over-corrected and
treated low-alpha gradient stops as opaque, scoring white-on-dark at 1.0:1.

So this one is built to be incapable of false positives rather than exact.
Backgrounds composite through alpha; where a gradient is involved every colour stop
is tried and the most flattering one used. A finding means the text fails even
under the reading most generous to it.

It's checked against known answers before being believed: black on white at 21:1,
white on white at 1:1, #767676 on white at the 4.54:1 AA boundary, white over a
dark gradient, dark text on that same gradient, white over a 6% white wash on a
near-black page. Nine cases, all correct — and building them found a real bug in
the check, which walked past an opaque gradient and offered the page background as
a candidate. That made it too lenient, which is as bad as the old false positives.

What it found:

Both blind badges failed against their own gradients, in 9px bold text a dealer
reads at a glance across a table. At the worst stop: small blind 3.52:1, big blind
2.15:1. The small blind keeps its blue and darkens to 4.77:1. The big blind flips
to dark text on brighter gold, 5.84:1 — how a gold chip is marked anyway, and it
makes the two badges far easier to tell apart at speed.

Quick Split's footnote names a control the dealer is told to reach for, and that
name inherited the muted grey of the surrounding aside: 3.39:1. Emphasised text
carrying an instruction gets its own colour now.

The felt watermark came back at 1.19:1 and is meant to. It's decoration; holding it
to a reading threshold would mean making it not a watermark. It's aria-hidden now,
which says the same thing where it also does some good, and the audit skips
aria-hidden subtrees.

32 contrast passes over the table, all five overlays and Quick Split at five
viewports: zero findings, zero unmeasured. Layout audit still clean. 137 tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 08:39:07 -06:00

542 lines
10 KiB
CSS

/* Quick Split is phone-first: sized for one thumb at the table, then allowed to
breathe on a laptop. Palette matches the dealer table so the two read as one tool. */
body {
background: radial-gradient(ellipse at 50% -10%, #1c1c1c 0%, #101010 55%, #0a0a0a 100%);
min-height: 100vh;
margin: 0;
color: #e6e6e6;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* Same touch handling as the dealer table — see the note there. This page is the one
a dealer opens mid-hand to chop a pot, so the ~300ms the browser spends deciding
whether a tap was a double-tap-to-zoom is the last thing it needs. */
button,
[role="button"],
input,
select,
textarea,
a {
touch-action: manipulation;
}
button,
[role="button"],
input,
select,
textarea,
a {
-webkit-tap-highlight-color: transparent;
}
/* Not on the fields — a dealer typing a pot amount still needs to select it. */
button,
[role="button"] {
-webkit-user-select: none;
user-select: none;
}
.clearPotBtn:active:not(:disabled),
.choiceChip:active:not(:disabled),
.stepBtn:active:not(:disabled),
.splitBtn:active:not(:disabled),
.splitBtnGhost:active:not(:disabled),
.unevenFoldBtn:active:not(:disabled),
.unevenRemoveBtn:active:not(:disabled) {
transform: translateY(1px);
filter: brightness(1.25);
transition-duration: 0.04s;
}
@media (prefers-reduced-motion: reduce) {
.clearPotBtn:active:not(:disabled),
.choiceChip:active:not(:disabled),
.stepBtn:active:not(:disabled),
.splitBtn:active:not(:disabled),
.splitBtnGhost:active:not(:disabled),
.unevenFoldBtn:active:not(:disabled),
.unevenRemoveBtn:active:not(:disabled) {
transform: none;
}
}
#splitApp {
max-width: 480px;
margin: 0 auto;
padding: 1.25rem 1rem 3rem;
}
/* ---------- Header ---------- */
#splitHeader h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 300;
letter-spacing: 0.04em;
color: #f5f5f5;
position: relative;
display: inline-block;
padding-bottom: 0.35rem;
}
#splitHeader h1::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 36px;
height: 3px;
border-radius: 2px;
background: linear-gradient(90deg, #d8b46a, #a9782f);
}
.splitSubtitle {
margin: 0.35rem 0 1.25rem;
color: #8f8f8f;
font-size: 0.85rem;
}
/* ---------- Mode toggle ---------- */
#modeToggle {
margin-bottom: 1.1rem;
}
/* ---------- Uneven amounts ---------- */
#unevenRows {
display: flex;
flex-direction: column;
gap: 0.45rem;
margin-bottom: 0.6rem;
}
.unevenRow {
display: flex;
align-items: center;
gap: 0.45rem;
}
.unevenRowLabel {
flex: none;
width: 2rem;
color: #8f8f8f;
font-size: 0.8rem;
font-weight: 600;
}
.unevenAmountWrap {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 0.3rem;
background: #141414;
border: 1px solid #3a3a3a;
border-radius: 10px;
padding: 0 0.6rem;
}
.unevenAmountWrap:focus-within {
border-color: #d8b46a;
}
.unevenCurrency {
color: #777;
}
.unevenAmount {
flex: 1;
min-width: 0;
background: none;
border: none;
outline: none;
color: #f2f2f2;
font-family: inherit;
font-size: 16px;
font-variant-numeric: tabular-nums;
padding: 0.6rem 0;
}
.unevenFoldBtn {
flex: none;
min-height: 44px;
padding: 0 0.6rem;
border-radius: 9px;
border: 1px solid #3a3a3a;
background: #232323;
color: #8f8f8f;
font-family: inherit;
font-size: 0.72rem;
cursor: pointer;
}
.unevenFoldBtn.selected {
background: linear-gradient(180deg, #3d1f1f, #2c1515);
border-color: #5b2626;
color: #ffd6d6;
}
.unevenRemoveBtn {
flex: none;
width: 30px;
height: 44px;
border: none;
background: none;
color: #6a6a6a;
font-size: 0.85rem;
cursor: pointer;
}
.unevenRemoveBtn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
#unevenResult {
background: linear-gradient(160deg, #1f2a21, #161c17);
border: 1px solid #2c3a2f;
border-radius: 14px;
padding: 0.9rem 1rem;
margin-bottom: 1.5rem;
}
#unevenResult[hidden] {
display: none;
}
.unevenPot {
display: flex;
align-items: baseline;
gap: 0.6rem;
padding: 0.3rem 0;
font-size: 0.85rem;
}
.unevenPot + .unevenPot {
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.unevenPotLabel {
flex: none;
color: #d8b46a;
font-weight: 600;
}
.unevenPotAmount {
flex: none;
color: #f2f2f2;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.unevenPotWho {
flex: 1;
min-width: 0;
text-align: right;
color: #9a9a9a;
font-size: 0.78rem;
}
.unevenPotReturn .unevenPotLabel,
.unevenPotReturn .unevenPotAmount {
color: #9a9a9a;
font-weight: 500;
}
.unevenPotTotal {
margin-top: 0.15rem;
border-top: 1px solid rgba(255, 255, 255, 0.14) !important;
}
.unevenPotTotal .unevenPotLabel {
color: #8f9d92;
font-weight: 500;
}
.unevenEmpty {
margin: 0;
color: #8f9d92;
font-size: 0.82rem;
line-height: 1.45;
}
/* ---------- The answer ---------- */
#splitResult {
background: linear-gradient(160deg, #1f2a21, #161c17);
border: 1px solid #2c3a2f;
border-radius: 14px;
padding: 1.4rem 1.1rem;
text-align: center;
margin-bottom: 1.5rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
#splitResult.hasAmount {
border-color: #3c6b46;
box-shadow: 0 0 0 1px rgba(78, 201, 122, 0.08), 0 6px 20px rgba(0, 0, 0, 0.35);
}
#splitResultLabel {
color: #8f9d92;
font-size: 0.78rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
#splitResultAmount {
margin-top: 0.35rem;
font-size: clamp(2.6rem, 14vw, 3.4rem);
font-weight: 600;
line-height: 1.05;
color: #f2f2f2;
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
}
#splitResult.hasAmount #splitResultAmount {
color: #7ee2a0;
}
#splitOddNote {
margin-top: 0.7rem;
color: #d8b46a;
font-size: 0.85rem;
line-height: 1.45;
}
#splitDustNote {
margin-top: 0.5rem;
color: #e0a1a1;
font-size: 0.8rem;
line-height: 1.4;
}
/* ---------- Inputs ---------- */
.splitField {
display: block;
margin-bottom: 1.35rem;
}
/* A `display` declaration outranks the browser's own [hidden] rule, so anything
given one has to opt back out explicitly or toggling `hidden` does nothing. */
[hidden] {
display: none !important;
}
.splitLabel {
display: block;
margin-bottom: 0.5rem;
color: #9a9a9a;
font-size: 0.75rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.potInputWrap {
display: flex;
align-items: center;
gap: 0.4rem;
background: #141414;
border: 1px solid #3a3a3a;
border-radius: 12px;
padding: 0.2rem 0.75rem;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.potInputWrap:focus-within {
border-color: #d8b46a;
box-shadow: 0 0 0 3px rgba(216, 180, 106, 0.12);
}
.potCurrency {
color: #777;
font-size: 1.6rem;
font-weight: 300;
}
#potInput {
flex: 1;
min-width: 0;
background: none;
border: none;
outline: none;
color: #f2f2f2;
font-size: 2rem;
font-weight: 500;
font-variant-numeric: tabular-nums;
padding: 0.55rem 0;
font-family: inherit;
}
#potInput::placeholder {
color: #4a4a4a;
}
.clearPotBtn {
flex: none;
width: 34px;
height: 34px;
border-radius: 50%;
border: none;
background: #262626;
color: #999;
font-size: 0.85rem;
cursor: pointer;
}
@media (hover: hover) {
.clearPotBtn:hover {
background: #333;
color: #ddd;
}
}
/* ---------- Tap targets ---------- */
.chipRow {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.choiceChip {
flex: 1 1 auto;
min-width: 58px;
min-height: 52px;
padding: 0.5rem 0.7rem;
background: linear-gradient(180deg, #2e2e2e, #232323);
border: 1px solid #3a3a3a;
border-radius: 11px;
color: #ddd;
font-size: 1rem;
font-family: inherit;
cursor: pointer;
transition: all 0.15s ease;
}
@media (hover: hover) {
.choiceChip:hover {
border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a);
}
}
.choiceChip.selected {
background: linear-gradient(180deg, #3a2f18, #241d0f);
border-color: #d8b46a;
color: #f0d9a8;
font-weight: 600;
}
.waysStepper {
display: flex;
align-items: center;
gap: 0.35rem;
flex: 1 1 auto;
min-width: 150px;
background: #191919;
border: 1px solid #333;
border-radius: 11px;
padding: 0.2rem 0.4rem;
}
.stepBtn {
width: 40px;
height: 44px;
border: none;
border-radius: 8px;
background: #262626;
color: #ddd;
font-size: 1.25rem;
line-height: 1;
font-family: inherit;
cursor: pointer;
}
@media (hover: hover) {
.stepBtn:hover {
background: #333;
}
}
.waysReadout {
flex: 1;
text-align: center;
color: #999;
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
}
/* ---------- Footer actions ---------- */
#splitActions {
display: flex;
gap: 0.6rem;
margin-top: 1.75rem;
}
.splitBtn {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 48px;
padding: 0.6rem 0.9rem;
border-radius: 10px;
border: 1px solid #3a3a3a;
background: linear-gradient(180deg, #2e2e2e, #232323);
color: #ddd;
font-size: 0.9rem;
font-family: inherit;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
}
@media (hover: hover) {
.splitBtn:hover {
border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a);
}
}
.splitBtnGhost {
background: transparent;
}
.splitFootnote {
margin-top: 1.5rem;
color: #6f6f6f;
font-size: 0.78rem;
line-height: 1.5;
text-align: center;
}
/* This footnote names a control the dealer is being told to reach for, so the name
itself has to be readable — it was 3.39:1 at its worst, inheriting the muted grey
the surrounding sentence is deliberately set in. Emphasised text carrying the
instruction gets its own colour rather than the aside's. */
.splitFootnote strong {
color: #a8a8a8;
font-weight: 600;
}
.splitFootnote a {
color: #a9782f;
}
/* Roomier once there's a real screen to work with. */
@media (min-width: 700px) {
#splitApp {
padding-top: 2.5rem;
}
#splitHeader h1 {
font-size: 1.8rem;
}
}