:root {
  --card-w: 84px;
  --card-h: 122px;
  /* How much of a tableau card peeks out before the next one covers it -
     split by the covered card's own face, since a back only needs to show
     its top border plus a sliver of the pattern, while a face needs enough
     to read the corner's rank + the top of its suit pip. */
  --cascade-down: 9px;
  --cascade-up: 24px;
  --pile-gap: 14px;
  /* The tableau's own natural width (7 columns + 6 gaps) - #top-row and
     #tableau are both pinned to exactly this width (see their rules
     below) so the foundations land flush with the tableau's right edge
     instead of drifting with #board's fluid width. Recomputes correctly
     at the mobile breakpoint since --card-w changes there too. */
  --tableau-width: calc(7 * var(--card-w) + 6 * var(--pile-gap));
  --felt: #0b5d3b;
  --felt-dark: #084a2f;
  --gold: #d9b45c;
  --ease-out-smooth: cubic-bezier(0.16, 1, 0.3, 1);
  /* For transforms that start and end at rest (the card-flip rotation),
     unlike ease-out-smooth which is for travel that lands somewhere -
     eases into the turn instead of starting it at full speed. */
  --ease-in-out-smooth: cubic-bezier(0.45, 0, 0.15, 1);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  /* 100% alone can compute shorter than the true visual viewport in iOS
     standalone (home-screen) mode specifically - a documented mismatch
     between the layout viewport percentage-height math resolves against
     and the actual safe-area-inclusive rendered surface. The gap between
     the two shows as a strip of iOS's own default white, not this page's
     felt, wherever html/body's box ends short of the real bottom edge.
     100dvh (dynamic viewport height) is the unit built specifically to
     close that gap - falls back to the 100% above in any browser that
     doesn't recognize it, since an invalid value is ignored, not applied. */
  height: 100dvh;
  /* Confirmed on a real device that 100dvh alone wasn't enough in this
     app's standalone mode specifically - --app-vh (see the inline script
     in index.html's <head>) is window.innerHeight itself, the same value
     getTableauAvailableHeight() in script.js already relies on
     successfully for toolbar-avoidance, so it's already proven accurate in
     this exact runtime rather than a second guess at a CSS unit. Falls
     back to the 100dvh above until that inline script runs (effectively
     immediately, and before first paint). */
  height: var(--app-vh, 100dvh);
  background: radial-gradient(ellipse at top, var(--felt), var(--felt-dark));
  color: #eee;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  /* Was overflow-x: hidden only - the page never legitimately scrolls
     vertically either, outside the one deliberate exception just below, so
     there's nothing overscroll-behavior: none should ever need to chain
     past. But iOS Safari's elastic/rubber-band bounce isn't reliably
     stopped by overscroll-behavior alone: a touch that starts on empty
     felt (not a card - the drag system's own preventDefault only covers
     card pointerdowns) can still bounce the whole document a few px, which
     is exactly enough to expose real iOS white beyond html/body's own
     background at the top or bottom. overflow: hidden removes the
     scrollable range that bounce needs in the first place, at the CSS
     layer rather than trying to catch every gesture in JS - confirmed
     against user reports of a white bar appearing mid-play from what felt
     like an accidental drag, not just at standalone launch. */
  overflow: hidden;
  user-select: none;
  -webkit-user-select: none;
  overscroll-behavior: none;
}

/* Only while a tableau column is in its temporary expanded/inspection
   state (see toggleColumnExpanded, which toggles this class on both <html>
   and <body>) - height:100% caps the document and clips anything past it
   from ever being scrollable, so this swaps to min-height for just that
   window, letting the page grow enough to scroll to an expanded column's
   overflowing bottom card. Reverts to the normal fixed-viewport, no-scroll
   layout the instant it's collapsed - never a permanent change to page
   height.
   Both elements need this, not just body: document.scrollingElement is
   <html> (confirmed - this is the element whose own height actually
   governs how far the page can scroll), so leaving it pinned to height:100%
   while only body grows underneath it meant content could overflow body
   without ever becoming reachable by scrolling at all - the "scroll down
   to see the rest" promise this class exists for didn't actually hold once
   an expanded column needed more room than min-height:100% alone (i.e. the
   viewport) could show for it. */
html.tableau-inspecting,
body.tableau-inspecting {
  height: auto;
  min-height: 100%;
  /* Same iOS-standalone mismatch the base html/body rule above addresses -
     min-height needs the same floor, or an expanded column in standalone
     mode could stop just short of the true bottom edge. */
  min-height: 100dvh;
  min-height: var(--app-vh, 100dvh);
  /* The one deliberate exception to the base rule's overflow: hidden -
     this is the single case where the document is actually meant to
     scroll (down to an expanded column's overflowing bottom card). */
  overflow-y: auto;
}

/* Standalone iOS home-screen mode (viewport-fit=cover in index.html) lets
   the page draw into the notch/status-bar/home-indicator area instead of
   iOS letterboxing it in white - html's own full-bleed background now
   shows through there instead. This pushes the real, interactive content
   back in from the physical edges by exactly the safe-area amount so
   nothing sits under the notch or rounded corners. Falls back to 0 on
   any browser without safe-area support (env() with no fallback would be
   invalid, not just ignored). */
body {
  padding: env(safe-area-inset-top, 0) env(safe-area-inset-right, 0) env(safe-area-inset-bottom, 0) env(safe-area-inset-left, 0);
}

#update-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 8px 16px;
  background: var(--gold);
  color: #241c05;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
}
#update-bar.hidden { display: none; }

#update-bar button {
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-weight: 700;
}

#updateReloadBtn {
  background: #241c05;
  color: var(--gold);
  padding: 4px 12px;
  font-size: 12px;
}
#updateReloadBtn:hover { background: #382c0c; }

#updateDismissBtn {
  background: transparent;
  color: #241c05;
  padding: 2px 6px;
  font-size: 16px;
  line-height: 1;
  opacity: 0.65;
}
#updateDismissBtn:hover { opacity: 1; }

#topbar {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 10px 20px;
  background: rgba(0,0,0,0.25);
  border-bottom: 1px solid rgba(255,255,255,0.08);
  flex-wrap: wrap;
}

#topbar h1 {
  font-size: 18px;
  font-weight: 600;
  margin: 0;
  letter-spacing: 0.04em;
  color: var(--gold);
  margin-right: auto;
}

.stats {
  display: flex;
  gap: 16px;
  font-size: 13px;
  color: #cde;
  font-variant-numeric: tabular-nums;
}

.controls {
  display: flex;
  gap: 8px;
}

.controls button {
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.18);
  color: #eee;
  padding: 6px 12px;
  border-radius: 6px;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s;
}
.controls button:hover { background: rgba(255,255,255,0.18); }
.controls button:disabled { opacity: 0.4; cursor: default; }

#autoFinishBtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
#autoFinishBtn svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
}

/* Auto Finish's one-shot start flash - gold, gone in a single pass rather
   than looping, unlike .hint-highlight's continuous pulse below. */
@keyframes auto-finish-flash {
  from { background: rgba(217, 180, 92, 0.55); }
  to { background: rgba(255, 255, 255, 0.08); }
}
#autoFinishBtn.flash {
  animation: auto-finish-flash 260ms ease-out;
}

/* Auto Finish's persistent glow - on any time the button is clickable,
   whether or not a run has actually started yet (see script.js's
   updateAutoFinishReady). It's meant to read as an invitation ("there's a
   free win here, tap me") the moment the board makes Auto Finish possible,
   not just as a running-indicator once tapped - so the same look carries
   straight through an actual run rather than turning on only then. .flash
   still plays its own one-shot pulse on top of this at the exact instant a
   run starts, so starting still reads as a distinct event. A vivid,
   glowing chartreuse rather than the app's usual gold, so this reads
   unmistakably differently from every other toolbar state at a glance. */
#autoFinishBtn.ready {
  background: rgba(198, 255, 61, 0.12);
  border-color: #c6ff3d;
  color: #c6ff3d;
  animation: auto-finish-ready-glow 1.4s ease-in-out infinite;
}
#autoFinishBtn.ready:hover {
  background: rgba(198, 255, 61, 0.18);
}
@keyframes auto-finish-ready-glow {
  0%, 100% { box-shadow: 0 0 6px 1px rgba(198, 255, 61, 0.25), 0 0 16px 3px rgba(198, 255, 61, 0.15); }
  50% { box-shadow: 0 0 10px 2px rgba(198, 255, 61, 0.43), 0 0 26px 7px rgba(198, 255, 61, 0.28); }
}
@media (prefers-reduced-motion: reduce) {
  #autoFinishBtn.ready {
    animation: none;
    box-shadow: 0 0 8px 1px rgba(198, 255, 61, 0.33), 0 0 20px 4px rgba(198, 255, 61, 0.2);
  }
}

/* .running: actively mid-run, not just available - a visibly stronger
   version of .ready's glow (solid-er fill, brighter border/text, wider
   glow) rather than a different color, so the two states read as "more
   of the same thing" instead of unrelated. Still just a class alongside
   .ready (not a replacement for it - see startAutoFinish/stopAutoFinish),
   so this layers on top in every toolbar context (top row or any bottom
   toolbar) without needing its own per-layout rules. */
#autoFinishBtn.running {
  background: rgba(198, 255, 61, 0.24);
  border-color: #d9ff70;
  color: #eaffb8;
  animation: auto-finish-running-glow 1.1s ease-in-out infinite;
}
@keyframes auto-finish-running-glow {
  0%, 100% { box-shadow: 0 0 8px 2px rgba(198, 255, 61, 0.45), 0 0 22px 5px rgba(198, 255, 61, 0.3); }
  50% { box-shadow: 0 0 14px 3px rgba(198, 255, 61, 0.75), 0 0 34px 10px rgba(198, 255, 61, 0.5); }
}
@media (prefers-reduced-motion: reduce) {
  #autoFinishBtn.running {
    animation: none;
    box-shadow: 0 0 12px 2px rgba(198, 255, 61, 0.6), 0 0 26px 6px rgba(198, 255, 61, 0.4);
  }
}

.controls button.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px 10px;
  font-size: 16px;
  line-height: 1;
}

/* Undo/Hint's SVG icons - text-glyph icon-buttons (Settings' gear) are
   sized via font-size above and don't match this selector. */
.controls button.icon-button svg {
  display: block;
  width: 18px;
  height: 18px;
}

/* --new: added for the iPhone-portrait bottom toolbar (see the
   max-width:480px/orientation:portrait block near the end of this file) -
   hidden everywhere else so desktop/tablet/landscape stay exactly as they
   were: Undo/Hint/Settings icon-only, New Game/Restart text-only. */
.btn-label--new, .btn-icon--new { display: none; }

/* Help mode's "on" state - a solid gold fill (not just a color swap on the
   same transparent background), so it reads as a real pressed/filled state
   at a glance rather than depending on color alone; aria-pressed and the
   title/aria-label swap in script.js carry the same information for anyone
   not relying on sight. An ID selector so this reliably wins over every
   breakpoint's own `.controls button`/`.icon-button` rules below regardless
   of source order - deliberately no animation, unlike Auto Finish's glow:
   Help is a restrained, persistent mode indicator, not an invitation. */
#helpBtn.active {
  background: var(--gold);
  border-color: var(--gold);
  color: #041209;
}
#helpBtn.active:hover { background: #e8c473; }

#hint-message {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 8px 16px;
  background: rgba(217, 180, 92, 0.14);
  border-bottom: 1px solid rgba(217, 180, 92, 0.4);
  color: var(--gold);
  font-size: 13px;
  font-weight: 600;
  text-align: center;
}
#hint-message.hidden { display: none; }

/* A hint's card labels render as a small chip - light background matching
   the actual card-face color (#ded9ca, same as .card.face-up), rank and
   suit set tight against each other with no gap, so it reads like a
   miniature card rather than plain sentence text. The chip's own dark text
   color is what makes true black (#231f20, the exact ink color requested)
   legible here at all - against #hint-message's own dark background it
   nearly vanished, which is exactly why the chip exists. */
.hint-card {
  display: inline-flex;
  align-items: center;
  background: #ded9ca;
  color: #231f20;
  border-radius: 4px;
  padding: 1px 6px;
  margin: 0 1px;
  font-weight: 700;
  line-height: 1.4;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
.hint-card .suit-red { color: #e07860; }
.hint-card .suit-black { color: #231f20; }

/* ==========================================================================
   Card annotation - the ONE shared presentation for all four Automatic Tip
   categories (see showCardAnnotation()/triggerAutoTip() in script.js and
   TIP_CATALOG in autoTips.js). A floating, non-modal graphic callout
   anchored near the specific card/column that triggered it -
   "Pop-Up Video"-spirited (personality + spatial pointing), never a literal
   copy of that show's own graphics/type. Approved 2026-09-05 (was compared
   against two other prototype treatments; this is the only one left).
   `position: fixed` and zero effect on layout: its place in index.html is
   irrelevant to where it renders, and it can never push the board down or
   reserve space.
   ========================================================================== */
#card-annotation {
  position: fixed;
  z-index: 900; /* above ordinary card stacking (small idx-based z-indexes), below an active drag ghost (1000+) */
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: min(78vw, 260px);
  pointer-events: none; /* never blocks a card/drag underneath, even if positioning slightly overlaps one */
  /* Shared fill/outline/text tokens every category's annotation reads (via
     getComputedStyle in script.js) - one place to change the look later,
     never duplicated per category. Defined on the nearest common ancestor
     since custom properties only inherit downward. */
  --tip-fill: #041209; /* deliberately darker than --felt/--felt-dark (#0b5d3b/#084a2f) so the fill always reads as solid against the table, not just whichever card happens to be underneath */
  --tip-outline: var(--gold);
  --tip-text: #f1ead9;
}
#card-annotation.hidden { display: none; }
#card-annotation-shape {
  position: relative;
  text-align: center;
  transform: rotate(var(--rot, 0deg)); /* randomized -2..2deg per trigger (see showCardAnnotation) - a static tilt, kept even under reduced motion since it's a look, not an animation */
}
#card-annotation-fill {
  position: relative; /* stacks above the SVG background via DOM order */
  color: var(--tip-text);
  padding: 16px 20px;
}
/* Reserves room for the pointer on whichever edge it's actually on (see
   showCardAnnotation() for which class gets applied) - ANNOTATION_POINTER_H
   in script.js must match this value; the two are kept in sync by both
   pointing at the same comment, not by sharing a literal constant, since
   CSS can't read a JS value or vice versa. */
#card-annotation.anchor-below #card-annotation-fill { padding-top: 42px; }
#card-annotation.anchor-above #card-annotation-fill { padding-bottom: 42px; }
#card-annotation-headline {
  margin: 0 0 3px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: 18px;
  font-weight: 800;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  line-height: 1.15;
}
#card-annotation-headline svg {
  width: 15px;
  height: 15px;
  flex: none;
  color: var(--gold); /* stroke="currentColor" - a gold icon, restrained, not colored like an emoji */
}
#card-annotation-text {
  margin: 0;
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.35;
}
#card-annotation-disclosure {
  margin: 6px 0 0;
  font-size: 10px;
  font-weight: 500;
  font-style: italic;
  opacity: 0.72;
}
#card-annotation-disclosure.hidden { display: none; }

/* Contextual Help's quieter presentation - same component, same fill/
   outline/type family/pointer construction, just a smaller footprint
   (~20-30% down) with tighter padding/line-height: Help is player-
   requested and likely tapped through repeatedly while exploring the
   board, so it shouldn't carry the same visual weight as a volunteered
   Automatic Tip. Applied via showCardAnnotation's `size: 'help'` param
   (script.js) - Automatic Tips never carry this class. Padding-top/bottom
   below must match HELP_ANNOTATION_POINTER_H in script.js (12px base + 20
   = 32px) - same "kept in sync by hand" convention as the un-prefixed
   rules above. Stroke width is deliberately untouched - "same gold
   outline" per the brief. */
#card-annotation.size-help { max-width: min(58vw, 190px); }
#card-annotation.size-help #card-annotation-fill { padding: 12px 14px; }
#card-annotation.size-help.anchor-below #card-annotation-fill { padding-top: 32px; }
#card-annotation.size-help.anchor-above #card-annotation-fill { padding-bottom: 32px; }
#card-annotation.size-help #card-annotation-headline { margin-bottom: 2px; font-size: 14px; }
#card-annotation.size-help #card-annotation-text { font-size: 12px; line-height: 1.25; }

/* A quick, restrained "pop" - the shape's own static rotation (above) is
   untouched by this; only scale+fade animate. Reduced-motion gets the
   final state immediately, never a delayed reveal of the same information. */
#card-annotation.pop-in { animation: card-annotation-pop 200ms cubic-bezier(.34, 1.56, .64, 1) both; }
@keyframes card-annotation-pop {
  0%   { opacity: 0; transform: scale(0.7); }
  60%  { opacity: 1; transform: scale(1.05); }
  100% { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  #card-annotation.pop-in { animation: none; opacity: 1; }
}

/* The rounded-rectangle-plus-pointer background, as ONE continuous SVG path
   (see buildAnnotationPointerPath() in script.js) - fill and stroke are
   both properties of that single path, so the pointer/body join is
   mathematically the same line, not two shapes meeting. Sized and
   positioned in real script.js-computed pixels every time a tip shows
   (never a percentage/viewBox stretch), so it can never distort regardless
   of text length or viewport. */
#card-annotation-svg {
  position: absolute;
  pointer-events: none;
  overflow: visible; /* the stroke intentionally bleeds slightly past the nominal w/h - see script.js */
}

#board {
  padding: 24px 28px 60px;
  max-width: 900px;
  margin: 0 auto;
}

#top-row {
  position: relative; /* anchors #top-row-watermark below */
  display: flex;
  justify-content: space-between;
  width: var(--tableau-width);
  margin-inline: auto;
  margin-bottom: 34px;
}

/* Decorative-only: the open gap #top-row's own space-between leaves
   between stock/waste and the foundations, at whatever size that gap
   happens to be at the current breakpoint. Taken out of flex flow with
   position: absolute so it can never become a third flex item and widen
   that gap or shift the piles - it consumes zero layout space, same as if
   it weren't there at all.

   The left/width formulas below are pure algebra on #top-row's own
   existing box model, not tuned/guessed pixel values: #top-row is exactly
   7 card-widths + 6 pile-gaps wide (same as the tableau below it);
   stock-waste uses 2 card-widths + 1 gap, foundations use 4 card-widths +
   3 gaps, so what's left for the middle gap is always exactly
   1 card-width + 2 pile-gaps, centered at 2.5 card-widths + 2 pile-gaps
   from the left edge - regardless of which breakpoint's --card-w/
   --pile-gap are currently in effect.

   Tweak these three custom properties directly - nothing else needs to
   change to resize, dim, or nudge the mark:
     --watermark-width    size (proportional to the gap, so it already
                           scales with the board - override to resize)
     --watermark-opacity  visibility - halve it for "half as visible"
     --watermark-y        vertical nudge from dead-center, in px */
#top-row-watermark {
  --watermark-width: calc((var(--card-w) + 2 * var(--pile-gap)) * 0.52);
  --watermark-opacity: 0.26;
  --watermark-y: 0px;
  /* Matches the real mark's own cropped proportions (145:207), so
     mask-size: contain doesn't have to guess. */
  --watermark-aspect: 145 / 207;

  position: absolute;
  z-index: 0;
  pointer-events: none;
  left: calc(2.5 * var(--card-w) + 2 * var(--pile-gap));
  top: calc(50% + var(--watermark-y));
  transform: translate(-50%, -50%);

  width: var(--watermark-width);
  aspect-ratio: var(--watermark-aspect);
  opacity: var(--watermark-opacity);

  /* Black composited at low opacity directly over the felt - the result
     reads as a darker shade of the same green, not literal black, since
     the felt shows through. --watermark-opacity above is the single knob
     for how dark it reads; no separate color-mix step, so there's no
     double-attenuation between a "how dark" color choice and a separate
     "how visible" opacity choice. */
  background-color: black;
  -webkit-mask-image: url('assets/watermark-logo.png');
  mask-image: url('assets/watermark-logo.png');
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

/* Below this, the middle gap itself has shrunk enough that the mark can
   no longer sit in it cleanly without crowding the waste or first
   foundation - hide rather than risk overlapping a playable card. */
@media (max-width: 420px) {
  #top-row-watermark { display: none; }
}

#stock-waste, #foundations, #tableau {
  display: flex;
  gap: var(--pile-gap);
}

.pile {
  position: relative;
  width: var(--card-w);
  height: var(--card-h);
  border-radius: 8px;
  border: 1.5px solid rgba(255,255,255,0.18);
  background: rgba(0,0,0,0.12);
  scale: 1;
  transition: filter 150ms ease-out, box-shadow 150ms ease-out, scale 150ms ease-out;
  touch-action: none; /* the pile itself is a tap target (stock) or drop target under an active drag */
}

.pile.drop-target-active {
  filter: brightness(1.18);
  box-shadow: 0 0 0 2px rgba(217, 180, 92, 0.55), 0 0 22px 4px rgba(217, 180, 92, 0.32);
  scale: 1.01;
}

/* Hint's source/destination highlight - reuses the same gold used for
   drop-target-active above, animated into a slow pulse so it reads as "look
   here" rather than a static selection state. Applies to both .card and
   .pile elements (a hinted stock draw highlights the pile, not a card). */
@keyframes hint-pulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(217, 180, 92, 0.55), 0 0 14px 3px rgba(217, 180, 92, 0.28); }
  50% { box-shadow: 0 0 0 3px rgba(217, 180, 92, 0.95), 0 0 26px 8px rgba(217, 180, 92, 0.55); }
}
.hint-highlight {
  animation: hint-pulse 1.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .hint-highlight {
    animation: none;
    box-shadow: 0 0 0 2px rgba(217, 180, 92, 0.85), 0 0 18px 4px rgba(217, 180, 92, 0.4);
  }
}

.foundation::before {
  content: attr(data-placeholder);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 40px;
  color: rgba(255,255,255,0.12);
  pointer-events: none;
}

.column {
  min-height: 420px;
  border: none;
  background: rgba(0,0,0,0.10);
  border-radius: 8px;
}

/* Temporary inspection state (see toggleColumnExpanded) - a subtle glow,
   no modal or message per design. Raised above sibling columns so an
   overflowing expanded cascade - allowed to extend past the viewport
   while inspecting it - never renders under a neighboring column. */
.column.expanded {
  z-index: 50;
  box-shadow: 0 0 0 2px rgba(217, 180, 92, 0.5), 0 0 16px 3px rgba(217, 180, 92, 0.3);
}

#tableau {
  align-items: flex-start;
  width: var(--tableau-width);
  margin-inline: auto;
}

/* A compressed column's expand/collapse handle - deliberately its own
   small element rather than a click-area carved out of the column
   background, since in heavy compression a card's own full-size box
   already covers nearly the whole visual stack (see createExpandToggle).
   Top-right, clear of the corner rank/suit index every card in this deck
   shows top-left; a z-index above any card (however many are in the
   column) so it's always reachable. */
.tableau-expand-toggle {
  position: absolute;
  top: 3px;
  right: 3px;
  z-index: 700;
  width: 20px;
  height: 20px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(217, 180, 92, 0.55);
  background: rgba(0, 0, 0, 0.45);
  color: var(--gold);
  font-size: 10px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.15s, transform 0.15s;
}
.tableau-expand-toggle:hover { background: rgba(0, 0, 0, 0.7); transform: scale(1.08); }
.tableau-expand-toggle:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }
/* Invisibly extends the clickable area well past the visible 20px circle -
   a pseudo-element's rendered box still counts as its host button's hit
   area, so this makes a near-miss still register without growing what's
   actually drawn. */
.tableau-expand-toggle::before {
  content: '';
  position: absolute;
  inset: -10px;
}

.card {
  position: absolute;
  top: 0; left: 0;
  width: var(--card-w);
  height: var(--card-h);
  border-radius: 8px;
  box-shadow: 0 2px 5px rgba(0,0,0,0.4);
  cursor: grab;
  /* Without this, iOS Safari can claim a press-and-move as a page scroll
     before our pointermove handler ever gets a chance to preventDefault(). */
  touch-action: none;
  -webkit-touch-callout: none; /* suppress the long-press save/copy menu on the card image */
}

/* A click/tap that can't become a move (see bounceCard in script.js) - a
   quick, small settle-and-recoil rather than a shake, so it reads as "got
   it, nothing to do" rather than "error". Individual scale/translate
   properties (not the transform shorthand) match .pile's own convention
   just above, and stay clear of top/left, which is how tableau cards are
   actually positioned. */
@keyframes card-touch-bounce {
  0% { scale: 1; translate: 0 0; }
  35% { scale: 0.96; translate: 0 3px; }
  65% { scale: 1.02; translate: 0 -1.5px; }
  100% { scale: 1; translate: 0 0; }
}
.card.touch-bounce, .pile.touch-bounce { animation: card-touch-bounce 260ms ease-out; }
@media (prefers-reduced-motion: reduce) {
  .card.touch-bounce, .pile.touch-bounce { animation: none; }
}

/* The King-cascade press-and-hold's own feedback (see triggerKingCascade's
   pointerdown handler in script.js) - the empty column gradually lights up
   over the hold, so an accidental brush never completes it (releasing or
   moving away early just lets .pile's own base 150ms transition ease this
   back off) and a deliberate hold has a clear "something is about to
   happen" cue before anything actually moves. The transition duration
   below must match KING_CASCADE_HOLD_MS in script.js, so the glow finishes
   building at the exact instant the hold completes - same convention as
   FLIP_MS/.flip-inner staying in sync elsewhere in this file. Reuses the
   same gold as .hint-highlight/.drop-target-active, since it means the
   same thing: look here. */
.pile.cascade-charging {
  transition: filter 450ms ease-out, box-shadow 450ms ease-out, scale 450ms ease-out;
  filter: brightness(1.22);
  box-shadow: 0 0 0 2px rgba(217, 180, 92, 0.9), 0 0 26px 6px rgba(217, 180, 92, 0.55);
  scale: 1.015;
}

.card img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 8px;
  pointer-events: none;
  -webkit-user-drag: none;
}

.card.face-up {
  /* Matches the card PNGs' own background exactly - these assets already
     have a rounded corner baked in with transparency outside it, at a
     radius that doesn't line up pixel-for-pixel with this element's own
     8px CSS border-radius. Rather than chase an exact radius match, this
     fills the gap between the two curves with the same color the art
     itself uses, so any seam is invisible instead of reading as a mismatched
     white corner. */
  background: #ded9ca;
  border: 1px solid rgba(0,0,0,0.15);
}

.card.face-down {
  /* Every back art shipped before this stayed fully opaque, so this was
     never needed - but several of the current clean-condition back
     designs (their worn counterparts are all opaque) leave large areas
     transparent by design, the same way Clean face art does, relying on
     this fill rather than baking in their own background. Same #ded9ca
     .card.face-up already uses, for the same reason: one consistent
     card-colored ground regardless of how much of its own background a
     given back design fills in. */
  background: #ded9ca;
  border: 1px solid rgba(0,0,0,0.25);
  cursor: default;
}

.card.not-draggable { cursor: default; }

/* Drag ghost: a positioning wrapper (instant, untransitioned — tracks the
   cursor exactly) containing a visual layer (lift/scale/rotate/shadow,
   transitioned independently). Keeping these on separate elements is what
   lets the card follow the pointer with zero lag while the "lifted" feel
   still eases in and out smoothly. */
.drag-ghost {
  position: fixed;
  z-index: 1000;
  pointer-events: none;
  cursor: grabbing;
}

.drag-visual {
  position: relative;
  width: 100%;
  height: 100%;
  translate: 0 0;
  scale: 1;
  rotate: 0deg;
  transition:
    translate 120ms var(--ease-out-smooth),
    scale 120ms var(--ease-out-smooth),
    rotate 90ms ease-out;
}

.drag-visual::after {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: 10px;
  box-shadow: 0 22px 40px rgba(0,0,0,0.5), 0 8px 16px rgba(0,0,0,0.35);
  opacity: 0;
  transition: opacity 120ms var(--ease-out-smooth);
}

.drag-visual.lifted {
  translate: 0 -7px;
  scale: 1.02;
}

.drag-visual.lifted::after {
  opacity: 1;
}

/* Waste-pile draw transition: a plain, no-flip positioning ghost for
   cards that are already face up and just gathering into the pile or
   sliding to a new fanned slot (see animateWasteDraw) - simpler than
   .flip-ghost since there's no turn involved, just a translate. */
.gather-ghost {
  position: fixed;
  pointer-events: none;
  translate: 0 0;
}
.gather-ghost img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 8px;
  box-shadow: 0 2px 5px rgba(0,0,0,0.4);
}

/* Dealing from the stock: a whole dealt packet flips face-up together
   while held at the stock (see animateDraw's Phase 1), then each card
   glides out to its fanned waste slot (Phase 2). The wrapper's translate
   transition is set inline per-card for Phase 2, since its duration
   differs from the flip's - no default transition here. Two faces on
   the inner flip, each hidden when facing away, so the turn reveals the
   real front partway through rather than swapping instantly. */
.flip-ghost {
  position: fixed;
  z-index: 1000;
  pointer-events: none;
  perspective: 800px;
  translate: 0 0;
}

.flip-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 260ms var(--ease-in-out-smooth);
}

.flip-inner.flipped {
  transform: rotateY(180deg);
}

.flip-face {
  /* Same fill as .card.face-up/.card.face-down, for the same reason -
     shared by both sides of this element (the back art before the flip,
     the face art after), either of which can now be a design that leaves
     part of its own background transparent. */
  background: #ded9ca;
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 5px rgba(0,0,0,0.4);
}

.flip-face img {
  display: block;
  width: 100%;
  height: 100%;
}

.flip-face.flip-back {
  transform: rotateY(180deg);
}

.empty-hint {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255,255,255,0.35);
  font-size: 12px;
  pointer-events: none;
}

#drag-layer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 1000;
}

#settings-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2000;
}
#settings-overlay.hidden { display: none; }

#settings-card {
  background: #133;
  border: 1px solid var(--gold);
  border-radius: 12px;
  padding: 0 28px 28px; /* top padding lives on #settings-header itself - see its own comment */
  width: min(90vw, 380px);
  max-height: 85vh;
  overflow-y: auto;
}

/* Sticky within the card's own scroll area, so the close button stays
   reachable no matter how far a short viewport (e.g. iPhone landscape)
   needs to scroll to reach Stats/Feedback - see the settings-modal short-
   viewport fix this was added for. Deliberately no margin/negative-margin
   trick to reach the card's own edges: an earlier version used
   `margin: -24px -28px 12px` to escape #settings-card's padding, but
   position: sticky combined with a negative top margin measurably
   miscalculates how much flow-space this reserves in WebKit - confirmed
   on-device (iPad, both orientations) as a ~12px overlap where the very
   next element (Stats' Draw 1/Draw 3 toggle) rendered up under the
   header's own box. Removing #settings-card's own top padding and letting
   this handle its own top inset via padding-top instead sidesteps that
   entirely - no negative margins, no cross-element math, just a normal
   sticky box with normal padding. Its background doesn't need to reach
   the card's outer edges either: everything that scrolls past it already
   sits inside the same 28px side inset this padding provides, so nothing
   would ever show through in that gutter regardless. */
#settings-header {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 24px;
  padding-bottom: 12px;
  background: #133;
}
#settings-header h2 {
  color: var(--gold);
  margin: 0;
  font-size: 18px;
}
#settingsCloseBtn, #statsCloseBtn, #testingCloseBtn {
  background: none;
  border: none;
  color: #eee;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.7;
  padding: 0;
}
#settingsCloseBtn:hover, #statsCloseBtn:hover, #testingCloseBtn:hover { opacity: 1; }

.settings-section + .settings-section {
  margin-top: 20px;
}
.settings-section-label {
  color: #cde;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 0 0 10px;
}

/* Short descriptive copy under a settings-section heading, for a section
   whose label alone isn't self-explanatory (e.g. Automatic Tips) - same
   body-copy tone as #support-card p, the app's existing "settings
   description text" convention. Optional per-section (see
   PREFERENCE_SECTIONS' `caption` field in script.js); most sections don't
   need one. */
.settings-section-caption {
  color: #9fb3ac;
  font-size: 13px;
  line-height: 1.4;
  margin: 0 0 10px;
}

.settings-options {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

/* Card Back: a single horizontal row instead of the default wrapping grid
   above - 13 designs would otherwise take three full rows. Plain native
   overflow-x scrolling, no JS carousel/snap machinery: momentum scrolling
   on iOS and trackpad/wheel horizontal scroll on desktop both come free
   from the browser. overflow-y stays clipped so a swipe here can only
   ever scroll this one row, never bleed into #settings-card's own
   vertical scroll (orthogonal-axis touch scrolling is standard browser
   behavior, not something this needs to implement). The extra block
   padding/negative margin is just enough room for .selected's own glow
   (box-shadow spreads ~9px) to render in full instead of being clipped
   flat by this container's own edge. */
.settings-options--scroll {
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  padding-block: 10px;
  margin-block: -10px;
  /* A light static fade at both edges - a polish on top of the primary,
     self-evident scroll cue: the row's width rarely divides evenly into
     whole cards, so the next design is already partially visible at the
     right edge on its own. Masks the container itself rather than
     overlaying a separate element, so it can never sit on top of a card
     or read as a button. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 16px, #000 calc(100% - 16px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 16px, #000 calc(100% - 16px), transparent);
}
/* Never shrinks below its own natural size when the row is full - flex's
   default min-width:auto would otherwise let options compress instead of
   overflowing, defeating the whole point of this variant. */
.settings-options--scroll .settings-option { flex-shrink: 0; }

.settings-option {
  width: 48px;
  height: 68px;
  padding: 0;
  border-radius: 6px;
  border: 2px solid transparent;
  background: rgba(255,255,255,0.08);
  cursor: pointer;
  overflow: hidden;
  transition: border-color 0.15s, transform 0.15s;
}
.settings-option:hover { transform: scale(1.05); }
.settings-option.selected {
  border-color: var(--gold);
  box-shadow: 0 0 8px 1px rgba(217, 180, 92, 0.5);
}

/* Direct child only - the Card Back swatch img sits right in the button,
   but Deal Style's fan cards sit two levels down (inside .settings-option-
   fan), and must keep their own fixed size from .settings-option-fan-card
   instead of stretching to fill the fan. A descendant selector here would
   match both and (at equal-ish specificity) fight that rule for width/
   height/object-fit - which is exactly what stretched each fan card to the
   fan's own box and made object-fit: cover crop its top and bottom. */
.settings-option > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Matches .card.face-up's own background (#ded9ca) - see that rule's
   comment. A Cards-section preview isn't a real .card element, so without
   this a collection whose art fills less of its own background (Clean
   leaves nearly all of it transparent) would show this dark settings
   panel through the card instead of the card-colored fill it gets
   everywhere else in the game. */
.settings-option-card-preview {
  background: #ded9ca;
}

.settings-option-swatch {
  width: 100%;
  height: 100%;
}

/* Deal Style's larger tiles: illustration + a visible text label below it,
   and (unlike the small Card Back swatches) a subtle border even when
   unselected, so the two unselected tiles still read as separate buttons
   rather than blending into the panel background. */
.settings-option--stack {
  width: 96px;
  height: 108px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border-color: rgba(255,255,255,0.18);
  background: rgba(255,255,255,0.05);
}
.settings-option--stack:hover { border-color: rgba(255,255,255,0.32); }
.settings-option--stack.selected { border-color: var(--gold); }

/* A plain text segmented control (Cards: Worn/New) - no art to preview,
   so unlike every other .settings-option variant this sizes to its label
   instead of a fixed image-shaped box. */
.settings-option--text {
  width: auto;
  height: auto;
  padding: 10px 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-color: rgba(255,255,255,0.18);
  background: rgba(255,255,255,0.05);
  font-size: 14px;
  font-weight: 600;
  color: #cde;
  letter-spacing: 0.03em;
}
.settings-option--text:hover { border-color: rgba(255,255,255,0.32); }
.settings-option--text.selected {
  border-color: var(--gold);
  color: var(--gold);
  box-shadow: 0 0 8px 1px rgba(217, 180, 92, 0.5);
}

.settings-option-label {
  font-size: 12px;
  color: #cde;
  font-weight: 500;
}
.settings-option--stack.selected .settings-option-label {
  color: var(--gold);
}

.settings-option-fan {
  position: relative;
  width: 46px;
  height: 52px;
}

.settings-option-fan-card {
  /* Same fill as .card.face-up/.card.face-down/.flip-face, for the same
     reason - this reuses the real, live-resolved back art (getCardBackSrc),
     which for several clean-condition designs leaves most of its own
     background transparent. Without this it shows the dark settings panel
     through the art instead of a card-colored ground. */
  background: #ded9ca;
  position: absolute;
  top: 0;
  left: calc(50% - 15px + var(--offset) * 10px);
  width: 30px;
  height: 44px;
  border-radius: 4px;
  object-fit: cover;
  box-shadow: 0 1px 3px rgba(0,0,0,0.4);
  rotate: calc(var(--offset) * 10deg);
  transform-origin: bottom center;
}

/* Feedback: a secondary settings action, not a gameplay preference - set
   off with its own divider (same rule the Deal Style/Card Back sections
   already get between each other) rather than living inside a
   .settings-section, and deliberately quieter than .settings-option
   (no gold selected-state, no hover scale-up) so it never reads as one
   more choice to make. */
#settings-secondary {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid rgba(255,255,255,0.12);
}

/* MIKE Games System (see mike-games-system/SYSTEM.md, Standard Settings
   Architecture) - the standard utility block (Support/Feedback/Reload
   App/Testing), same divider treatment as #settings-secondary just above
   so the two read as distinct groups (game-specific vs. framework-level)
   through spacing alone - no visible heading for either. */
#settings-utilities {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid rgba(255,255,255,0.12);
}

.settings-feedback-link {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border-radius: 6px;
  border: 1px solid rgba(255,255,255,0.14);
  background: rgba(255,255,255,0.05);
  color: #cde;
  font-size: 13px;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
  /* Every other row here is a <button>, which centers its text by default
     in the browser's own UA stylesheet - #feedbackLink is the one real <a>
     in this group and doesn't get that for free, so it needs to be made
     explicit here rather than left to an accident of which element type
     each row happens to use. */
  text-align: center;
}
.settings-feedback-link:hover {
  background: rgba(255,255,255,0.1);
  border-color: rgba(255,255,255,0.24);
}
.settings-feedback-link:active { background: rgba(255,255,255,0.14); }
.settings-feedback-link:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}
.settings-feedback-link svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  opacity: 0.8;
}
.settings-feedback-link-label { flex: 1; }
/* #installLink only - hidden until beforeinstallprompt actually fires
   (see install-prompt.js's onInstallAvailabilityChange in script.js),
   never a dead/disabled row on platforms that don't support it. */
#installLink.hidden { display: none; }
.settings-feedback-link-arrow {
  opacity: 0.6;
  font-size: 14px;
}

/* Support Mike's Games - Buy Me a Coffee's own brand yellow tint, so this
   row reads as a distinct, branded entry point rather than blending into
   the plain Stats/Backup/Restore rows above it. */
#supportLink {
  background: rgba(255, 221, 0, 0.14);
  border-color: rgba(255, 221, 0, 0.5);
  color: #ffdd00;
}
#supportLink:hover { background: rgba(255, 221, 0, 0.22); }
#supportLink .settings-feedback-link-arrow { opacity: 0.85; }

/* Backup/Restore's own transient outcome line - success reuses the same
   gold #hint-message already uses elsewhere for "worked, here's what
   happened" feedback; error is the one place in Settings that needs a
   genuinely different (red) tone, since "that file couldn't be restored"
   isn't a neutral status. */
#settings-status {
  margin: 10px 0 0;
  padding: 8px 10px;
  border-radius: 6px;
  font-size: 12.5px;
  line-height: 1.4;
  text-align: center;
}
#settings-status.hidden { display: none; }
.settings-status--success {
  background: rgba(217, 180, 92, 0.14);
  border: 1px solid rgba(217, 180, 92, 0.4);
  color: var(--gold);
}
.settings-status--error {
  background: rgba(217, 92, 92, 0.14);
  border: 1px solid rgba(217, 92, 92, 0.4);
  color: #e8a3a3;
}

/* MIKE Games System (see mike-games-system/SYSTEM.md, Standard Settings
   Architecture) - identifies the build, nothing else. Quieter than every
   row above it on purpose (smallest text, lowest-contrast color, no
   border/background at all) and a plain <p> in index.html now, not a
   button - there's no interactive treatment left to style here at all. */
.settings-version {
  display: block;
  width: 100%;
  margin-top: 14px;
  padding: 4px 0 0;
  color: rgba(255,255,255,0.32);
  font-size: 11px;
  text-align: center;
}

/* Stats: one level "into" Settings (see index.html/script.js) - same
   overlay/card shell as #settings-overlay/#settings-card, just its own ids
   since both exist in the DOM simultaneously. */
#stats-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2000;
}
#stats-overlay.hidden { display: none; }

#stats-card {
  background: #133;
  border: 1px solid var(--gold);
  border-radius: 12px;
  padding: 0 28px 28px; /* top padding lives on #settings-header itself - see its own comment */
  width: min(90vw, 380px);
  max-height: 85vh;
  overflow-y: auto;
}

/* Testing Tools (development only - see IS_LOCAL_DEV in script.js): same
   "one level into Settings" shell as #stats-overlay/#stats-card just
   above, own ids for the same reason (both can exist in the DOM at once,
   in a dev build). */
#testing-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2000;
}
#testing-overlay.hidden { display: none; }

#testing-card {
  background: #133;
  border: 1px solid var(--gold);
  border-radius: 12px;
  padding: 0 28px 28px;
  width: min(90vw, 380px);
  max-height: 85vh;
  overflow-y: auto;
}

/* Same zero-gap stacked-row look as #settings-secondary/#settings-utilities
   above (each .settings-feedback-link supplies its own border/background;
   see that class's own comment) - no separate spacing rule needed. */
#testing-rows { margin-top: 4px; }

#stats-mode-toggle {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}
/* .settings-option--stack sizes for a preview image above a label (see
   Deal Style in Settings); these are plain text buttons, so let them size
   to their row instead of that fixed tile height. */
#stats-mode-toggle .settings-option--stack {
  width: auto;
  height: auto;
  flex: 1;
  padding: 10px 0;
}

.stats-row + .stats-row {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid rgba(255,255,255,0.12);
}
.stats-row-label {
  color: #cde;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 0 0 4px;
}
.stats-row-value {
  color: var(--gold);
  font-size: 22px;
  font-weight: 700;
}

/* Deliberately no scrim, no boxed card - this sits directly on the visible
   green felt once the celebration's cards have cleared. The cards are the
   show; this is the after-credits, kept quiet by comparison. */
#win-message {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  text-align: center;
  padding: 24px;
}
#win-message.hidden { display: none; }

/* Emphasis hierarchy top to bottom: winEmoji (small accent) < winRecords
   (smaller still, quietest) < winResultLine (prominent) < winHeadline
   (largest/strongest) - see script.js's showVictoryMessage. */
#winEmoji {
  font-size: clamp(22px, 5vw, 30px);
  line-height: 1;
}

#winHeadline {
  margin: 0;
  color: var(--gold);
  font-size: clamp(40px, 10vw, 72px);
  font-weight: 800;
  letter-spacing: 0.02em;
  text-shadow: 0 3px 14px rgba(0,0,0,0.55), 0 1px 0 rgba(0,0,0,0.3);
}

#winResultLine {
  margin: 0;
  color: #f3ead2;
  font-size: clamp(18px, 4vw, 22px);
  font-weight: 600;
  text-shadow: 0 1px 8px rgba(0,0,0,0.45);
}

/* Same quiet tier as the two record lines below - it's a plain count, not
   a record, so it always shows (never gated by winNumber > 1 like those
   are) but never gets their trophy treatment either. Sits closer to
   winHeadline specifically than #win-message's own 18px flex gap gives
   every pair by default - a negative margin-top here pulls it toward the
   headline without touching the gap on either side of it (winEmoji-to-
   headline above, or this-to-winResultLine below stay at the full 18px). */
#winPlaysLine {
  margin: 0;
  margin-top: -9px;
  color: rgba(255,255,255,0.7);
  font-size: 14px;
  text-shadow: 0 1px 6px rgba(0,0,0,0.4);
}

/* The two record lines sit close together as one visual unit (tight own
   gap), separate from #win-message's own 18px gap around the whole group. */
#winRecords {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
#winRecords.hidden { display: none; }
#winRecordTime, #winRecordMoves {
  margin: 0;
  color: rgba(255,255,255,0.7);
  font-size: 14px;
  text-shadow: 0 1px 6px rgba(0,0,0,0.4);
}
#winRecordTime.win-record-trophy, #winRecordMoves.win-record-trophy {
  color: var(--gold);
  font-weight: 700;
}

#winNewGameBtn {
  background: var(--gold);
  border: none;
  color: #1a2e22;
  font-weight: 700;
  font-size: 16px;
  padding: 12px 28px;
  border-radius: 8px;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0,0,0,0.35);
  transition: filter 0.15s, transform 0.15s;
}
#winNewGameBtn:hover { filter: brightness(1.08); transform: translateY(-1px); }
#winNewGameBtn:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

/* Win-message entrances: five subtle, quick, single-shot treatments -
   script.js picks one per win (see victory.js's messageEntrance) and adds
   the matching class to #win-message. All five land at the same at-rest
   state (opacity:1, transform:none), just via a different path in. */
@keyframes win-enter-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes win-enter-scale-up {
  from { opacity: 0; transform: scale(0.85); }
  to { opacity: 1; transform: scale(1); }
}
@keyframes win-enter-drop-settle {
  from { opacity: 0; transform: translateY(-28px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes win-enter-bounce {
  0% { opacity: 0; transform: scale(0.7); }
  60% { opacity: 1; transform: scale(1.06); }
  100% { opacity: 1; transform: scale(1); }
}
@keyframes win-enter-rotate-straighten {
  from { opacity: 0; transform: rotate(-6deg) scale(0.92); }
  to { opacity: 1; transform: rotate(0deg) scale(1); }
}
.win-enter-fade { animation: win-enter-fade 420ms var(--ease-out-smooth) both; }
.win-enter-scaleUp { animation: win-enter-scale-up 420ms var(--ease-out-smooth) both; }
.win-enter-dropSettle { animation: win-enter-drop-settle 480ms var(--ease-out-smooth) both; }
.win-enter-bounce { animation: win-enter-bounce 520ms var(--ease-out-smooth) both; }
.win-enter-rotateStraighten { animation: win-enter-rotate-straighten 460ms var(--ease-out-smooth) both; }

@media (prefers-reduced-motion: reduce) {
  .win-enter-fade, .win-enter-scaleUp, .win-enter-dropSettle, .win-enter-bounce, .win-enter-rotateStraighten {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* Win-celebration clone cards - own layer, own z-index tier (above
   #drag-layer's 1000, below #win-message's 2000): see script.js's
   createVictoryCelebration. Wrapper/inner split mirrors .drag-ghost/
   .drag-visual exactly - position captured once into the (untouched)
   wrapper, every animated property lives on the inner .card element via
   the Web Animations API. perspective/preserve-3d are only added (via
   .celebration-card--3d) for cards whose assigned behavior actually
   rotates in 3D - cheaper to composite for the other, purely-2D cards. */
#celebration-layer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 1100;
}
.celebration-card {
  position: fixed;
  pointer-events: none;
}
.celebration-card--3d { perspective: 900px; }
.celebration-card-inner { transform-origin: 50% 50%; }
.celebration-card--3d .celebration-card-inner { transform-style: preserve-3d; }

#confirm-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2100; /* above settings/win overlays, in case one is open behind it */
  padding: 20px;
}
#confirm-overlay.hidden { display: none; }

#confirm-card {
  background: #133;
  border: 1px solid var(--gold);
  border-radius: 12px;
  padding: 28px 32px;
  width: min(90vw, 400px);
  text-align: center;
}
#confirm-card h2 {
  color: var(--gold);
  margin: 0 0 10px;
  font-size: 19px;
}
#confirm-card p {
  color: #dce7e0; /* cream-tinted body text, readable against the dark teal card */
  margin: 0 0 22px;
  font-size: 14.5px;
  line-height: 1.5;
}
#confirm-message.hidden { display: none; }

#confirm-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
}
#confirm-actions button {
  flex: 1;
  padding: 10px 18px;
  border-radius: 6px;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
  transition: filter 0.15s;
}
#confirm-actions button:hover { filter: brightness(1.1); }
#confirm-actions button:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

/* Give Up / Shuffle Me a New Game: de-emphasized outline - still gold, just
   not the fill, so it doesn't compete with the emphasized choice on the
   other button. */
#confirmGiveUpBtn {
  background: transparent;
  border: 1px solid rgba(217, 180, 92, 0.6);
  color: var(--gold);
}
#confirmGiveUpBtn:hover { background: rgba(217, 180, 92, 0.12); }

/* Keep Playing: the emphasized, filled choice - same gold-fill treatment as
   the win card's button. DOM order puts this button last (see index.html),
   which matters for the mobile column-reverse rule below: it shows the
   *last* DOM child first/on top, so the emphasized choice stays prominent
   on phones too, without a separate mobile-only override. */
#confirmKeepBtn {
  background: var(--gold);
  border: none;
  color: #222;
}

/* MIKE Games System (SYSTEM.md §12, Support) - its own small card rather
   than reusing #confirm-card/#confirm-actions' selectors (which are
   scoped to actual <button> elements) since #supportCoffeeBtn below is a
   real <a>, not a button + JS handler. Same visual weight/shape as
   #confirm-card on purpose - this should read as "the same kind of tiny
   sheet you've already seen in this app," not a new visual system. */
#support-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2100;
  padding: 20px;
}
#support-overlay.hidden { display: none; }

#support-card {
  background: #133;
  border: 1px solid var(--gold);
  border-radius: 12px;
  padding: 28px 32px;
  width: min(90vw, 400px);
  text-align: center;
}
#support-card h2 {
  color: var(--gold);
  margin: 0 0 10px;
  font-size: 19px;
}
#support-card p {
  color: #dce7e0;
  margin: 0 0 22px;
  font-size: 14.5px;
  line-height: 1.5;
}

#support-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
}
#support-actions a,
#support-actions button {
  flex: 1;
  padding: 10px 18px;
  border-radius: 6px;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
  text-decoration: none;
  transition: filter 0.15s;
}
#support-actions a:hover,
#support-actions button:hover { filter: brightness(1.1); }
#support-actions a:focus-visible,
#support-actions button:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

/* Close: de-emphasized outline, same treatment as #confirmGiveUpBtn. */
#supportCloseBtn {
  background: transparent;
  border: 1px solid rgba(217, 180, 92, 0.6);
  color: var(--gold);
}
#supportCloseBtn:hover { background: rgba(217, 180, 92, 0.12); }

/* Buy Mike a Coffee: the emphasized, filled choice - this is the actual
   point of the sheet, unlike the abandon-game dialogs above where the
   safe default is what's emphasized. DOM order puts this last (see
   index.html) for the same mobile column-reverse reason as
   #confirmKeepBtn above. Buy Me a Coffee's own brand yellow rather than
   this app's own (more muted/tan) --gold, so it reads as the actual BMC
   button rather than just another gold-accented control in this UI. */
#supportCoffeeBtn {
  background: #ffdd00;
  border: none;
  color: #222;
}

/* MIKE Games System (see mike-games-system/SYSTEM.md §02, "Custom
   install prompt") - same overlay/card shell as #support-overlay/
   #support-card above, reused rather than a second set of rules, since
   the two are visually identical (dark felt-green card, gold border,
   gold heading, muted body copy, two-button row). */
#install-prompt-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2100;
  padding: 20px;
}
#install-prompt-overlay.hidden { display: none; }

#install-prompt-card {
  background: #133;
  border: 1px solid var(--gold);
  border-radius: 12px;
  padding: 28px 32px;
  width: min(85vw, 22rem);
  text-align: center;
}
#install-prompt-icon {
  display: flex;
  justify-content: center;
  color: var(--gold);
  margin: 0 0 8px;
}
#install-prompt-card h2 {
  color: var(--gold);
  margin: 0 0 10px;
  font-size: 19px;
}
#install-prompt-card p {
  color: #dce7e0;
  margin: 0 0 22px;
  font-size: 14.5px;
  line-height: 1.5;
}
#install-prompt-card p strong { color: var(--gold); }

#install-prompt-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
}
#install-prompt-actions button {
  flex: 1;
  padding: 10px 18px;
  border-radius: 6px;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
  transition: filter 0.15s;
}
#install-prompt-actions button:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}
/* Not now: de-emphasized outline, same treatment as #supportCloseBtn -
   this is the soft-dismiss choice, not the one worth visually pushing. */
#installPromptNotNowBtn {
  background: transparent;
  border: 1px solid rgba(217, 180, 92, 0.6);
  color: var(--gold);
}
#installPromptNotNowBtn:hover { background: rgba(217, 180, 92, 0.12); }
/* Got it: the filled, emphasized choice - same gold-fill treatment as
   #confirmKeepBtn's own primary action. */
#installPromptGotItBtn {
  background: var(--gold);
  border: 1px solid var(--gold);
  color: #222;
}
#installPromptGotItBtn:hover { filter: brightness(1.1); }

/* Above the mobile breakpoint, --card-w was a flat 84px regardless of how
   wide the viewport actually was - fine for a modest browser window, but
   on a genuinely wide screen (a landscape iPad, a large monitor) it left
   the whole board stranded small and centered with huge unused felt on
   either side. This scales card size up with the viewport - same "fill
   the available width" approach the mobile tier already uses - capped so
   it doesn't grow unreasonably large on an oversized display. */
@media (min-width: 721px) {
  :root {
    --pile-gap: 16px;
    --card-w: clamp(84px, calc((100vw - 120px - 6 * var(--pile-gap)) / 7), 170px);
    --card-h: calc(var(--card-w) * 122 / 84);
    /* Same peek-amount ratios as the original fixed 84px card (9px/24px). */
    --cascade-down: calc(var(--card-w) * 9 / 84);
    --cascade-up: calc(var(--card-w) * 24 / 84);
  }
  #board { padding: 24px 40px 60px; max-width: 1500px; }
}

@media (max-width: 720px) {
  :root {
    --board-pad: 8px;
    --pile-gap: 6px;
    /* Fill the screen edge-to-edge (minus the board padding and the 6
       inter-column gaps) rather than a fixed px size, so cards read as
       large and tightly packed - like a native mobile solitaire app -
       on every phone width instead of just the one they were tuned for.
       Height keeps the card art's native 84:122 aspect ratio so it's
       never stretched. */
    --card-w: calc((100vw - 2 * var(--board-pad) - 6 * var(--pile-gap)) / 7);
    --card-h: calc(var(--card-w) * 122 / 84);
    --cascade-down: calc(var(--card-w) * 0.143);
    --cascade-up: calc(var(--card-w) * 0.343);
  }
  #board { padding: var(--board-pad); }
  #top-row { margin-bottom: 40px; }
  .column { min-height: 260px; }

  /* Large, stacked, easy-to-tap targets rather than the desktop's
     side-by-side pair - a 44px+ tap height on each. */
  #confirm-actions { flex-direction: column-reverse; }
  #confirm-actions button { padding: 14px 18px; font-size: 15px; }
  #support-actions { flex-direction: column-reverse; }
  #support-actions a,
  #support-actions button { padding: 14px 18px; font-size: 15px; }
}

/* ---------- Bottom-toolbar touch layouts ----------
   Three separate device targets share the same underlying idea (vertical
   space is precious and the device is primarily touch-based, so pull the
   controls out of #topbar's flow and into a fixed bottom strip instead),
   but need different sizing/distribution to actually look right:
     - iPhone portrait: narrow width, plenty of height - six controls
       stretch to fill the width, icon-over-label, tallest touch targets.
     - iPhone landscape: generous width, almost no height - the toolbar
       itself has to be as short as possible, so icon+label sit side by
       side in a single compact row instead of stacked.
     - iPad portrait: generous width AND height - six controls stretching
       edge to edge across 750-1000+px would look absurd, so they stay a
       comfortable fixed size, centered in the strip, with room to spare.
   iPad landscape and desktop are deliberately untouched - neither matches
   any of these three.

   pointer:coarse gates the landscape and iPad rules specifically, since
   their width/height ranges alone could otherwise also match a resized
   desktop browser window (e.g. a short-and-wide or tall-and-narrow Chrome
   window) - a real mouse/trackpad reports pointer:fine and never matches,
   regardless of window shape. The existing iPhone-portrait rule below
   predates this and doesn't need it: nothing resizes a desktop window to
   480px wide in normal use, so it was never actually at risk. */

/* Shared visual language across all three - one continuous dark strip,
   not six outlined buttons: transparent/borderless buttons take their
   look entirely from the strip's own background, icons stay full-strength
   while labels sit visually secondary, and env(safe-area-inset-bottom) is
   handled once here so no per-breakpoint block below can forget it.
   Structural per-breakpoint bits (button width/height, icon size, stack
   vs row, font sizes) stay in each block separately - they differ enough
   between a cramped landscape phone and a spacious iPad that trying to
   share them would fight itself. */
@media (max-width: 480px) and (orientation: portrait),
       (max-height: 480px) and (orientation: landscape) and (pointer: coarse),
       (min-width: 481px) and (max-width: 1024px) and (orientation: portrait) and (pointer: coarse) {
  .btn-label--new, .btn-icon--new { display: block; }

  /* The six existing controls, physically unchanged (same elements, same
     script.js references, same disabled/.ready/.running/.flash state
     management) - just repositioned and restyled here and in the
     per-breakpoint blocks below. Moving .controls out of #topbar's flex
     flow via position:fixed is what shrinks the header to one row;
     nothing else needed to change there. */
  .controls {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 400; /* above tableau cards, below drag-layer's 1000 and every overlay */
    display: flex;
    background: rgba(6, 22, 15, 0.94);
    border-top: 1px solid rgba(255,255,255,0.1);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }

  .controls button,
  .controls button.icon-button {
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    border-radius: 8px;
    color: #eee;
    -webkit-tap-highlight-color: transparent;
  }
  /* Press feedback instead of :hover (meaningless on touch, and would stay
     "stuck" on after a tap on some browsers) - the one place these buttons
     get any visible fill at all, confirming a tap landed. */
  .controls button:active,
  .controls button.icon-button:active {
    background: rgba(255,255,255,0.12);
  }
  .controls button:disabled,
  .controls button.icon-button:disabled {
    opacity: 0.38;
  }
  .controls button svg,
  .controls button.icon-button svg {
    display: block;
    flex-shrink: 0;
  }
  .controls .btn-label {
    text-align: center;
    color: rgba(238, 238, 238, 0.7); /* secondary to the icon - the icon alone should read first */
    font-weight: 500;
    white-space: nowrap;
  }
}

/* ---------- iPhone portrait ---------- */
@media (max-width: 480px) and (orientation: portrait) {
  :root {
    --board-pad: 4px;
    --pile-gap: 3px;
  }

  #topbar { padding: 5px 14px; gap: 10px; }
  #topbar h1 { font-size: 16px; }
  .stats { font-size: 12px; gap: 10px; }

  #top-row { margin-bottom: 40px; }

  .controls { gap: 2px; padding: 6px 4px; }
  .controls button,
  .controls button.icon-button {
    flex: 1 1 0;
    flex-direction: column;
    gap: 2px;
    min-height: 48px;
    padding: 4px 2px;
    font-size: 9.5px;
    line-height: 1.05;
  }
  .controls button svg,
  .controls button.icon-button svg { width: 19px; height: 19px; }
  .controls .settings-gear, .controls .help-question { font-size: 17px; line-height: 1; }
}

/* ---------- iPhone landscape ----------
   The height budget is brutal (as little as ~375px total on the smallest
   current devices), so unlike portrait's stacked icon-over-label, icon and
   label sit side by side in one compact row - shorter than a stacked
   layout at the same font/icon size, which is the entire point here: this
   only earns its keep if the toolbar itself stays thin enough that moving
   it off the top produces a real net gain underneath. min-height stays at
   Apple's actual 44px floor rather than the 48px used elsewhere - still a
   generous touch target, just not spending height it can't spare. */
@media (max-height: 480px) and (orientation: landscape) and (pointer: coarse) {
  :root {
    --board-pad: 6px;
    --pile-gap: 5px;
    /* Filling the available WIDTH edge to edge (like every other mobile
       breakpoint) is wrong here specifically: width is never the tight
       constraint in landscape, height is, and a wider viewport only makes
       cards taller at the same aspect ratio - directly fighting the one
       thing actually scarce. On a wide-enough landscape phone this let a
       deep starting column's last card (compression already at its
       legibility floor - see computeTableauTops in script.js) render
       partway under the fixed toolbar, confirmed by measurement. The
       second min() term caps card width by a height budget instead: chrome
       (header + top-row's own card height + toolbar + margins) plus the
       tableau's worst realistic case (one card height + up to 6 collapsed
       face-down gaps) both have to fit in 100vh, solved for card width.
       Whichever formula is smaller wins, so this only ever pulls cards
       *down* from the width-fill size, never up past it. */
    --card-w: min(
      calc((100vw - 2 * var(--board-pad) - 6 * var(--pile-gap)) / 7),
      calc(16.7vh + 30px)
    );
    --card-h: calc(var(--card-w) * 122 / 84);
    --cascade-down: calc(var(--card-w) * 0.143);
    --cascade-up: calc(var(--card-w) * 0.343);
  }
  #board { padding: var(--board-pad) var(--board-pad) 0; }
  #topbar { padding: 4px 14px; gap: 10px; }
  #topbar h1 { font-size: 15px; }
  .stats { font-size: 11px; gap: 8px; }
  #top-row { margin-bottom: 6px; }
  .column { min-height: 160px; }

  .controls {
    gap: 2px;
    padding: 3px 8px;
    transition: transform 180ms ease-in-out, opacity 180ms ease-in-out;
  }
  .controls button,
  .controls button.icon-button {
    flex: 1 1 0;
    flex-direction: row;
    gap: 5px;
    min-height: 44px;
    padding: 4px 6px;
    font-size: 10px;
  }
  .controls button svg,
  .controls button.icon-button svg { width: 16px; height: 16px; }
  .controls .settings-gear, .controls .help-question { font-size: 15px; line-height: 1; }

  /* Expanding a compressed cascade for inspection (see toggleColumnExpanded
     in script.js) is the one time this toolbar gets out of the way
     entirely, rather than just staying compact - landscape's height budget
     is tight enough that even the legibility floor computeTableauTops
     already enforces can outgrow it (confirmed: an extreme 13-card,
     mostly-face-up column overlapped the toolbar by 237px before this).
     Sliding/fading out instead of just vanishing is what keeps it reading
     as "the toolbar stepped aside for this" rather than a glitch.
     pointer-events:none is set instantly (not part of the transition) so
     it can never be tapped mid-fade, and getToolbarReservedHeight() in
     script.js reads this exact property to know the moment it's inert -
     see that function's own comment. body.tableau-inspecting itself is
     already existing, pre-inspection-mode machinery (render() toggles it
     purely from expandedColumnIndex) - nothing new to wire up here beyond
     this one rule reacting to it. */
  body.tableau-inspecting .controls {
    transform: translateY(100%);
    opacity: 0;
    pointer-events: none;
  }
}

/* ---------- iPad portrait ----------
   Width/height are both generous here, so the six controls stay a
   comfortable fixed size and sit centered in the strip - stretching them
   edge to edge across 750-1000+px would look stretched and thin, not like
   a toolbar. --card-w/--pile-gap/--board-pad are untouched: iPad's
   existing min-width:721px sizing (a clamped, generous formula) already
   applies at every real iPad width here and doesn't need to change - only
   the controls' position does. */
@media (min-width: 481px) and (max-width: 1024px) and (orientation: portrait) and (pointer: coarse) {
  .controls {
    justify-content: center;
    gap: 6px;
    padding: 10px 20px;
  }
  .controls button,
  .controls button.icon-button {
    flex: 0 0 auto;
    flex-direction: column;
    width: 88px;
    gap: 4px;
    min-height: 56px;
    padding: 8px 6px;
    font-size: 12px;
  }
  .controls button svg,
  .controls button.icon-button svg { width: 23px; height: 23px; }
  .controls .settings-gear, .controls .help-question { font-size: 21px; line-height: 1; }
}

/* ============================================================
   OPENING INTRO - markup lives in index.html (three independent layers:
   #intro-mike, #intro-apostrophe, #intro-object, same split as Mike's
   Mahjong's own intro so the MIKE+'s pair is identical across games and
   only #intro-object's image/entrance changes per game).

   MIKE Games System readiness gate (CORE, see mike-games-system/SYSTEM.md
   §01) - MIKE himself has no animation at all, so he's simply present from
   the very first paint, completely unconditionally. Everything after him
   (apostrophe-s, the dealt card, the whole screen's exit drop) is CSS-timed
   via animation-delay exactly as before, but now starts every one of those
   animations already `animation-play-state: paused` - a paused CSS
   animation holds at its first frame and its own delay does not tick down,
   so the whole sequence genuinely cannot proceed past "MIKE alone" until
   script.js adds `.intro-ready` to #intro-screen (see initIntro()/
   finishWhenReady()), which happens once this game's critical startup
   readiness contract resolves (the dealt board's card art, decoded) or a
   generous safety-net elapses. Releasing play-state doesn't reset or skip
   any delay - the 1000ms/1350ms/2900ms timing below is exactly the same
   sequence as always, it just starts counting from release instead of from
   page load. initIntro() still only ever *removes* #intro-screen once its
   own exit animation finishes (or an outer safety-net timeout fires) - the
   gate only changes when the sequence is allowed to *begin*, not how it
   ends.

   Timeline is beat-for-beat identical to Mahjong's (~3.25s total): MIKE is
   simply present from first paint (no animation, @0ms) and holds ALONE
   for 1000ms; apostrophe-s then snaps in near-instantly @1000ms and sits
   with MIKE alone for another 350ms; the card begins its deal @1350ms and
   lands @1700ms; the complete lockup then holds completely still for
   1200ms; at 2900ms the ENTIRE #intro-screen - white background included -
   drops straight down and off the bottom of the viewport as one rigid
   unit (ends ~3250ms). The only intentional difference from Mahjong is
   HOW the game-specific object arrives: Mahjong's tile slides in flat from
   the left; here the card is dealt - it starts fully below the viewport,
   flicks/spins upward, and decelerates into its resting tilt (see
   introCardIn below) - same 350ms duration and easing as Mahjong's tile,
   same 1350ms delay, same landing instant at 1700ms.

   Mahjong sizes its intro with vh/vw because that whole app renders as a
   capped 430px mobile column even on desktop. Solitaire uses the full
   viewport, so sizing here is built around --intro-unit: a single scale
   driven by vmin (the smaller of viewport width/height, so short landscape
   phones and narrow portrait phones both shrink correctly) with three
   breakpoint tiers for its upper bound, calibrated against design/intro/
   sizing.png so the composition stays restrained on tablet/desktop instead
   of ballooning with viewport width. MIKE/apostrophe/card sizes are all
   multiples of this one variable, so the whole lockup scales as a single
   composition rather than as independently-tuned elements. */
:root {
  --intro-unit: clamp(7px, 2.6vmin, 9px);
}
@media (min-width: 700px) {
  :root { --intro-unit: clamp(8px, 2.2vmin, 11px); }
}
@media (min-width: 1100px) {
  :root { --intro-unit: clamp(9px, 1.7vmin, 12.5px); }
}
#intro-screen {
  position: fixed;
  inset: 0;
  z-index: 9999;
  overflow: hidden;
  background: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: introScreenDrop 350ms cubic-bezier(.4, 0, 1, 1) 2900ms forwards;
  /* MIKE Games System readiness gate (see initIntro()/finishWhenReady() in
     script.js) - paused by default so the drop can never start counting
     down its 2900ms delay until script.js flips this to running. A CSS
     animation's own delay does not advance while paused, so releasing this
     later doesn't skip or compress anything - the 2900ms/350ms drop timing
     below is unchanged, it just doesn't begin until the gate says so. */
  animation-play-state: paused;
}
#intro-frame {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* MIKE Games System readiness-gate recovery state (see mike-games-system/
   SYSTEM.md §01) - taken out of #intro-screen's own flex layout via
   position:absolute (not a flex sibling of #intro-frame) so it can't
   compete for centering with the MIKE/card lockup above it or shift that
   lockup's own position when it appears; anchored toward the bottom third
   of the screen instead, comfortably clear of the lockup at any
   reasonable viewport size. Intentionally plain: dark, legible text on
   the intro's own white field, one bordered button, no spinner/progress
   bar - the message itself is the whole affordance. */
#intro-recovery {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: max(48px, calc(env(safe-area-inset-bottom, 0px) + 32px));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  text-align: center;
}
#intro-recovery.hidden { display: none; }
#intro-recovery p {
  margin: 0;
  color: #333;
  font-size: 15px;
}
#intro-recovery button {
  padding: 10px 22px;
  border-radius: 6px;
  border: 1px solid #333;
  background: #fff;
  color: #333;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
#intro-recovery button:hover { background: #f2f2f2; }
#intro-recovery button:focus-visible {
  outline: 2px solid #333;
  outline-offset: 2px;
}
/* The single lockup: MIKE's + the card stacked as ONE flex column, tight
   fixed gap, centered as a unit - so the two layers below never drift
   apart or need independent viewport-relative anchoring the way Mahjong's
   dual top:% layers do (Mahjong can get away with that because its tile
   dominates the frame; Solitaire's restrained composition needs a gap that
   stays tight regardless of viewport height). */
#intro-lockup {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(var(--intro-unit) * 1.3);
}
/* MIKE alone is what's centered in the wordmark row - this wrapper
   shrink-wraps to #intro-mike's own box (the only normal-flow child;
   apostrophe-s is taken out of flow below), so MIKE's true center always
   lands on the lockup's true center regardless of the 's hanging off to
   its right. */
#intro-wordmark {
  position: relative;
}
#intro-wordmark img { display: block; }
#intro-mike { display: block; }
#intro-mike img { height: calc(var(--intro-unit) * 6.2); width: auto; }
/* Positioned relative to #intro-wordmark (centered on MIKE), not a flex
   sibling of it - left:100% + margin-left reserves its space without ever
   affecting MIKE's own box, so apostrophe-s appearing can't shift/resize/
   re-center MIKE. */
#intro-apostrophe {
  position: absolute;
  left: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-left: calc(var(--intro-unit) * 0.5);
  display: block;
}
#intro-apostrophe img { height: calc(var(--intro-unit) * 2.3); width: auto; }
/* MIKE has no animation at all - it's simply part of the static page,
   present from the very first paint, which is what gives Phase 1 its true
   MIKE-only beat. */
#intro-mike { opacity: 1; }
/* Apostrophe-s stays invisible through the MIKE-only pause, then snaps to
   visible near-instantly at 1000ms - a 10ms "animation" only exists so it
   has a real animationend and a delay to hang the snap on; there is no
   transform/scale/rotation to see it move through. */
#intro-apostrophe { opacity: 0; animation: introFadeIn 10ms linear 1000ms forwards; animation-play-state: paused; }
/* Same static/animated split as the wordmark: #intro-object-pos reserves
   the card's resting slot in the flex column (a normal in-flow box, sized
   by width + its image's intrinsic aspect-ratio) so the layout never
   shifts while the card is transformed away above/below it; #intro-object
   is what actually animates the deal - transforms don't affect layout, so
   the reserved slot stays put throughout. */
#intro-object-pos {
  width: calc(var(--intro-unit) * 26);
}
/* filter:drop-shadow (not box-shadow) so the shadow follows the card's
   actual rounded-rect silhouette and rides along with #intro-object's own
   transform automatically, since it's painted post-transform - including
   through the rotation below, since the alpha-shape it traces is whatever
   the PNG's opaque pixels currently look like post-transform, not a
   static rectangle. Tight offset + tight blur + darker alpha so the card
   reads as sitting just barely above the white surface, not floating. */
#intro-object-pos img { display: block; width: 100%; height: auto; filter: drop-shadow(1px 2px 3px rgba(0, 0, 0, .5)); }
#intro-object {
  display: block;
  opacity: 0;
  animation: introCardIn 350ms cubic-bezier(.215, .61, .355, 1) 1350ms forwards;
  animation-play-state: paused;
}
/* MIKE Games System readiness gate release (see script.js) - script.js adds
   this class to #intro-screen the instant the critical startup readiness
   contract resolves (the dealt board's own card art, decoded - see
   criticalAssetsReadyPromise), or a generous safety-net elapses. Every
   animation-delay above starts counting from this moment, not from page
   load, so MIKE simply holds alone for longer on a slow/cold load rather
   than the game appearing behind unfinished art. */
#intro-screen.intro-ready,
.intro-ready #intro-apostrophe,
.intro-ready #intro-object {
  animation-play-state: running;
}

@keyframes introFadeIn { from { opacity: 0; } to { opacity: 1; } }
/* The dealt-card entrance: begins fully below the viewport (100vh of pure
   vertical clearance stacked on top of the lockup's own already-centered
   resting position - generous overkill on purpose, so the card is offscreen
   at every supported viewport regardless of its own rendered size) at a
   dramatic -55deg tilt, then spins up to its 0,0 resting transform - TWO
   full extra clockwise rotations (+720deg) on top of the original ~61deg
   sweep to the resting angle, landing at 726deg, which is visually
   identical to the +6deg tilt shown in design/intro/layout.png (726 mod
   360 = 6) - before settling. easeOutCubic (a standard, well-tested
   decelerate curve - both y-control-points <=1, so it's monotonic and can
   never overshoot past the resting position) rather than Mahjong tile's
   own far more front-loaded curve: still fast-start/decelerate-hard, but
   spread widely enough across the full 350ms for two full rotations to
   actually read as two rotations instead of resolving as a blur in the
   first few ms. Translate-only + rotate - no layout properties animated. */
@keyframes introCardIn {
  0% { opacity: 0; transform: translateY(100vh) rotate(-55deg); }
  15% { opacity: 1; }
  100% { opacity: 1; transform: translateY(0) rotate(726deg); }
}
/* Exit: the whole screen (white bg + everything in it) translates straight
   down as one rigid unit - no opacity change, no scale, no rotation,
   nothing animated independently inside it. 115% (of the element's own
   height, a full 100vh via inset:0) guarantees full clearance past the
   viewport edge with a little margin to spare. cubic-bezier(.4,0,1,1) is a
   pure accelerate curve: a brief slow start that keeps speeding up the
   whole way, never easing off at the end. */
@keyframes introScreenDrop {
  0% { transform: translateY(0); }
  100% { transform: translateY(115%); }
}
@keyframes introFadeOutReduced { from { opacity: 1; } to { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  /* Intro: MIKE is already static and apostrophe already a near-instant
     opacity snap - neither has real motion to trim. Swap the card's deal
     spin and the screen's drop for quick plain fades at the same beats, no
     transform - same philosophy as Mahjong's own reduced-motion intro. */
  #intro-object {
    animation-name: introFadeIn;
    animation-duration: 150ms;
    animation-timing-function: ease-out;
    animation-delay: 1350ms;
    animation-fill-mode: forwards;
  }
  #intro-screen {
    animation-name: introFadeOutReduced;
    animation-duration: 200ms;
    animation-timing-function: ease-out;
    animation-delay: 2900ms;
    animation-fill-mode: forwards;
  }
}

/* ---------- Home Screen icon migration notice ---------- */
/* Same overlay/card shell conventions as #settings-overlay/#confirm-overlay
   (dark scrim, #133 card, gold border/radius) - new markup rather than a
   literal reuse of either, since its content shape (icon, numbered steps,
   checkbox, two full-width stacked buttons) doesn't fit either existing
   card's structure. */

#icon-notice-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2100; /* same tier as #confirm-overlay - both are the "most important thing right now" */
  padding: 20px;
}
#icon-notice-overlay.hidden { display: none; }

#icon-notice-card {
  position: relative;
  background: #133;
  border: 1px solid var(--gold);
  border-radius: 12px;
  padding: 34px 28px 24px;
  width: min(90vw, 420px);
  max-height: 90vh;
  overflow-y: auto;
  text-align: center;
}

#iconNoticeCloseBtn {
  position: absolute;
  top: 14px;
  right: 14px;
  background: none;
  border: none;
  color: #eee;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.7;
  padding: 4px;
}
#iconNoticeCloseBtn:hover { opacity: 1; }

/* The complete supplied icon PNG, shown as-is - no cropping, no
   re-extracting the sign from it. Rounded corners + a thin white outline
   are a UI treatment applied around the image here, not an alteration of
   the source artwork itself. */
#icon-notice-icon {
  width: 84px;
  height: 84px;
  border-radius: 18px;
  border: 2px solid #fff;
  display: block;
  margin: 0 auto 18px;
  object-fit: cover;
}

#icon-notice-title {
  color: var(--gold);
  font-size: 25px;
  line-height: 1.2;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin: 0 0 16px;
}
.icon-notice-sparkle {
  font-size: 16px;
  opacity: 0.85;
}

#icon-notice-card > p {
  color: #dce7e0;
  font-size: 14px;
  line-height: 1.5;
  margin: 0 0 14px;
}
#icon-notice-card > p strong { color: #fff; }

#icon-notice-card > p.icon-notice-subhead {
  color: var(--gold);
  font-weight: 600;
  font-size: 13px;
  text-align: left;
  margin: 18px 0 10px;
}

#icon-notice-steps {
  list-style: none;
  padding: 0;
  margin: 0 0 18px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  text-align: left;
}
#icon-notice-steps li {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
.icon-notice-step-num {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--gold);
  color: #143;
  font-weight: 700;
  font-size: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
}
#icon-notice-steps li > span:last-child {
  color: #dce7e0;
  font-size: 13.5px;
  line-height: 1.45;
  padding-top: 3px;
}
#icon-notice-steps li strong { color: #fff; }
#icon-notice-url { color: var(--gold); }

#icon-notice-card > p.icon-notice-footnote {
  border-top: 1px solid rgba(255,255,255,0.12);
  padding-top: 16px;
  font-size: 13px;
  margin-bottom: 18px;
}
.icon-notice-footnote em { color: #fff; font-style: normal; } /* "may" is the one word that matters most here - see script.js's own comment on why this can never promise "will" */

.icon-notice-btn {
  display: block;
  width: 100%;
  padding: 12px 18px;
  border-radius: 8px;
  font-weight: 700;
  font-size: 15px;
  cursor: pointer;
  transition: filter 0.15s, background 0.15s;
}
.icon-notice-btn + .icon-notice-btn { margin-top: 10px; }
.icon-notice-btn:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}
.icon-notice-btn--primary {
  background: var(--gold);
  border: none;
  color: #222;
}
.icon-notice-btn--primary:hover { filter: brightness(1.08); }
.icon-notice-btn--primary:disabled {
  opacity: 0.75;
  cursor: default;
  filter: none;
}
.icon-notice-btn--secondary {
  background: transparent;
  border: 1px solid rgba(217, 180, 92, 0.6);
  color: var(--gold);
}
.icon-notice-btn--secondary:hover { background: rgba(217, 180, 92, 0.12); }

#icon-notice-dismiss-label {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: 16px;
  color: #9fb3ab;
  font-size: 13px;
  cursor: pointer;
  user-select: none;
}
#icon-notice-dismiss-label input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--gold);
  cursor: pointer;
}
