.rd-root *, .rd-root ::before, .rd-root ::after {
    --tw-border-spacing-x: 0;
    --tw-border-spacing-y: 0;
    --tw-translate-x: 0;
    --tw-translate-y: 0;
    --tw-rotate: 0;
    --tw-skew-x: 0;
    --tw-skew-y: 0;
    --tw-scale-x: 1;
    --tw-scale-y: 1;
    --tw-pan-x:  ;
    --tw-pan-y:  ;
    --tw-pinch-zoom:  ;
    --tw-scroll-snap-strictness: proximity;
    --tw-gradient-from-position:  ;
    --tw-gradient-via-position:  ;
    --tw-gradient-to-position:  ;
    --tw-ordinal:  ;
    --tw-slashed-zero:  ;
    --tw-numeric-figure:  ;
    --tw-numeric-spacing:  ;
    --tw-numeric-fraction:  ;
    --tw-ring-inset:  ;
    --tw-ring-offset-width: 0px;
    --tw-ring-offset-color: #fff;
    --tw-ring-color: rgb(59 130 246 / 0.5);
    --tw-ring-offset-shadow: 0 0 #0000;
    --tw-ring-shadow: 0 0 #0000;
    --tw-shadow: 0 0 #0000;
    --tw-shadow-colored: 0 0 #0000;
    --tw-blur:  ;
    --tw-brightness:  ;
    --tw-contrast:  ;
    --tw-grayscale:  ;
    --tw-hue-rotate:  ;
    --tw-invert:  ;
    --tw-saturate:  ;
    --tw-sepia:  ;
    --tw-drop-shadow:  ;
    --tw-backdrop-blur:  ;
    --tw-backdrop-brightness:  ;
    --tw-backdrop-contrast:  ;
    --tw-backdrop-grayscale:  ;
    --tw-backdrop-hue-rotate:  ;
    --tw-backdrop-invert:  ;
    --tw-backdrop-opacity:  ;
    --tw-backdrop-saturate:  ;
    --tw-backdrop-sepia:  ;
    --tw-contain-size:  ;
    --tw-contain-layout:  ;
    --tw-contain-paint:  ;
    --tw-contain-style:  ;
}
.rd-root ::backdrop {
    --tw-border-spacing-x: 0;
    --tw-border-spacing-y: 0;
    --tw-translate-x: 0;
    --tw-translate-y: 0;
    --tw-rotate: 0;
    --tw-skew-x: 0;
    --tw-skew-y: 0;
    --tw-scale-x: 1;
    --tw-scale-y: 1;
    --tw-pan-x:  ;
    --tw-pan-y:  ;
    --tw-pinch-zoom:  ;
    --tw-scroll-snap-strictness: proximity;
    --tw-gradient-from-position:  ;
    --tw-gradient-via-position:  ;
    --tw-gradient-to-position:  ;
    --tw-ordinal:  ;
    --tw-slashed-zero:  ;
    --tw-numeric-figure:  ;
    --tw-numeric-spacing:  ;
    --tw-numeric-fraction:  ;
    --tw-ring-inset:  ;
    --tw-ring-offset-width: 0px;
    --tw-ring-offset-color: #fff;
    --tw-ring-color: rgb(59 130 246 / 0.5);
    --tw-ring-offset-shadow: 0 0 #0000;
    --tw-ring-shadow: 0 0 #0000;
    --tw-shadow: 0 0 #0000;
    --tw-shadow-colored: 0 0 #0000;
    --tw-blur:  ;
    --tw-brightness:  ;
    --tw-contrast:  ;
    --tw-grayscale:  ;
    --tw-hue-rotate:  ;
    --tw-invert:  ;
    --tw-saturate:  ;
    --tw-sepia:  ;
    --tw-drop-shadow:  ;
    --tw-backdrop-blur:  ;
    --tw-backdrop-brightness:  ;
    --tw-backdrop-contrast:  ;
    --tw-backdrop-grayscale:  ;
    --tw-backdrop-hue-rotate:  ;
    --tw-backdrop-invert:  ;
    --tw-backdrop-opacity:  ;
    --tw-backdrop-saturate:  ;
    --tw-backdrop-sepia:  ;
    --tw-contain-size:  ;
    --tw-contain-layout:  ;
    --tw-contain-paint:  ;
    --tw-contain-style:  ;
}
  .rd-root * {
    box-sizing: border-box;
  }
.rd-root .visible {
    visibility: visible;
}
.rd-root .invisible {
    visibility: hidden;
}
.rd-root .collapse {
    visibility: collapse;
}
.rd-root .static {
    position: static;
}
.rd-root .fixed {
    position: fixed;
}
.rd-root .absolute {
    position: absolute;
}
.rd-root .relative {
    position: relative;
}
.rd-root .sticky {
    position: sticky;
}
.rd-root .mx-auto {
    margin-left: auto;
    margin-right: auto;
}
.rd-root .mb-0\.5 {
    margin-bottom: 0.125rem;
}
.rd-root .mb-2 {
    margin-bottom: 0.5rem;
}
.rd-root .mb-3 {
    margin-bottom: 0.75rem;
}
.rd-root .mb-4 {
    margin-bottom: 1rem;
}
.rd-root .mt-0\.5 {
    margin-top: 0.125rem;
}
.rd-root .mt-1 {
    margin-top: 0.25rem;
}
.rd-root .mt-2 {
    margin-top: 0.5rem;
}
.rd-root .mt-3 {
    margin-top: 0.75rem;
}
.rd-root .block {
    display: block;
}
.rd-root .inline-block {
    display: inline-block;
}
.rd-root .inline {
    display: inline;
}
.rd-root .flex {
    display: flex;
}
.rd-root .table {
    display: table;
}
.rd-root .grid {
    display: grid;
}
.rd-root .hidden {
    display: none;
}
.rd-root .shrink {
    flex-shrink: 1;
}
.rd-root .grow {
    flex-grow: 1;
}
.rd-root .transform {
    transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.rd-root .flex-wrap {
    flex-wrap: wrap;
}
.rd-root .flex-nowrap {
    flex-wrap: nowrap;
}
.rd-root .items-start {
    align-items: flex-start;
}
.rd-root .items-center {
    align-items: center;
}
.rd-root .justify-center {
    justify-content: center;
}
.rd-root .gap-1\.5 {
    gap: 0.375rem;
}
.rd-root .gap-2 {
    gap: 0.5rem;
}
.rd-root .gap-\[3px\] {
    gap: 3px;
}
.rd-root .overflow-hidden {
    overflow: hidden;
}
.rd-root .rounded-lg {
    border-radius: 0.5rem;
}
.rd-root .rounded-xl {
    border-radius: 0.75rem;
}
.rd-root .border {
    border-width: 1px;
}
.rd-root .border-2 {
    border-width: 2px;
}
.rd-root .border-b {
    border-bottom-width: 1px;
}
.rd-root .border-t {
    border-top-width: 1px;
}
.rd-root .border-amber-400 {
    --tw-border-opacity: 1;
    border-color: rgb(251 191 36 / var(--tw-border-opacity, 1));
}
.rd-root .border-white\/10 {
    border-color: rgb(255 255 255 / 0.1);
}
.rd-root .bg-amber-50 {
    --tw-bg-opacity: 1;
    background-color: rgb(255 251 235 / var(--tw-bg-opacity, 1));
}
.rd-root .p-3 {
    padding: 0.75rem;
}
.rd-root .px-2 {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
}
.rd-root .px-4 {
    padding-left: 1rem;
    padding-right: 1rem;
}
.rd-root .px-6 {
    padding-left: 1.5rem;
    padding-right: 1.5rem;
}
.rd-root .py-2 {
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
}
.rd-root .py-3 {
    padding-top: 0.75rem;
    padding-bottom: 0.75rem;
}
.rd-root .py-8 {
    padding-top: 2rem;
    padding-bottom: 2rem;
}
.rd-root .text-left {
    text-align: left;
}
.rd-root .text-center {
    text-align: center;
}
.rd-root .font-mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.rd-root .text-3xl {
    font-size: 1.875rem;
    line-height: 2.25rem;
}
.rd-root .text-6xl {
    font-size: 3.75rem;
    line-height: 1;
}
.rd-root .text-7xl {
    font-size: 4.5rem;
    line-height: 1;
}
.rd-root .text-base {
    font-size: 1rem;
    line-height: 1.5rem;
}
.rd-root .text-lg {
    font-size: 1.125rem;
    line-height: 1.75rem;
}
.rd-root .text-sm {
    font-size: 0.875rem;
    line-height: 1.25rem;
}
.rd-root .text-xl {
    font-size: 1.25rem;
    line-height: 1.75rem;
}
.rd-root .text-xs {
    font-size: 0.75rem;
    line-height: 1rem;
}
.rd-root .font-black {
    font-weight: 900;
}
.rd-root .font-bold {
    font-weight: 700;
}
.rd-root .font-medium {
    font-weight: 500;
}
.rd-root .font-semibold {
    font-weight: 600;
}
.rd-root .uppercase {
    text-transform: uppercase;
}
.rd-root .leading-none {
    line-height: 1;
}
.rd-root .leading-relaxed {
    line-height: 1.625;
}
.rd-root .leading-tight {
    line-height: 1.25;
}
.rd-root .tracking-widest {
    letter-spacing: 0.1em;
}
.rd-root .text-amber-900 {
    --tw-text-opacity: 1;
    color: rgb(120 53 15 / var(--tw-text-opacity, 1));
}
.rd-root .text-blue-200 {
    --tw-text-opacity: 1;
    color: rgb(191 219 254 / var(--tw-text-opacity, 1));
}
.rd-root .text-white {
    --tw-text-opacity: 1;
    color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
.rd-root .ring {
    --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
    --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
    box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.rd-root .drop-shadow {
    --tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
    filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.rd-root .filter {
    filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.rd-root .transition {
    transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
    transition-duration: 150ms;
}
.rd-root .ease-in {
    transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
}
.rd-root .ease-in-out {
    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.rd-root .ease-out {
    transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
/* --- from src/app/globals.css (the app's own reveal keyframes) --- */



.rd-root {
  --color-navy: #002E5D;
  --color-burgundy: #5D1B1C;
  --color-amber: #b45309;
}

/* Results page animations */
@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

@keyframes popIn {
  0% { opacity: 0; transform: scale(0.5); }
  100% { opacity: 1; transform: scale(1); }
}

/* The wheel's winning slice lifting out of the wheel to be read at full size.
   rotateX so it reads as the wedge tipping up toward the viewer rather than a
   caption fading in over the top of it. */
@keyframes wheelLift {
  0%   { opacity: 0; transform: rotateX(-80deg) scale(0.7); }
  100% { opacity: 1; transform: rotateX(0deg) scale(1); }
}

/* Prize Showcase (2026-08-10): the pre-draw prize intro on the results page.
   Every keyframe below is percentage-locked to PRIZE_SHOWCASE_MS (3400ms) in
   ResultsClient.tsx, so the constant stays the single knob for the whole beat.
   Three acts, in percent of that total:

     ENTRANCE  0% → ~22% (0–750ms)   spring in, STAGGERED
     HOLD     ~22% → ~84% (~2.1s)    never frozen — float + glow + drift
     EXIT     ~84% → 100% (~545ms)   anticipation blip, then dive

   The 2026-08-10 motion overhaul (the brief: "make it more fluid, add some bounce,
   don't make it so static") replaced a flat ease-in/dead-hold/ease-out with:
   overshoot springs on the way in; a cascade (kicker ~340ms → photo peak
   ~440ms → name ~750ms) so the three elements land as beats rather than as one
   block; continuous low-amplitude life during the hold; and an exit that winds
   UP before it dives, so the prize reads as diving into the draw card.

   Keyframes carry their own animation-timing-function per segment — the
   shorthand's easing is only a fallback for intervals that don't set one.

   showcaseStill is the reduced-motion twin of showcaseZoom (opacity ONLY, same
   total duration — see the stillFadeIn note below for the policy). Under
   prefers-reduced-motion ResultsClient sets `animation: none` on the staggered
   children and does not render the glow/vignette layers at all, so the whole
   family below is inert and only showcaseStill + showcaseBackdrop run.
   Everything animates transform/opacity only — no layout-thrashing props. */

/* The card as a whole: burst out of the draw-card area with an overshoot past
   1, ring down to rest, then wind up and dive back in. */
@keyframes showcaseZoom {
  0%   { opacity: 0; transform: scale(0.12); animation-timing-function: cubic-bezier(0.16, 0.88, 0.3, 1); }
  9%   { opacity: 1; transform: scale(1.06); animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
  13%  { opacity: 1; transform: scale(0.982); animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
  17%  { opacity: 1; transform: scale(1.008); animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
  21%  { opacity: 1; transform: scale(1); animation-timing-function: linear; }
  84%  { opacity: 1; transform: scale(1); animation-timing-function: cubic-bezier(0.25, 0.9, 0.4, 1); }
  89%  { opacity: 1; transform: scale(1.055); animation-timing-function: cubic-bezier(0.6, 0, 0.85, 0.35); }
  100% { opacity: 0; transform: scale(0.08); }
}

/* Beat 1 — the placement kicker drops in first (settles ~340ms). */
@keyframes showcaseKicker {
  0%   { opacity: 0; transform: translateY(-16px); animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
  10%  { opacity: 1; transform: translateY(0); animation-timing-function: linear; }
  85%  { opacity: 1; transform: translateY(0); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.4); }
  100% { opacity: 0; transform: translateY(-14px); }
}

/* Beat 2 — the photo pops a beat later with its own bounce (peak ~440ms,
   settled ~714ms), then rides the card's exit anticipation before diving. */
@keyframes showcasePhotoPop {
  0%   { opacity: 0; transform: scale(0.72) translateY(20px); animation-timing-function: linear; }
  5%   { opacity: 0; transform: scale(0.72) translateY(20px); animation-timing-function: cubic-bezier(0.18, 0.92, 0.32, 1); }
  13%  { opacity: 1; transform: scale(1.075) translateY(-6px); animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
  17%  { opacity: 1; transform: scale(0.978) translateY(2px); animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
  21%  { opacity: 1; transform: scale(1) translateY(0); animation-timing-function: linear; }
  84%  { opacity: 1; transform: scale(1) translateY(0); animation-timing-function: cubic-bezier(0.25, 0.9, 0.4, 1); }
  89%  { opacity: 1; transform: scale(1.02) translateY(-4px); animation-timing-function: cubic-bezier(0.6, 0, 0.85, 0.35); }
  100% { opacity: 0.8; transform: scale(0.85) translateY(14px); }
}

/* Beat 3 — the prize name slides up last (settles ~750ms). */
@keyframes showcaseName {
  0%   { opacity: 0; transform: translateY(28px) scale(0.95); animation-timing-function: linear; }
  11%  { opacity: 0; transform: translateY(28px) scale(0.95); animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
  22%  { opacity: 1; transform: translateY(0) scale(1); animation-timing-function: linear; }
  86%  { opacity: 1; transform: translateY(0) scale(1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.4); }
  100% { opacity: 0; transform: translateY(18px) scale(0.92); }
}

/* The hold is not a freeze. A 4.2s breathe on the photo frame — 9px of travel
   and 1.4% of scale — is enough to read as "alive" at projector distance and
   slow enough that it never pulls focus. Runs independently of the 3400ms
   timeline (infinite) because it must not land on a beat. */
@keyframes showcaseFloat {
  0%   { transform: translateY(0) scale(1); }
  50%  { transform: translateY(-9px) scale(1.014); }
  100% { transform: translateY(0) scale(1); }
}

/* Soft halo behind the photo, breathing on a different period (5.2s) to the
   float so the two never sync into a single obvious pulse. */
@keyframes showcaseGlow {
  0%   { opacity: 0.22; transform: scale(0.96); }
  50%  { opacity: 0.6; transform: scale(1.06); }
  100% { opacity: 0.22; transform: scale(0.96); }
}

/* Backdrop vignette drift — a slow push-in over the hold so the dark field
   behind the prize isn't a flat wall. Locked to the 3400ms timeline. */
@keyframes showcaseVignette {
  0%   { opacity: 0; transform: scale(1.16) translate3d(0, 2%, 0); }
  22%  { opacity: 0.85; transform: scale(1.09) translate3d(0, 1%, 0); }
  60%  { opacity: 1; transform: scale(1) translate3d(0, -1.5%, 0); }
  100% { opacity: 0; transform: scale(1.14) translate3d(0, 2.5%, 0); }
}

@keyframes showcaseStill {
  0%   { opacity: 0; }
  10%  { opacity: 1; }
  90%  { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes showcaseBackdrop {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes grandPopIn {
  0% { opacity: 0; transform: scale(0.3) rotate(-5deg); }
  60% { transform: scale(1.1) rotate(2deg); }
  80% { transform: scale(0.97) rotate(-1deg); }
  100% { opacity: 1; transform: scale(1) rotate(0deg); }
}

@keyframes confettiBounce {
  0% { opacity: 0; transform: translateY(20px) scale(0.5); }
  60% { transform: translateY(-8px) scale(1.2); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}

/* Search-to-section visual echo. Fired by useHashScrollHighlight when
   a search result lands the operator on a specific settings card. The
   navy box-shadow + outer glow draws the eye to the target card for
   ~2.5s without being garish. */
@keyframes scrollHighlight {
  0%   { box-shadow: 0 0 0 0   rgba(0, 46, 93, 0),    0 0 0  0  rgba(0, 46, 93, 0); }
  10%  { box-shadow: 0 0 0 4px rgba(0, 46, 93, 0.55), 0 0 28px 8px rgba(0, 46, 93, 0.30); }
  100% { box-shadow: 0 0 0 0   rgba(0, 46, 93, 0),    0 0 0  0  rgba(0, 46, 93, 0); }
}
.rd-root .scroll-highlight-pulse {
  animation: scrollHighlight 2500ms ease-out;
  border-radius: 0.75rem;
}

/* The same echo, for a TABLE ROW target.
 *
 * The rule above is box-shadow only, and a <tr> inside a border-collapse
 * table does not paint box-shadow or outline in Chrome — the pulse simply
 * never appeared on the participants row a readiness Fix link scrolls to
 * (#participant-<id>, punch-list item 3, 2026-09-17). The cells DO paint
 * background-color, so the row pulses by animating the <td> backgrounds
 * instead. Amber rather than navy because the only thing that scroll-targets
 * a participant row today is a warning about that participant, and the row is
 * already tinted red underneath; the animation has to be visible ON that
 * tint, which a navy glow at 30% would not be. */
@keyframes scrollHighlightRow {
  0%   { background-color: rgba(251, 191, 36, 0); }
  10%  { background-color: rgba(251, 191, 36, 0.75); }
  100% { background-color: rgba(251, 191, 36, 0); }
}
.rd-root tr.scroll-highlight-pulse > td {
  animation: scrollHighlightRow 2500ms ease-out;
}

/* Split-flap: the character strip rolling inside an unsettled cell.
   steps() gives the discrete flap-to-flap jumps of a real board while the
   whole thing stays a compositor-only transform — no JS interval, no React
   re-render per flick. --flap-shift is -(strip length x cell height), set
   per cell as an inline CSS variable. */
@keyframes flapScroll {
  from { transform: translateY(0); }
  to { transform: translateY(var(--flap-shift, -400px)); }
}

/* A split-flap cell locking in: the settled letter falls into place like the
   last flap dropping. */
@keyframes flapSettle {
  0% { transform: rotateX(-70deg); }
  100% { transform: rotateX(0deg); }
}

/* Lottery balls churning in the drum.
 *
 * The waypoints live in per-ball CSS variables so ONE keyframe rule serves
 * every ball and the whole tumble runs on the compositor — no JS tick.
 *
 * Eight waypoints rather than four, and driven LINEAR rather than eased. Eased
 * is how something suspended in liquid moves — decelerating into every turn —
 * not how something being thrown around a chamber moves.
 *
 * These stops are EVENLY SPACED, so each leg gets the same amount of time. That
 * only comes out as constant speed because ballTumblePath makes every leg the
 * same LENGTH; see the note there. Equal time over unequal distance was the
 * "starts fast then slows down" bug, and it lives in the interaction between
 * this rule and that function, not in either one alone.
 *
 * The rotation is a small wobble, not a full tumble, so the names stay readable
 * while they go past — reading them is half the point of this style.
 *
 * 0% and 100% share a waypoint so the loop is seamless; the negative delays in
 * the component start each ball part-way through its own cycle. */
@keyframes ballTumble {
  0%, 100% { transform: translate(var(--b0x, 71px), var(--b0y, 71px)) rotate(var(--r0, 0deg)); }
  12.5%    { transform: translate(var(--b1x, 71px), var(--b1y, 71px)) rotate(var(--r1, 0deg)); }
  25%      { transform: translate(var(--b2x, 71px), var(--b2y, 71px)) rotate(var(--r2, 0deg)); }
  37.5%    { transform: translate(var(--b3x, 71px), var(--b3y, 71px)) rotate(var(--r3, 0deg)); }
  50%      { transform: translate(var(--b4x, 71px), var(--b4y, 71px)) rotate(var(--r4, 0deg)); }
  62.5%    { transform: translate(var(--b5x, 71px), var(--b5y, 71px)) rotate(var(--r5, 0deg)); }
  75%      { transform: translate(var(--b6x, 71px), var(--b6y, 71px)) rotate(var(--r6, 0deg)); }
  87.5%    { transform: translate(var(--b7x, 71px), var(--b7y, 71px)) rotate(var(--r7, 0deg)); }
}

/* The winner growing out of the point every other ball collapsed into. */
@keyframes ballGrow {
  0% { transform: scale(0); }
  72% { transform: scale(1.07); }
  100% { transform: scale(1); }
}

/* Countdown digit: pop in with a slight overshoot, hold, then ease down a
   touch at the end of its beat so the next digit reads as a fresh strike
   rather than an abrupt swap. Fill mode holds the eased-down pose. */
@keyframes countPop {
  0% { opacity: 0; transform: scale(0.4); }
  22% { opacity: 1; transform: scale(1.08); }
  36% { transform: scale(1); }
  82% { opacity: 1; transform: scale(1); }
  100% { opacity: 0.85; transform: scale(0.92); }
}

/* Countdown ring: expands out from behind each digit and fades. */
@keyframes countRing {
  0% { opacity: 0.5; transform: scale(0.55); }
  100% { opacity: 0; transform: scale(1.6); }
}

/* Envelope anticipation: a twitch, a rest, a twitch.
 *
 * This used to be a single 0.5s shiver timed to finish just before the flap
 * opened. That was fine when the flap opened at 700ms, but the flap now opens
 * near the END of the window (the old timing left an open envelope sitting
 * there doing nothing for seconds), which would have left a sealed envelope
 * perfectly motionless for most of the reveal instead.
 *
 * So it loops, and it spends most of each cycle at rest. A periodic twitch
 * reads as something moving inside; a continuous wobble reads as a broken
 * transform. Every cycle starts and ends at rotate(0), so dropping the
 * animation when the flap opens can never snap. */
@keyframes envelopeStir {
  0%, 62%, 100% { transform: rotate(0deg); }
  68% { transform: rotate(-1.6deg); }
  74% { transform: rotate(1.4deg); }
  80% { transform: rotate(-1.1deg); }
  88% { transform: rotate(0.6deg); }
}

/* Wheel pointer: recoils as the wheel ticks into the winning slice. */
@keyframes pointerTick {
  0% { transform: rotate(0deg); }
  35% { transform: rotate(-16deg); }
  70% { transform: rotate(7deg); }
  100% { transform: rotate(0deg); }
}

@keyframes pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.6; transform: scale(0.95); }
}

@keyframes shrinkBar {
  from { transform: scaleX(1); }
  to { transform: scaleX(0); }
}

@keyframes dotDance {
  0%, 60%, 100% { opacity: 0.2; }
  30% { opacity: 1; }
}

/* The reduced-motion reveal landing on the winner's name.
 *
 * Opacity ONLY, and that is the whole specification. prefers-reduced-motion is
 * set for vestibular disorders, migraine and motion sickness, so the fallback
 * may not translate, rotate or scale anything — not even the gentle pop-in the
 * other styles land with. A cross-fade is the strongest reveal available that
 * costs the viewer nothing. */
@keyframes stillFadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Global focus-visible ring for keyboard navigation */
.rd-root button:focus-visible {
  outline: 2px solid #002E5D;
  outline-offset: 2px;
}

/* --- demo chrome (src/demo/reveal-demo.css) --- */
/*
 * Chrome for the standalone reveal demo — the picker, the stage, the controls.
 *
 * Only the surround. Nothing here styles the animations themselves: those come
 * from the app's own globals.css keyframes and Tailwind utilities, which the
 * build concatenates ahead of this file so what plays is what the product
 * plays. A rule here that reached inside `.rd-stage` would be the beginning of
 * the marketing site having its own opinion about the reveals, which is the one
 * thing this whole arrangement exists to prevent.
 *
 * Written against the marketing site's neobrutalist tokens (yellow/pink/blue,
 * hard 3px borders, offset shadows) rather than the app's navy, because the
 * page around it is simplyraffle.com. The preview panel keeps the app's navy
 * and Open Sans — that part is a screenshot of the product and should look like
 * one.
 */

.rd-root {
  --rd-dark: #111111;
  --rd-yellow: #FFE135;
  --rd-pink: #FF3D82;
  --rd-blue: #2563EB;
  --rd-white: #FFFFFF;

  display: grid;
  gap: 28px;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}

@media (min-width: 900px) {
  .rd-root {
    grid-template-columns: minmax(0, 420px) minmax(0, 1fr);
    gap: 36px;
  }
}

/* --- stage -------------------------------------------------------------- */

.rd-root .rd-stage {
  position: sticky;
  top: 24px;
}

@media (max-width: 899px) {
  .rd-root .rd-stage { position: static; }
}

/*
 * The preview panel is the product's own surface, so it keeps the product's
 * font. The marketing site is DM Sans / Outfit and the app is Open Sans; a
 * reveal set in the wrong face is a small lie about how it will look on the
 * night, and the split-flap board — which is monospace and sized per cell —
 * is visibly different between the two.
 */
.rd-root .rd-stage {
  font-family: 'Open Sans', Arial, Helvetica, sans-serif;
}

.rd-root .rd-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  margin-top: 16px;
  font-family: 'DM Sans', sans-serif;
}

.rd-root .rd-btn {
  font-family: 'Outfit', sans-serif;
  font-weight: 700;
  font-size: 15px;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  padding: 11px 20px;
  border: 3px solid var(--rd-dark);
  border-radius: 8px;
  background: var(--rd-white);
  color: var(--rd-dark);
  cursor: pointer;
  box-shadow: 3px 3px 0 var(--rd-dark);
  /* Touch target: the demo is going to be opened on phones from a QR code as
     often as on a laptop. */
  min-height: 44px;
  touch-action: manipulation;
  transition: transform 0.08s ease, box-shadow 0.08s ease;
}

.rd-root .rd-btn:active {
  transform: translate(3px, 3px);
  box-shadow: 0 0 0 var(--rd-dark);
}

.rd-root .rd-btn-primary {
  background: var(--rd-yellow);
}

.rd-root .rd-toggle {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-size: 15px;
  font-weight: 500;
  color: var(--rd-dark);
  cursor: pointer;
  min-height: 44px;
}

.rd-root .rd-select {
  min-height: 40px;
  padding: 6px 10px;
  font: inherit;
  color: var(--rd-dark);
  background: #fff;
  border: 2px solid var(--rd-dark);
  border-radius: 8px;
  cursor: pointer;
}

.rd-root .rd-select:focus-visible {
  outline: 3px solid var(--rd-pink);
  outline-offset: 2px;
}

.rd-root .rd-toggle input {
  width: 20px;
  height: 20px;
  accent-color: var(--rd-pink);
  cursor: pointer;
}

/* --- picker ------------------------------------------------------------- */

.rd-root .rd-picker {
  font-family: 'DM Sans', sans-serif;
}

.rd-root .rd-picker-heading {
  font-family: 'Outfit', sans-serif;
  font-weight: 900;
  text-transform: uppercase;
  font-size: 22px;
  margin: 0 0 14px;
  color: var(--rd-dark);
}

.rd-root .rd-tiles {
  display: grid;
  gap: 12px;
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 560px) {
  .rd-root .rd-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.rd-root .rd-tile {
  display: flex;
  flex-direction: column;
  gap: 5px;
  text-align: left;
  padding: 14px 15px;
  border: 3px solid var(--rd-dark);
  border-radius: 10px;
  background: var(--rd-white);
  cursor: pointer;
  box-shadow: 3px 3px 0 var(--rd-dark);
  min-height: 44px;
  touch-action: manipulation;
  transition: transform 0.08s ease, box-shadow 0.08s ease, background 0.12s ease;
}

.rd-root .rd-tile:hover { background: #FFFDF2; }

.rd-root .rd-tile:active {
  transform: translate(3px, 3px);
  box-shadow: 0 0 0 var(--rd-dark);
}

/*
 * Selected state carries a background AND an inset bar, not colour alone —
 * the tiles are a radio group in everything but markup, and "which one am I
 * watching" must survive a monochrome screen or a red-green deficiency.
 * aria-pressed carries it for assistive tech.
 */
.rd-root .rd-tile[data-selected] {
  background: var(--rd-yellow);
  box-shadow: 3px 3px 0 var(--rd-dark), inset 6px 0 0 var(--rd-pink);
}

.rd-root .rd-tile-label {
  font-family: 'Outfit', sans-serif;
  font-weight: 800;
  font-size: 16px;
  color: var(--rd-dark);
  display: flex;
  align-items: center;
  gap: 8px;
}

.rd-root .rd-tile-badge {
  font-family: 'DM Sans', sans-serif;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 2px 7px;
  border: 2px solid var(--rd-dark);
  border-radius: 999px;
  background: var(--rd-white);
}

.rd-root .rd-tile[data-selected] .rd-tile-badge { background: var(--rd-white); }

.rd-root .rd-tile-blurb {
  font-size: 14px;
  line-height: 1.4;
  color: #3f3f3f;
}

.rd-root .rd-tile[data-selected] .rd-tile-blurb { color: #2a2600; }

/*
 * The names-are-published caveat, and it is deliberately ON the tile rather
 * than in a footnote.
 *
 * Four of the eight styles put someone other than the winner on a public page,
 * and the product gates them behind an explicit setting. Someone comparing
 * tiles here is choosing the same thing the operator chooses in settings, and
 * the constraint belongs at the moment of the choice — a caveat further down
 * the page gets read after the decision, if at all.
 */
.rd-root .rd-tile-note {
  font-size: 12.5px;
  line-height: 1.4;
  font-weight: 600;
  color: #7a4a00;
  padding-top: 4px;
  border-top: 1px dashed rgba(17, 17, 17, 0.22);
}

.rd-root .rd-tile[data-selected] .rd-tile-note { color: #5a3600; }

.rd-root .rd-footnote {
  margin: 18px 0 0;
  font-size: 14px;
  line-height: 1.55;
  color: #4a4a4a;
}

/* --- focus -------------------------------------------------------------- */

.rd-root .rd-tile:focus-visible,
.rd-root .rd-btn:focus-visible,
.rd-root .rd-toggle input:focus-visible {
  outline: 3px solid var(--rd-blue);
  outline-offset: 3px;
}
