/* Self-hosted rather than loaded from Google. An overlay that reaches out to
 * a CDN at load renders in a fallback face whenever the network is slow or
 * down, and that shows up on stream as the title suddenly being the wrong
 * font. 64KB for both, latin subset only. */
@font-face {
  font-family: 'Bangers';
  src: url(fonts/bangers.woff2) format('woff2');
  font-weight: 400;
  font-display: block;
}

@font-face {
  font-family: 'Nunito';
  src: url(fonts/nunito.woff2) format('woff2');
  font-weight: 400 700;
  font-display: block;
}

:root {
  /* Third palette: 683558, 300101, 192E44, A74A4E, EBEBEB. Wedges cycle
     light, purple, teal, rose; purple doubles as the rim. */
  --k-purple: #683558;   /* the RIM, and wedge 2 */
  --k-ink:    #300101;   /* body, dividers, outer edge, dark text on light buttons */
  --k-teal:   #192E44;   /* wedge 3 */
  --k-rose:   #A74A4E;   /* wedge 4, accents, warnings */
  --k-light:  #EBEBEB;   /* wedge 1, ticks, light foreground text */

  --text: #EBEBEB;

  /* The scene's own width, mirrored here so things OUTSIDE the scene can be
     measured against it. The 48px is .stage's left+right padding, the 140px is
     the same allowance .scene subtracts from the viewport height, and the 16/9
     is its aspect ratio. If any of those change in .scene, they change here. */
  --scene-w: min(calc(100vw - 48px), calc((100vh - 140px) * 16 / 9));

  /* ---- the history column's width ---------------------------------------
   * Derived, never chosen. The scene is centred and its width is capped by the
   * viewport HEIGHT, so how much room is left beside it changes with the window
   * - and at 1920x1080, the size a browser source is usually set to, it is only
   * about 124px per side.
   *
   * That is why this is a calculation rather than a number. A fixed 240px
   * column would sit inside the keyed area at that size and go out on stream,
   * and it would look perfectly fine in a wide dev window while doing it.
   * max(0px, ...) is what makes the column vanish instead of overlapping when
   * the window is too narrow to have a margin at all.
   *
   * The outline is 9px because .scene's dashed edge is an `outline` - 6px of
   * offset plus a 3px stroke - which paints OUTSIDE the box and takes no layout
   * space, so CSS thinks the scene ends 9px before it visibly does. */
  --scene-outline: 9px;
  --hist-gap: 12px;
  --hist-inset: 10px;
  --side-space: max(0px, calc(
    (100vw - var(--scene-w)) / 2
    - var(--scene-outline) - var(--hist-gap) - var(--hist-inset)
  ));
  --hist-w: min(var(--side-space), 260px);

  /* THE CHROMA KEY BED. Green - re-measured for the third palette (683558,
     300101, 192E44, A74A4E, EBEBEB). OBS keys on distance in the UV (CbCr)
     chroma plane, not hue angle. Clearance to the nearest wheel colour,
     full RGB sweep at step 5; nothing beat pure green:

       green   #00ff00   0.534   <-- this one
       blue    #0000ff   0.444
       yellow  #ffff00   0.457
       magenta #ff00ff   0.443
       cyan    #00ffff   0.465
       red     #ff0000   0.339   -- rose (A74A4E) is a red, so it lost

     Redo this measurement on any palette change. */
  --key-color: #00ff00;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  /* Fixed-canvas overlay — scrolling is never intended. */
  overflow: hidden;
}

body {
  /* Dark, for the operator. This is outside the crop, so it never goes out;
     it is here so the controls and the history column have something readable
     to sit on. Only .scene's background is keyed. */
  background: var(--k-ink);
  color: var(--text);
  font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
  letter-spacing: 0.01em;
}

.stage {
  position: relative;
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  padding: 24px;
}

/* ---------------- Scene (the keyable 16:9 region) ----------------
 * Crop the OBS source to this area and key its background. Anything outside the
 * dashed outline (controls, history, padding) is the operator's and won't be on
 * stream.
 *
 * It defaults to the key colour, so the overlay is stream-ready the moment it
 * loads. It used to default to white and need a button press, which is a step
 * that only ever gets missed in one direction: nobody notices the wheel is
 * white until it is already on stream sitting in a white box.
 * ---------------------------------------------------------------- */

.scene {
  position: relative;
  aspect-ratio: 16 / 9;
  /* Pick whichever constraint is smaller so 16:9 always holds inside the viewport. */
  width: min(100%, calc((100vh - 140px) * 16 / 9));
  max-height: calc(100vh - 140px);
  background: var(--key-color);
  /* Was --k-light in the first palette - a dedicated accent colour, distinct
     from the general text colour. Plain white now instead, reusing --text:
     the page behind this outline is dark again, so anything light works,
     and white is the specific ask rather than a themed accent. */
  outline: 3px dashed var(--text);
  outline-offset: 6px;
  overflow: hidden;
  border-radius: 4px;
}

/* Review beds, cycled by the Bed button. Neither is what goes out - they exist
   so edges can be checked against something other than the key. White finds
   dark fringing and any pale wedge text; clear shows the page behind and is
   where light haloing shows. */
.scene.bed-white { background: #ffffff; }
.scene.bed-clear { background: transparent; }

/* ---------------- The drop group (everything that goes on stream) -----------
 * The wheel and its string, dropping in and snapping away as one thing.
 *
 * NOT called `.show`, which is what it was first, and which was a bug: the
 * result card's visible state is `.result.show`, so a bare `.show` rule matched
 * the card too and threw it 150% above the scene the moment it opened. The card
 * was opening correctly and landing off screen, which looked like the popup was
 * broken rather than like a selector collision. Keep this name distinct from any
 * state class.
 * -------------------------------------------------------------------------- */

.drop-group {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;

  /* Parked above the crop until Trigger Drop. The WHOLE group moves, string
     included, so hiding it leaves the scene as flat key colour with nothing left
     in frame to key around - not even a cord running off the top. */
  transform: translateY(-150%);
  /* Low, so a squash reads as the group landing on something rather than
     shrinking towards its own middle. */
  transform-origin: 50% 78%;
  will-change: transform;
}

/* The settled state the drop animation hands over to. */
.drop-group.down {
  transform: none;
}

.drop-group.dropping { animation: drop-in 1150ms both; }
.drop-group.hiding   { animation: snap-away 620ms both; }

/* Blood-demon effects: a shockwave ring off the rim on landing and on recoil. */
.wheel-frame::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 0.7vmin solid var(--k-rose);
  opacity: 0;
  pointer-events: none;
}
.drop-group.dropping .wheel-frame::after { animation: blood-ring 700ms ease-out 600ms both; }
.drop-group.hiding   .wheel-frame::after { animation: blood-ring 420ms ease-out both; }

@keyframes blood-ring {
  0%   { opacity: 0.95; transform: scale(1); }
  100% { opacity: 0;    transform: scale(1.45); }
}

/* Summoned: nothing falls. A runed blood-red summoning circle (.wheel-frame::before)
 * spins up around the spot, the wheel and chain spiral out of nothing inside
 * it with a turn and a flash, overshoot slightly, and the circle burns out. */
.drop-group.dropping { transform-origin: 50% 50%; }

@keyframes drop-in {
  0%   { transform: scale(0.04) rotate(-540deg); opacity: 0; filter: brightness(2.2); animation-timing-function: cubic-bezier(0.2, 0.7, 0.3, 1); }
  15%  { opacity: 1; }
  58%  { transform: scale(1.07) rotate(0deg);   opacity: 1; filter: brightness(1.6); animation-timing-function: ease-in-out; }
  100% { transform: scale(1) rotate(0deg);      opacity: 1; filter: brightness(1); }
}

.wheel-frame::before {
  content: '';
  position: absolute;
  inset: -20%;
  background: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20viewBox=%270%200%20100%20100%27%3E%3Ccircle%20cx=%2750%27%20cy=%2750%27%20r=%2749.2%27%20fill=%27none%27%20stroke=%27%23A74A4E%27%20stroke-width=%271.1%27/%3E%3Ccircle%20cx=%2750%27%20cy=%2750%27%20r=%2747.2%27%20fill=%27none%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.5%27/%3E%3Ccircle%20cx=%2750%27%20cy=%2750%27%20r=%2739.6%27%20fill=%27none%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.5%27/%3E%3Ccircle%20cx=%2750%27%20cy=%2750%27%20r=%2737.8%27%20fill=%27none%27%20stroke=%27%23A74A4E%27%20stroke-width=%271%27%20stroke-dasharray=%272.2%201.4%27/%3E%3Cpath%20d=%27M91.00%2050.00L96.00%2050.00M92.50%2051.60L95.00%2048.40M91.00%2050.00L92.50%2048.40%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M89.97%2059.12L94.85%2060.24M91.79%2057.90L92.54%2061.35M94.72%2058.56L95.20%2058.68M94.00%2061.68L95.20%2058.68%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M86.94%2067.79L91.44%2069.96M87.63%2066.35L89.89%2067.43%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M82.06%2075.56L85.96%2078.68M83.23%2076.50L86.18%2076.81%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M75.56%2082.06L78.68%2085.96M74.31%2083.05L78.37%2083.01M75.56%2082.06L77.75%2082.23M74.31%2083.05L77.75%2082.23%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M67.79%2086.94L69.96%2091.44M69.74%2090.99L68.52%2092.14M70.53%2088.95L71.40%2090.75%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M59.12%2089.97L60.24%2094.85M57.90%2091.79L58.23%2093.25M58.56%2094.72L58.68%2095.20M61.35%2092.54L58.56%2094.72%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M50.00%2091.00L50.00%2096.00M50.00%2094.00L48.40%2095.50%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M40.88%2089.97L39.76%2094.85M40.88%2089.97L38.76%2092.05%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M32.21%2086.94L30.04%2091.44M31.70%2091.69L31.48%2092.14M29.47%2088.95L31.48%2092.14M30.77%2086.25L29.68%2088.50%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M24.44%2082.06L21.32%2085.96M22.88%2086.57L20.07%2084.97M21.32%2083.40L22.88%2086.57%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M17.94%2075.56L14.04%2078.68M17.94%2075.56L17.77%2077.75M16.77%2076.50L13.82%2076.81%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M13.06%2067.79L8.56%2069.96M11.71%2068.44L8.76%2068.08%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M10.03%2059.12L5.15%2060.24M5.28%2058.56L4.80%2058.68%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M9.00%2050.00L4.00%2050.00M4.50%2051.60L4.00%2051.60M7.50%2051.60L6.00%2051.60%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M10.03%2040.88L5.15%2039.76M5.64%2039.88L5.51%2038.20%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M13.06%2032.21L8.56%2030.04M12.40%2030.12L10.15%2029.03%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M17.94%2024.44L14.04%2021.32M16.60%2021.32L13.43%2022.88M14.43%2021.63L15.03%2020.07M14.43%2021.63L15.03%2020.07%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M24.44%2017.94L21.32%2014.04M22.25%2017.77L23.82%2014.60%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M32.21%2013.06L30.04%208.56M33.65%2012.37L29.68%2011.50%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M40.88%2010.03L39.76%205.15M42.44%209.67L38.98%208.92%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M50.00%209.00L50.00%204.00M48.40%209.00L51.60%207.50M50.00%207.50L51.60%205.00%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M59.12%2010.03L60.24%205.15M59.12%2010.03L61.02%208.92M60.12%205.64L58.68%204.80%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M67.79%2013.06L69.96%208.56M69.23%2013.75L70.32%2011.50%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M75.56%2017.94L78.68%2014.04M75.25%2015.77L78.68%2016.60M77.75%2017.77L78.68%2016.60%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M82.06%2024.44L85.96%2021.32M83.05%2025.69L85.01%2024.13M82.23%2022.25L86.18%2023.19M82.23%2022.25L85.40%2023.82%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M86.94%2032.21L91.44%2030.04M88.99%2033.00L90.34%2032.35M88.95%2029.47L91.69%2031.70M90.30%2028.82L92.14%2031.48%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Cpath%20d=%27M89.97%2040.88L94.85%2039.76M92.54%2038.65L94.49%2038.20M89.97%2040.88L91.08%2038.98%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27%20stroke-linecap=%27round%27%20fill=%27none%27/%3E%3Ccircle%20cx=%2793.23%27%20cy=%2754.87%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2791.06%27%20cy=%2764.37%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2786.83%27%20cy=%2773.14%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2780.76%27%20cy=%2780.76%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2773.14%27%20cy=%2786.83%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2764.37%27%20cy=%2791.06%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2754.87%27%20cy=%2793.23%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2745.13%27%20cy=%2793.23%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2735.63%27%20cy=%2791.06%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2726.86%27%20cy=%2786.83%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2719.24%27%20cy=%2780.76%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2713.17%27%20cy=%2773.14%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%278.94%27%20cy=%2764.37%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%276.77%27%20cy=%2754.87%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%276.77%27%20cy=%2745.13%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%278.94%27%20cy=%2735.63%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2713.17%27%20cy=%2726.86%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2719.24%27%20cy=%2719.24%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2726.86%27%20cy=%2713.17%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2735.63%27%20cy=%278.94%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2745.13%27%20cy=%276.77%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2754.87%27%20cy=%276.77%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2764.37%27%20cy=%278.94%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2773.14%27%20cy=%2713.17%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2780.76%27%20cy=%2719.24%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2786.83%27%20cy=%2726.86%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2791.06%27%20cy=%2735.63%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Ccircle%20cx=%2793.23%27%20cy=%2745.13%27%20r=%27.35%27%20fill=%27%23A74A4E%27/%3E%3Cpath%20d=%27M50.00%2014.00L71.16%2079.12L15.76%2038.88L84.24%2038.88L28.84%2079.12L50.00%2014.00%27%20fill=%27none%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.9%27%20stroke-linejoin=%27round%27/%3E%3Ccircle%20cx=%2750.00%27%20cy=%2714.00%27%20r=%272.1%27%20fill=%27%23300101%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27/%3E%3Ccircle%20cx=%2784.24%27%20cy=%2738.88%27%20r=%272.1%27%20fill=%27%23300101%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27/%3E%3Ccircle%20cx=%2771.16%27%20cy=%2779.12%27%20r=%272.1%27%20fill=%27%23300101%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27/%3E%3Ccircle%20cx=%2728.84%27%20cy=%2779.12%27%20r=%272.1%27%20fill=%27%23300101%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27/%3E%3Ccircle%20cx=%2715.76%27%20cy=%2738.88%27%20r=%272.1%27%20fill=%27%23300101%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.7%27/%3E%3Ccircle%20cx=%2750%27%20cy=%2750%27%20r=%2714%27%20fill=%27none%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.6%27/%3E%3Ccircle%20cx=%2750%27%20cy=%2750%27%20r=%2711.5%27%20fill=%27none%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.4%27%20stroke-dasharray=%271%201%27/%3E%3Cpath%20d=%27M50.00%2037.00L61.26%2056.50L38.74%2056.50ZM50.00%2063.00L38.74%2043.50L61.26%2043.50Z%27%20fill=%27none%27%20stroke=%27%23A74A4E%27%20stroke-width=%27.5%27/%3E%3C/svg%3E") center / 100% 100% no-repeat;
  filter: drop-shadow(0 0 3px rgba(167, 74, 78, 0.9));
  opacity: 0;
  pointer-events: none;
}
.drop-group.dropping .wheel-frame::before { animation: summon-circle 1150ms ease-out both; }

@keyframes summon-circle {
  0%   { opacity: 0;    transform: scale(0.5) rotate(0deg); }
  25%  { opacity: 0.95; transform: scale(1)   rotate(90deg); }
  75%  { opacity: 0.9;  transform: scale(1.05) rotate(240deg); }
  100% { opacity: 0;    transform: scale(1.3) rotate(320deg); }
}

/* Recoil, then yank. The wheel convulses on its chain, squats, and is hauled
 * up out of frame - the shudder is what makes it read as being dragged away
 * rather than deleted. */
@keyframes snap-away {
  0%   { transform: translate(0, 0) rotate(0) scale(1, 1);            animation-timing-function: ease-in-out; }
  8%   { transform: translate(-1.2%, 0) rotate(-0.9deg);              animation-timing-function: ease-in-out; }
  16%  { transform: translate(1.2%, 0) rotate(0.9deg);               animation-timing-function: ease-in-out; }
  24%  { transform: translate(-1.6%, 0) rotate(-1.1deg);             animation-timing-function: ease-in-out; }
  32%  { transform: translate(0, 4%) rotate(0) scale(1.1, 0.88);     animation-timing-function: ease-in; }
  50%  { transform: translate(0, -14%) scale(0.94, 1.12);            animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5); }
  100% { transform: translate(0, -160%) scale(0.78, 1.38); }
}

/* ---------------- Wheel ---------------- */

.wheel-frame {
  position: relative;
  /* Driven off the scene's HEIGHT, not its width. The scene is 16:9 and the
     wheel is the tallest thing in it, so height is the constraint that binds -
     sizing on width would let the wheel grow past the crop on a wide window. */
  height: min(64%, 64vh);
  aspect-ratio: 1;
  border-radius: 50%;
  /* Where it hangs from: the point at which the cord LEAVES FRAME, not the far
     end of the string element.

     This was -197.5% (the string's actual end, far off screen) and it looked
     wrong. With a lever that long, the rotation needed to move the wheel a
     visible distance tilts the visible run of cord by almost nothing - so the
     cord stayed vertical and the wheel appeared to slide sideways rather than
     swing under it.

     -28.125% is the top edge of .scene, exactly. The frame is height:min(64%,
     64vh) and .scene is always shorter than the viewport, so 64% always wins and
     the gap above the wheel is a fixed 18% of the scene = 28.125% of the frame's
     own height. The cord is clipped there, so pivoting at that line means it
     appears anchored precisely where it vanishes, and now visibly tilts. */
  transform-origin: 50% -28.125%;
  /* The design's warm bloom.
   *
   * WORTH KNOWING ON STREAM: this is a soft, semi-transparent glow, and it sits
   * on the chroma key. Wherever it is partly transparent the key colour shows
   * through it, so OBS keys some of the glow away and can leave a fringe. It
   * looks correct on the white review bed and against a dark scene; judge it on
   * the green one before going live, and drop this one declaration if it
   * fringes. */
  box-shadow: 0 0 44px 6px rgba(167, 74, 78, 0.35);  /* --k-rose's RGB */
}

/* ---- The sway ----
 *
 * TWO keyframes, extreme to extreme, with `alternate` - not four keyframes
 * around a full cycle.
 *
 * This matters more than it looks. A keyframe timing function applies BETWEEN
 * each pair of keyframes, so the first version of this - 0 -> +A -> 0 -> -A -> 0
 * with ease-in-out - eased at every one of those points, including the two where
 * the angle is 0. That put the slowest moment at the BOTTOM of the arc and the
 * quickest near the ends, which is precisely backwards: a pendulum is fastest as
 * it passes through the bottom and momentarily still at each end. It read as a
 * hesitation in the middle followed by a lurch.
 *
 * Written as a single sweep from one extreme to the other, `ease-in-out` lands
 * where it should on its own - slow at both ends, quick through the middle -
 * because the ends of the swing are now the ends of the segment. One iteration
 * is half a cycle, so the period below is half the full swing.
 *
 * The negative delay starts it mid-sweep at 0deg rather than parked at one
 * extreme, so it picks up from where the drop leaves the wheel instead of
 * snapping sideways the instant the animation is allowed to run. Half of the
 * duration, because ease-in-out is symmetric and its midpoint is the midpoint of
 * the travel.
 *
 * On .wheel-frame rather than .drop-group because .drop-group's transform is
 * already spoken for by the drop and the snap-away. Two animations cannot share
 * one property; nesting them costs nothing.
 *
 * The spin is unaffected: .wheel rotates inside this frame, and the pointer
 * sways WITH the frame, so which wedge sits under the pointer never changes.
 * winningIndex reads the rotation figure rather than the DOM, so it could not be
 * confused by the sway even if that were not true. */
.drop-group.down:not(.dropping):not(.hiding) .wheel-frame {
  animation: sway 2.9s ease-in-out infinite alternate;
  animation-delay: -1.45s;
  will-change: transform;
}

/* While spinning: the wheel throbs like a heartbeat and the pointer rattles. */
.drop-group.down.spinning:not(.dropping):not(.hiding) .wheel-frame {
  animation: sway 2.9s ease-in-out -1.45s infinite alternate,
             heartbeat 0.8s ease-in-out infinite;
}
.drop-group.spinning .pointer { animation: pointer-rattle 0.16s linear infinite; transform-origin: 50% 20%; }

@keyframes heartbeat {
  0%, 100% { box-shadow: 0 0 30px 4px rgba(167, 74, 78, 0.3); }
  18%      { box-shadow: 0 0 64px 18px rgba(167, 74, 78, 0.75); }
  36%      { box-shadow: 0 0 36px 6px rgba(167, 74, 78, 0.35); }
  54%      { box-shadow: 0 0 56px 14px rgba(167, 74, 78, 0.6); }
}
@keyframes pointer-rattle {
  0%, 100% { transform: translate(-50%, -26%) rotate(-7deg); }
  50%      { transform: translate(-50%, -26%) rotate(7deg); }
}

/* The period is deliberately NOT reduced along with the angle. A pendulum's
   period is set by its length, not by how far it swings, so a smaller arc over
   the same 2.9s is what actually happens - it just moves more slowly. Speeding
   it up to match the smaller travel is what would make it look wrong. */
@keyframes sway {
  from { transform: rotate(-0.65deg); }
  to   { transform: rotate(0.65deg); }
}

/* The suspension string. Runs up from behind the pointer and off the top of the
 * frame, so the wheel reads as hung from something rather than floating.
 *
 * A sibling of .wheel, not a child - a string that rotated with the face would
 * wind itself round the rim. */
.suspension {
  position: absolute;
  left: 50%;
  /* Bottom edge 2.5% BELOW the frame's top, which puts it inside the pointer's
     body: the pointer paints over it (z-index 3 against 1) and hides the join,
     so the cord appears to end at the triangle rather than behind it. */
  bottom: 97.5%;
  /* Far longer than the gap above the wheel ever is - the frame is at most 64%
     of the scene, so that gap is only about 28% of the frame's own height.
     .scene clips the excess, so the string always leaves frame rather than
     stopping in mid air, including at the top of the drop's bounce. */
  height: 200%;
  transform: translateX(-50%);
  /* A hanging iron chain: alternating front-on links and side-on links. */
  width: 2.6%;
  background: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20viewBox=%270%200%2024%2048%27%3E%3Crect%20x=%274%27%20y=%272%27%20width=%2716%27%20height=%2726%27%20rx=%278%27%20fill=%27none%27%20stroke=%27%23A74A4E%27%20stroke-width=%273.5%27/%3E%3Crect%20x=%274%27%20y=%272%27%20width=%2716%27%20height=%2726%27%20rx=%278%27%20fill=%27none%27%20stroke=%27%23300101%27%20stroke-width=%271%27%20opacity=%27.6%27/%3E%3Crect%20x=%279.5%27%20y=%2724%27%20width=%275%27%20height=%2722%27%20rx=%272.5%27%20fill=%27%23683558%27%20stroke=%27%23300101%27%20stroke-width=%271%27/%3E%3C/svg%3E") center top / 100% auto repeat-y;
  filter: drop-shadow(0 0 4px rgba(167, 74, 78, 0.55));
  z-index: 1;
}

/* The only thing that rotates. */
.wheel {
  width: 100%;
  height: 100%;
  border-radius: 50%;
}

.wheel svg {
  width: 100%;
  height: 100%;
  display: block;
}

.wheel svg text {
  pointer-events: none;
  user-select: none;
}

/* The hub, a sibling of .wheel rather than a child, so it does not turn. Its
   size is bounded by LABEL_INNER in script.js: the labels stop at 26 of the
   viewBox's 114 units, which is 22.8% of the face, so anything under that
   clears them. */
.hub {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 18%;
  height: 18%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, var(--k-purple) 0%, var(--k-ink) 72%);
  border: 0.5vmin solid var(--k-light);
  box-shadow: 0 0 0 0.25vmin var(--k-ink);
  display: grid;
  place-items: center;
  z-index: 2;
}

/* The gold pupil at dead centre. */
.hub::after {
  content: '';
  width: 24%;
  height: 24%;
  border-radius: 50%;
  background: var(--k-light);
  box-shadow: 0 0 0 0.2vmin var(--k-ink);
}

/* The pointer, also a sibling: one that rotated would never leave the wedge it
   started on. Two stacked pseudo-elements rather than a stroke, because
   clip-path has no outline of its own - the ink one is drawn slightly larger
   and behind, which is what reads as the outline. */
.pointer {
  position: absolute;
  top: 0;
  left: 50%;
  width: 6.5%;
  aspect-ratio: 1 / 1.2;
  transform: translate(-50%, -26%);
  z-index: 3;
}

.pointer::before,
.pointer::after {
  content: '';
  position: absolute;
  inset: 0;
  clip-path: polygon(50% 100%, 0% 40%, 14% 0%, 86% 0%, 100% 40%);
}

.pointer::before {
  background: var(--k-ink);
  transform: scale(1.2);
}

.pointer::after {
  background: var(--k-light);
}

/* ---------------- History (operator only) ----------------
 * In the margin to the LEFT of the scene, never inside it. Absolutely
 * positioned rather than placed in .stage's flow, so it cannot push the scene
 * off centre or change its size - the OBS crop is framed against that scene,
 * and a panel that resized it would move the crop every time a spin was logged.
 * -------------------------------------------------------- */

.history {
  position: absolute;
  left: var(--hist-inset);
  top: 50%;
  transform: translateY(-50%);
  width: var(--hist-w);
  max-height: 78vh;
  /* Collapses to nothing rather than overlapping when the window is too narrow
     to have a margin. No border or padding on the box itself, for the same
     reason: at width 0 they would still paint a sliver. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 6px;
  color: var(--text);
}

.history-title {
  margin: 0;
  font-family: Nunito, Segoe UI, system-ui, sans-serif;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.24em;
  text-indent: 0.24em;
  text-transform: uppercase;
  color: var(--k-light);  /* back to gold now ink is dark again - 12.56:1 */
  opacity: 0.8;
}

.history-list {
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-family: Nunito, Segoe UI, system-ui, sans-serif;
  font-size: 10px;
  line-height: 1.3;
}

.history-list li {
  display: flex;
  gap: 6px;
  padding: 3px 6px;
  border-radius: 4px;
  background: rgba(235, 235, 235, 0.06);  /* --k-light's RGB, back to a light tint on a dark body */
  border-left: 2px solid var(--k-purple);
}

/* Newest first, so the one that just landed is the one at the top. */
.history-list li:first-child {
  border-left-color: var(--k-light);
  background: rgba(235, 235, 235, 0.1);  /* --k-light's RGB */
}

.history-when {
  flex: none;
  color: var(--k-light);  /* back to pink now ink is dark again - 16.64:1 */
  opacity: 0.65;
  font-variant-numeric: tabular-nums;
}

.history-what {
  /* Long entries wrap rather than being clipped: the point of the column is
     reading back what came up, and a truncated label can be ambiguous between
     two entries that share a prefix. */
  min-width: 0;
  overflow-wrap: anywhere;
}

.history-empty {
  margin: 0;
  font-family: Nunito, Segoe UI, system-ui, sans-serif;
  font-size: 10px;
  opacity: 0.4;
}

.history-empty[hidden] { display: none; }

/* ---------------- Controls ---------------- */

.controls {
  display: flex;
  gap: 14px;
  z-index: 10;
}

.btn {
  font: inherit;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 12px 24px;
  border-radius: 10px;
  border: none;
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.2s ease, background 0.2s ease, opacity 0.2s ease;
  background: linear-gradient(180deg, var(--k-light) 0%, var(--k-rose) 100%);
  /* --k-purple, not --text: --text is for the (dark) ink body, but this
     button's own background is light (pink -> orange), so it needs the
     opposite. Pink: 12.28:1, orange: 5.25:1 - both pass against plum. */
  color: var(--k-ink);
  /* Shadow re-derived from --k-rose (×0.5) rather than carried over as
     the old orange-red - independent of ink's colour either way. */
  box-shadow: 0 4px 14px rgba(84, 37, 39, 0.45);
}

/* The same control, as a link. An anchor does not inherit the button reset, so
   it needs the layout and the underline suppressed to sit level with the rest. */
.btn-link {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

.btn:hover:not(:disabled) {
  transform: translateY(-1px);
  /* Re-derived the same way the original pair was built: the resting
     gradient's own colours, darkened (~12%/15%) for a heavier hover weight,
     rather than two unrelated hand-picked hex values. */
  background: linear-gradient(180deg, #d0d0d0 0%, #8e3f43 100%);
  box-shadow: 0 6px 20px rgba(84, 37, 39, 0.6);
}

.btn:active:not(:disabled) {
  transform: translateY(0);
}

.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* Suppress the default focus ring on mouse click, keep it for keyboard nav. */
.btn:focus { outline: none; }
.btn:focus-visible {
  outline: 2px solid var(--k-light);  /* back to gold now the page behind it is dark again - 12.56:1 */
  outline-offset: 2px;
}

/* ---------------- Result pop-out ----------------
 * Covers the whole of .scene — which is the region OBS crops to, so the card
 * lands over the wheel on stream rather than beside it. It stays up until the
 * close button is pressed: the wheel is not the readout at 43 entries (a wedge
 * is 8.4deg wide and its label is squeezed to fit), so the card is what chat
 * actually reads, and a card that faded on its own would be gone before it had
 * been read out.
 *
 * No scrim: the wheel behind stays at full brightness, and the card carries its
 * own background and border so it reads against whatever it lands on. The layer
 * still spans the scene rather than hugging the card, because that is what
 * swallows clicks on the wheel while the result is up.
 * ------------------------------------------------ */

.result {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 4%;
  opacity: 0;
  /* Non-interactive while hidden, so it never swallows a click on the wheel. */
  pointer-events: none;
  transition: opacity 240ms ease;
  z-index: 10;
}

.result.show {
  opacity: 1;
  pointer-events: auto;
}

.result-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: clamp(28px, 4.2vw, 56px) clamp(34px, 6vw, 82px) clamp(30px, 4vw, 52px);
  max-width: 100%;
  /* Blood-demon card: near-black maroon with a rose-red edge, cut in at the
     corners and torn along the bottom, with blood dripping off the top (the
     ::before strip). clip-path would eat an ordinary border, so the edge is an
     inset shadow. */
  background:
    radial-gradient(ellipse at 50% 0%, rgba(167, 74, 78, 0.45) 0%, transparent 62%),
    linear-gradient(180deg, #3d0606 0%, var(--k-ink) 55%, #190000 100%);
  box-shadow: inset 0 0 0 3px var(--k-rose), inset 0 0 40px rgba(0, 0, 0, 0.6);
  clip-path: polygon(
    0 0, 100% 0, 100% 88%,
    94% 96%, 86% 90%, 77% 100%, 68% 91%, 58% 98%, 49% 90%, 40% 99%,
    31% 91%, 21% 98%, 12% 90%, 5% 97%, 0 88%);
  filter: drop-shadow(0 0 22px rgba(167, 74, 78, 0.45));
  text-align: center;
  transform: scale(0.92);
  transition: transform 240ms cubic-bezier(0.34, 1.45, 0.55, 1);
}

/* The dripping blood. Grows down from the top edge when the card appears. */
.result-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: clamp(30px, 4.2vw, 56px);
  background: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20viewBox=%270%200%20400%2052%27%20preserveAspectRatio=%27none%27%3E%3Cpath%20d=%27M0%200H400V10L363%2010C363%2031.599999999999998%20359.8%2046%20355%2046C350.2%2046%20347%2031.599999999999998%20347%2010L321%2010C321%2020.799999999999997%20316.6%2028%20310%2028C303.4%2028%20299%2020.799999999999997%20299%2010L269%2010C269%2029.2%20266.2%2042%20262%2042C257.8%2042%20255%2029.2%20255%2010L225%2010C225%2024.4%20221.0%2034%20215%2034C209.0%2034%20205%2024.4%20205%2010L178%2010C178%2034.0%20174.8%2050%20170%2050C165.2%2050%20162%2034.0%20162%2010L132%2010C132%2019.6%20127.2%2026%20120%2026C112.8%2026%20108%2019.6%20108%2010L82%2010C82%2030.4%2079.2%2044%2075%2044C70.8%2044%2068%2030.4%2068%2010L40%2010C40%2023.2%2036.0%2032%2030%2032C24.0%2032%2020%2023.2%2020%2010L0%2010Z%27%20fill=%27%23A74A4E%27/%3E%3C/svg%3E") top / 100% 100% no-repeat;
  transform: scaleY(0);
  transform-origin: top;
  pointer-events: none;
}

.result.show .result-card::before {
  transform: scaleY(1);
  transition: transform 900ms cubic-bezier(0.3, 0.8, 0.3, 1) 120ms;
}

.result.show .result-card {
  transform: scale(1);
}

.result-label {
  font-family: Bangers, Impact, Haettenschweiler, sans-serif;
  font-weight: 400;
  font-size: clamp(20px, 3.6vw, 46px);
  line-height: 1.15;
  letter-spacing: 0.1em;
  text-indent: 0.1em;
  color: var(--k-light);
  text-shadow: 0 2px 0 #190000, 0 0 18px rgba(167, 74, 78, 0.7);
  text-wrap: balance;
}

/* Sits on the card's corner rather than inside the text flow, so a long entry
   wrapping to three lines cannot push it off. */
.result-close {
  position: absolute;
  top: 8px;
  right: 12px;
  width: clamp(28px, 2.6vw, 40px);
  height: clamp(28px, 2.6vw, 40px);
  display: grid;
  place-items: center;
  padding: 0;
  font-size: clamp(20px, 2vw, 28px);
  line-height: 1;
  font-family: inherit;
  color: var(--k-light);
  background: transparent;
  border: 2px solid var(--k-light);
  border-radius: 50%;
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease, transform 140ms ease;
}

.result-close:hover {
  background: var(--k-rose);
  border-color: var(--k-rose);
  /* --k-purple, not --text: this button's hover background is --k-rose,
     light enough that --text (which is for the dark ink body) fails against
     it at 2.81:1. Same reasoning as .btn's text colour above. */
  color: var(--k-ink);
  transform: scale(1.08);
}

.result-close:focus { outline: none; }
.result-close:focus-visible {
  outline: 2px solid var(--k-light);
  outline-offset: 3px;
}

/* The winning wedge, so the card and the wheel can be checked against each
   other. The wedge itself only brightens - the outline is a separate path that
   script.js appends last.

   An SVG stroke straddles the edge it is drawn on, half inside the shape and
   half outside, and wedges are painted in order - so a stroke on the wedge had
   its outer half covered by the NEXT wedge on one side and not the other, and
   came out visibly thicker on one edge than the other. */
.seg.win { filter: brightness(1.15); }

.seg-outline {
  fill: none;
  /* Was a hardcoded near-white (#fff1dc), chosen to pop against the old
     (darker) wedge fills. All four new wedge fills are light-to-mid toned,
     so a light outline would blend into them - ink, now a true black, has
     reliable contrast against all four (7.1:1 to 16.6:1) regardless of
     whatever the body/page background happens to be, since this is purely
     about the wedges themselves. */
  stroke: var(--k-ink);
  stroke-width: 2.5;
  stroke-linejoin: round;
  pointer-events: none;
}


/* The operator's warning strip. Outside .scene, so it is never on stream. */
.notice {
  max-width: var(--scene-w);
  padding: 10px 14px;
  border-radius: 8px;
  border: 1px solid var(--k-light);
  background: rgba(235, 235, 235, 0.12);  /* --k-light's RGB, a light tint on the dark body */
  color: var(--text);
  font-size: 13px;
  line-height: 1.4;
}

.notice[hidden] { display: none; }
/* The pinned zone, beside the History heading. Dimmer than the heading: it is a
   qualifier on the times below, not a second heading. */
.history-zone {
  opacity: 0.6;
  letter-spacing: 0.12em;
}
