/* daily-wheel.css: Sparky's Daily Wheel. */
/* ============================================================
   SPARKY'S DAILY WHEEL  (templates/game/daily_wheel.html)

   "Void-forged brass": Sparky's rig is genuinely a salvage machine (brass,
   oxidized copper, stamped iron, warm bulbs) but it is not a second design
   system. Everything around the machine (panels, type, chips, tables) uses the
   site tokens, and the jackpot tier is Star-Steel, a pale luminous teal keyed
   to --color-primary, rather than the gold-chrome a real fairground wheel
   would use. That is what keeps the wheel legibly part of Starforge instead of
   a steampunk page someone bolted onto it, and it gives the jackpot a colour
   that cannot be confused with the amber Golden Gears.

   The metals live as scoped custom properties on .wheel-shop so the two light
   themes can retune them in one block at the bottom of this section rather
   than overriding thirty rules.

   Geometry note: the wedges, labels and pegs are all positioned from
   --wheel-count, which the template writes from the live plate count. Staff
   deactivating a plate in admin gives an 11-wedge wheel, not a broken one.
   ============================================================ */

.wheel-shop {
    /* ---- Salvage metals ---- */
    --brass: #b0813c;
    --brass-lit: #e0b061;
    --brass-dark: #5f4319;
    --copper: #bd6a35;
    --copper-lit: #e08a51;
    --iron: #48413d;
    --iron-lit: #6b615b;
    --starsteel: #7fd8ca;
    --starsteel-lit: #d9f7f1;
    --gold-gear: #f5b426;
    --gold-gear-lit: #ffdc7a;

    --rig-shell: #241d1a;
    --rig-shell-lit: #3c312b;
    --rig-edge: #14100e;
    --bulb: #ffd79a;
    --seam: rgba(0, 0, 0, 0.55);
    --plate-ink: #17110d;

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

/* ---------- Sparky ---------- */
.wheel-shop__crew { display: flex; flex-direction: column; gap: 16px; min-width: 0; }

.sparky { margin: 0; }
.sparky__frame {
    position: relative;
    /* The stencil placeholder sizes itself in cqw so it stays legible in both
       the full portrait slot and the 92px bust the mobile layout collapses it
       to. Fixed rem sizes cropped it to "PARK / GEARS" on a phone, which reads
       as a broken image rather than a placeholder. Real art needs none of this:
       object-fit crops the same photo to a bust on its own. */
    container-type: inline-size;
    aspect-ratio: 3 / 4;
    border-radius: var(--radius-lg);
    overflow: hidden;
    background:
        linear-gradient(160deg, var(--rig-shell-lit) 0%, var(--rig-shell) 55%, var(--rig-edge) 100%);
    border: 1px solid var(--rig-edge);
    box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.sparky__art { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Placeholder until the character plates land (personas/registry.py::art_ready).
   Deliberately a stencilled workshop panel rather than an empty frame or a
   "missing image" box: an unfinished slot should still look like part of the
   machine. */
.sparky__stencil {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    background-image:
        repeating-linear-gradient(135deg,
            rgba(255, 255, 255, 0.028) 0 12px,
            transparent 12px 24px);
    color: color-mix(in srgb, var(--brass-lit) 72%, transparent);
}
.sparky__stencil-mark {
    font-size: 17cqw;
    line-height: 1;
    opacity: 0.55;
    animation: sparky-cog 14s linear infinite;
}
.sparky__stencil-name {
    font-family: var(--font-display);
    font-size: 9.5cqw;
    font-weight: 800;
    letter-spacing: 0.16em;
    text-indent: 0.16em;
    color: var(--brass-lit);
}
.sparky__stencil-sub {
    font-family: var(--font-mono);
    font-size: 3.9cqw;
    letter-spacing: 0.38em;
    text-indent: 0.38em;
    color: color-mix(in srgb, var(--brass-lit) 60%, transparent);
}
/* The name/sub are now the persona's own name and role (data-driven, any length),
   so keep a long one wrapping and centered inside the plate rather than clipping
   or overrunning the rivets. */
.sparky__stencil { padding-inline: 7%; }
.sparky__stencil-name,
.sparky__stencil-sub { max-width: 100%; text-align: center; overflow-wrap: anywhere; }
/* Four corner rivets, drawn as one element so the placeholder needs no extra
   markup it will lose the moment real art arrives. */
.sparky__stencil-rivets {
    position: absolute;
    inset: 12px;
    border-radius: var(--radius-md);
    border: 1px dashed color-mix(in srgb, var(--brass) 40%, transparent);
    background-image:
        radial-gradient(circle, var(--brass) 38%, transparent 40%),
        radial-gradient(circle, var(--brass) 38%, transparent 40%),
        radial-gradient(circle, var(--brass) 38%, transparent 40%),
        radial-gradient(circle, var(--brass) 38%, transparent 40%);
    background-size: 7px 7px;
    background-position: 4px 4px, calc(100% - 4px) 4px, 4px calc(100% - 4px), calc(100% - 4px) calc(100% - 4px);
    background-repeat: no-repeat;
}
@keyframes sparky-cog { to { transform: rotate(360deg); } }
@container (max-width: 150px) {
    .sparky__stencil-name,
    .sparky__stencil-sub { display: none; }
    .sparky__stencil-mark { font-size: 34cqw; }
    .sparky__stencil-rivets { inset: 6px; }
}

.sparky__plate {
    display: flex;
    flex-direction: column;
    gap: 1px;
    margin-top: 10px;
    padding: 9px 12px;
    border-radius: var(--radius-sm);
    background: var(--bg-subtle);
    border: 1px solid var(--border-color);
    border-left: 3px solid var(--brass);
}
.sparky__name { font-family: var(--font-heading); font-weight: 700; }
.sparky__role {
    font-family: var(--font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Speech: a shop-floor note, not a chat bubble. */
.sparky-speech {
    position: relative;
    padding: 13px 15px;
    border-radius: var(--radius-md);
    background: var(--bg-surface);
    border: 1px solid var(--border-color);
    border-top: 2px solid color-mix(in srgb, var(--brass) 60%, var(--border-color));
    min-height: 76px;
}
.sparky-speech__line { margin: 0; color: var(--text-secondary); font-size: 0.93rem; }

/* ---------- The Overdrive gauge ----------
   --pressure (0-100) and --redline (0-100) are server-rendered from the same
   numbers the readout below states in words, so the needle can never disagree
   with the sentence beside it. */
.boiler {
    position: relative;
    padding: 14px 16px 16px;
    border-radius: var(--radius-md);
    background: var(--bg-subtle);
    border: 1px solid var(--border-color);
}
.wheel-rig__assembly {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    width: 100%;
    max-width: 520px;
    border: 1px solid var(--rig-edge);
    border-radius: var(--radius-lg);
    background: linear-gradient(160deg, var(--rig-shell-lit), var(--rig-shell) 58%, var(--rig-edge));
}
.wheel-rig__assembly > .wheel-rig {
    width: 100%;
    max-width: 520px;
    min-width: 0;
}
/* The lever is part of the machine's primary action bay. Keeping it between
   the wheel and the secondary console makes the action reachable before a
   player has to study the streak system. */
.wheel-rig__assembly > .wheel-lever {
    align-self: stretch;
    padding: 14px 12px 18px;
    border-top: 1px solid var(--rig-edge);
}
/* At the default width the pressure console remains attached below the action
   bay, so the wheel keeps its full size and the lever stays in the first
   viewport. */
.wheel-rig__assembly > .boiler {
    width: auto;
    max-width: none;
    margin: 0;
    border-color: var(--rig-edge);
    border-width: 1px 0 0;
    border-style: solid;
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
    background:
        linear-gradient(160deg,
            color-mix(in srgb, var(--rig-shell-lit) 22%, var(--bg-subtle)),
            var(--bg-subtle) 42%);
}
.boiler__connector {
    position: absolute;
    left: 50%;
    top: -14px;
    width: 28px;
    height: 14px;
    transform: translateX(-50%);
    border: 1px solid var(--rig-edge);
    border-bottom: 0;
    border-radius: 6px 6px 0 0;
    background: linear-gradient(to right, var(--copper), var(--copper-lit) 45%, var(--copper));
    z-index: 1;
}
/* A wide desktop has enough room to make the gauge a side-mounted console
   without shrinking the wheel. The same outer shell and shared border keep it
   reading as one machine, while the connector rotates into a short side pipe. */
@media (min-width: 1280px) {
    .wheel-rig__assembly {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(210px, 240px);
        grid-template-areas:
            "wheel gauge"
            "lever gauge";
        align-items: stretch;
        column-gap: 0;
        row-gap: 0;
        max-width: 780px;
    }
    .wheel-rig__assembly > .wheel-rig { grid-area: wheel; }
    .wheel-rig__assembly > .wheel-lever { grid-area: lever; }
    .wheel-rig__assembly > .boiler {
        grid-area: gauge;
        border-width: 0 0 0 1px;
        border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
    }
    .wheel-rig__assembly > .boiler .boiler__connector {
        left: -14px;
        top: 50%;
        width: 14px;
        height: 28px;
        transform: translateY(-50%);
        border-width: 1px 0 1px 1px;
        border-radius: 6px 0 0 6px;
    }
}
.boiler__heading {
    font-family: var(--font-mono);
    font-size: 0.7rem;
    font-weight: 400;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin: 0 0 10px;
}
.boiler__dial {
    position: relative;
    width: 100%;
    max-width: 190px;
    margin: 0 auto 16px;
    aspect-ratio: 2 / 1;
    overflow: visible;
}
/* The dial face: a half-annulus. The red zone is a conic slice of the same
   ring, starting exactly at --redline. */
/* Every gradient on the dial is centred at 50% 100%, the needle's pivot. A bare conic-gradient
   centres on the box middle, which cut the arc off at half height and put the red zone on a
   different centre from the needle (the gauge looked detached from its arm). */
.boiler__dial::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 999px 999px 0 0;
    background: conic-gradient(from 270deg at 50% 100%,
        var(--iron) 0deg 180deg, transparent 180deg);
    -webkit-mask: radial-gradient(circle farthest-side at 50% 100%, transparent 56%, #000 58%);
    mask: radial-gradient(circle farthest-side at 50% 100%, transparent 56%, #000 58%);
}
.boiler__zone {
    position: absolute;
    inset: 0;
    border-radius: 999px 999px 0 0;
    background: conic-gradient(from 270deg at 50% 100%,
        transparent 0deg calc(var(--redline) * 1.8deg),
        var(--copper) calc(var(--redline) * 1.8deg) 180deg,
        transparent 180deg);
    -webkit-mask: radial-gradient(circle farthest-side at 50% 100%, transparent 56%, #000 58%);
    mask: radial-gradient(circle farthest-side at 50% 100%, transparent 56%, #000 58%);
}
.boiler__ticks {
    position: absolute;
    inset: 0;
    border-radius: 999px 999px 0 0;
    /* Etched into the dial face, not painted on it: white ticks read as glare
       across the arc, and the needle is the only thing on this dial that
       should be bright. */
    background: repeating-conic-gradient(from 270deg at 50% 100%,
        rgba(0, 0, 0, 0.55) 0deg 0.8deg,
        transparent 0.8deg 18deg);
    -webkit-mask: radial-gradient(circle farthest-side at 50% 100%, transparent 68%, #000 70%, #000 92%, transparent 94%);
    mask: radial-gradient(circle farthest-side at 50% 100%, transparent 68%, #000 70%, #000 92%, transparent 94%);
}
.boiler__mount {
    position: absolute;
    left: 50%;
    bottom: -4px;
    width: 36px;
    height: 20px;
    transform: translateX(-50%);
    border: 1px solid var(--rig-edge);
    border-radius: 7px 7px 12px 12px;
    background: linear-gradient(to bottom, var(--rig-shell-lit), var(--rig-edge));
    box-shadow: 0 2px 3px rgba(0, 0, 0, 0.35);
    z-index: 1;
}
.boiler__mount::before {
    content: '';
    position: absolute;
    left: 50%;
    top: -8px;
    width: 10px;
    height: 12px;
    transform: translateX(-50%);
    border: 1px solid var(--rig-edge);
    border-bottom: 0;
    border-radius: 5px 5px 0 0;
    background: linear-gradient(to bottom, var(--brass-lit), var(--brass-dark));
}
.boiler__needle {
    position: absolute;
    left: 50%;
    bottom: 0;
    width: 3px;
    height: 82%;
    border-radius: 2px 2px 0 0;
    background: linear-gradient(to top, var(--brass-dark), var(--brass-lit));
    transform-origin: 50% 100%;
    /* -90deg is empty, +90deg is full: 1.8deg of sweep per percent. */
    transform: translateX(-50%) rotate(calc(-90deg + var(--pressure) * 1.8deg));
    transition: transform 0.9s var(--ease);
    z-index: 2;
}
.boiler--hot .boiler__needle { animation: needle-judder 0.42s ease-in-out infinite alternate; }
@keyframes needle-judder {
    from { transform: translateX(-50%) rotate(calc(-90deg + var(--pressure) * 1.8deg)); }
    to   { transform: translateX(-50%) rotate(calc(-87deg + var(--pressure) * 1.8deg)); }
}
/* A spin does not advance the streak, so the needle keeps its authoritative
   pressure reading. The small running rattle and lit connector acknowledge
   the same machine action the wheel and steam vent are showing. */
.boiler.is-running {
    border-color: var(--brass);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--brass-lit) 28%, transparent), var(--shadow-sm);
}
.boiler.is-running .boiler__connector { filter: brightness(1.18); }
.boiler.is-running .boiler__needle {
    animation: needle-running 0.22s ease-in-out infinite alternate;
}
@keyframes needle-running {
    from { transform: translateX(-50%) rotate(calc(-92deg + var(--pressure) * 1.8deg)); }
    to   { transform: translateX(-50%) rotate(calc(-88deg + var(--pressure) * 1.8deg)); }
}
.boiler__cap {
    position: absolute;
    left: 50%;
    bottom: -3px;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    transform: translateX(-50%);
    background: radial-gradient(circle at 35% 30%, var(--brass-lit), var(--brass-dark));
    border: 1px solid var(--rig-edge);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
    z-index: 3;
}
.boiler__meter {
    position: relative;
    height: 8px;
    margin: 0 0 8px;
    overflow: hidden;
    border-radius: var(--radius-pill);
    background: var(--iron);
    border: 1px solid var(--rig-edge);
}
.boiler__meter-fill {
    display: block;
    width: calc(var(--pressure) * 1%);
    height: 100%;
    border-radius: inherit;
    background: linear-gradient(to right, var(--brass), var(--brass-lit));
}
.boiler__meter-redline {
    position: absolute;
    top: -1px;
    bottom: -1px;
    left: calc(var(--redline) * 1%);
    width: 2px;
    background: var(--copper-lit);
    box-shadow: 0 0 5px var(--copper-lit);
}
.boiler__steps {
    display: flex;
    gap: 4px;
    margin: 0;
    padding: 0;
    list-style: none;
}
.boiler__step {
    flex: 1;
    min-width: 0;
    text-align: center;
}
.boiler__step-dot {
    display: block;
    width: 9px;
    height: 9px;
    margin: 0 auto 4px;
    border-radius: 50%;
    border: 1px solid var(--iron-lit);
    background: var(--bg-surface);
}
.boiler__step-label {
    display: block;
    font-family: var(--font-mono);
    font-size: 0.61rem;
    color: var(--text-muted);
    white-space: nowrap;
}
.boiler__step.is-complete .boiler__step-dot,
.boiler__step.is-current .boiler__step-dot {
    border-color: var(--brass-lit);
    background: var(--brass);
}
.boiler__step.is-complete .boiler__step-dot::after {
    content: '✓';
    display: block;
    margin-top: -3px;
    color: var(--plate-ink);
    font-size: 0.62rem;
    font-weight: 800;
}
.boiler__step.is-current .boiler__step-dot {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--brass-lit) 22%, transparent);
}
.boiler__step.is-current .boiler__step-label { color: var(--text-primary); font-weight: 700; }
.boiler__step.is-overdrive .boiler__step-label { color: var(--gold-gear-lit); }
.boiler__meter-note {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin: 7px 0 0;
    font-family: var(--font-mono);
    font-size: 0.66rem;
    color: var(--text-muted);
}
.boiler__readout { margin: 0; font-size: 0.86rem; color: var(--text-secondary); }
.boiler__state { margin: 8px 0 4px; }
.boiler__note { margin: 0; font-size: 0.82rem; color: var(--text-muted); }
.boiler__streak {
    margin: 10px 0 0;
    padding-top: 9px;
    border-top: 1px solid var(--border-subtle);
    font-size: 0.82rem;
    color: var(--text-muted);
}

/* ---------- The rig ---------- */
.wheel-shop__rig { display: flex; flex-direction: column; align-items: center; gap: 18px; min-width: 0; }
.wheel-legend {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 5px 12px;
    width: 100%;
    max-width: 520px;
    padding: 7px 10px;
    border-top: 1px solid var(--border-subtle);
    border-bottom: 1px solid var(--border-subtle);
}
.wheel-legend__title { margin-right: 2px; }
.wheel-legend__item { display: inline-flex; align-items: center; gap: 4px; font-size: 0.76rem; color: var(--text-secondary); }

.wheel-rig {
    --slice-angle: calc(360deg / var(--wheel-count, 12));
    width: 100%;
    max-width: 520px;
}

/* Marquee bulbs across the top arch. They breathe at rest, chase while the
   wheel turns, and burn hot in Overdrive: three states, all of which also have
   a worded equivalent elsewhere on the page. */
/* The marquee is the top of the same welded box as the frame below it, so it
   shares its side borders and only the outer corners round. */
.wheel-rig__marquee {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: clamp(8px, 2.6vw, 16px);
    padding: 10px 18px;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    background:
        linear-gradient(to bottom, var(--rig-shell-lit), var(--rig-shell) 70%, var(--rig-edge));
    border: 1px solid var(--rig-edge);
    border-bottom: none;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07);
}
.marquee-bulb {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 32%, var(--bulb), #8a6031 70%);
    box-shadow: 0 0 6px rgba(255, 190, 110, 0.45);
    animation: bulb-breathe 3.2s ease-in-out infinite;
    animation-delay: calc(var(--b) * 0.14s);
}
.wheel-rig.is-spinning .marquee-bulb {
    animation: bulb-chase 0.72s linear infinite;
    animation-delay: calc(var(--b) * 0.08s);
}
.wheel-shop.is-overdrive .marquee-bulb {
    background: radial-gradient(circle at 35% 32%, #fff3d0, var(--gold-gear) 70%);
    box-shadow: 0 0 12px rgba(245, 180, 38, 0.75);
}
@keyframes bulb-breathe { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
@keyframes bulb-chase { 0%, 60% { opacity: 0.35; } 30% { opacity: 1; } }

.wheel-rig__frame {
    position: relative;
    padding: clamp(14px, 4vw, 26px);
    background:
        linear-gradient(150deg, var(--rig-shell-lit) 0%, var(--rig-shell) 60%, var(--rig-edge) 100%);
    border: 1px solid var(--rig-edge);
    box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
/* Welded plate: rivets down both sides of the frame, which is the detail that
   stops the box behind the wheel reading as an empty dark rectangle. */
.wheel-rig__frame::before {
    content: '';
    position: absolute;
    inset: 6px;
    pointer-events: none;
    border: 1px solid rgba(255, 255, 255, 0.05);
    /* Explicit `circle 2.5px at ...` rather than a bare `circle`: a bare radial
       gradient is sized to its tile, so a 10x62 tile stretched each rivet into
       a capsule and the two columns read as film-strip perforations. Sparse
       pitch for the same reason. */
    background-image:
        radial-gradient(circle 2.5px at 5px 12px,
            rgba(255, 255, 255, 0.20) 0 55%, rgba(0, 0, 0, 0.5) 62%, transparent 72%),
        radial-gradient(circle 2.5px at 5px 12px,
            rgba(255, 255, 255, 0.20) 0 55%, rgba(0, 0, 0, 0.5) 62%, transparent 72%);
    background-size: 10px 58px, 10px 58px;
    background-position: 4px 10px, calc(100% - 4px) 10px;
    background-repeat: repeat-y, repeat-y;
}

/* The ticker: a spring-loaded ratchet finger that clatters against the pegs. */
.wheel-ticker {
    /* Overlaps the rim rather than hovering above it: the whole point of the
       ratchet finger is that it is in contact with the pegs. */
    position: absolute;
    top: calc(clamp(14px, 4vw, 26px) - 14px);
    left: 50%;
    transform: translateX(-50%);
    z-index: 3;
    width: 34px;
    height: 62px;
}
.wheel-ticker__arm {
    position: absolute;
    inset: 0;
    background: linear-gradient(100deg, var(--brass-lit) 0%, var(--brass) 42%, var(--brass-dark) 100%);
    clip-path: polygon(14% 0, 86% 0, 74% 62%, 54% 100%, 46% 100%, 26% 62%);
    transform-origin: 50% 6%;
    filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.65));
}
.wheel-ticker__pivot { z-index: 1; }
/* --tick-ms is written per spin stage by the dailyWheel component: fast while
   the disc is flat out, slow while it settles. The arm is meant to be a finger
   being knocked aside by each passing peg, so a clatter running at one fixed
   rate through a spin that visibly decelerates is the detail that gave the
   whole rig away as a CSS loop rather than a machine. Nothing is simulated
   here; two rates is enough for the motion to agree with the disc. */
.wheel-ticker.is-clattering .wheel-ticker__arm {
    animation: ticker-clatter var(--tick-ms, 0.085s) ease-in-out infinite alternate;
    transition: none;
}
@keyframes ticker-clatter { from { transform: rotate(-7deg); } to { transform: rotate(7deg); } }
.wheel-ticker__pivot {
    position: absolute;
    top: -3px;
    left: 50%;
    width: 13px;
    height: 13px;
    border-radius: 50%;
    transform: translateX(-50%);
    background: radial-gradient(circle at 35% 30%, var(--brass-lit), var(--brass-dark));
    border: 1px solid var(--rig-edge);
}

/* ---------- The disc ---------- */
/* Registered so it INTERPOLATES. An unregistered custom property jumps
   straight to its new value, which would snap the labels to their final
   orientation on frame one while the disc was still turning. Registering it is
   what lets one animated value drive both the disc and the counter-rotation,
   so the two cannot drift apart at any frame. Where @property is unsupported
   the transition simply does not run and the wheel snaps to its result, which
   is the same outcome reduced motion already produces. */
@property --disc-rot {
    syntax: '<angle>';
    inherits: true;
    initial-value: 0deg;
}

.wheel-disc {
    position: relative;
    width: 100%;
    aspect-ratio: 1;
    border-radius: 50%;
    transform: rotate(var(--disc-rot, 0deg));
    /* --spin-ms and --spin-ease are both written by the dailyWheel component,
       once per STAGE rather than once per spin: a launch that accelerates and
       holds, then a long deceleration onto the winning plate. One curve across
       the whole spin had the disc already slowing before its first full turn,
       which reads as a dial being set rather than a wheel being thrown.

       The site's reduced-motion rules clamp the duration with !important, so a
       calm-motion player gets an instant move without the component having to
       know: it only declines to add the extra turns and the jitter, so the
       disc lands square on the winning plate instead of teleporting to a
       deliberately off-centre one. */
    transition: --disc-rot var(--spin-ms, 4600ms) var(--spin-ease, cubic-bezier(0.18, 0.02, 0.05, 1));
    box-shadow:
        0 0 0 6px var(--iron),
        0 0 0 8px var(--brass-dark),
        0 10px 30px rgba(0, 0, 0, 0.5);
}
.wheel-disc__plates {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    overflow: hidden;
}
/* Sinks the plates into the iron band around them. Without this the wedges sit
   flush with the rim and the disc reads as printed rather than assembled. */
.wheel-disc__plates::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    pointer-events: none;
    box-shadow:
        inset 0 0 14px rgba(0, 0, 0, 0.55),
        inset 0 0 0 3px rgba(0, 0, 0, 0.35);
}

/* Each wedge is a triangle from the centre out past the rim; the round parent
   clips it back to a true sector. tan() gives the half-width at the rim, so
   the geometry follows --wheel-count with no per-count magic numbers. */
.wheel-slice {
    position: absolute;
    inset: 0;
    transform: rotate(calc(var(--i) * var(--slice-angle)));
    clip-path: polygon(
        50% 50%,
        calc(50% - 50% * tan(var(--slice-angle) / 2)) 0%,
        calc(50% + 50% * tan(var(--slice-angle) / 2)) 0%);
}
/* Each plate is dark at the hub and lifts to a single sheen band near the rim,
   then falls back. A straight ramp to the light tone washed every wedge out to
   pastel and the whole disc read as a pie chart; a band reads as light catching
   one edge of a metal plate, which is the thing being drawn. */
.wheel-slice--iron {
    background: linear-gradient(to top, #221e1c 0%, #38322f 52%, var(--iron-lit) 86%, #3f3936 100%);
}
.wheel-slice--bronze {
    background: linear-gradient(to top, #3d2a0e 0%, #7a5620 52%, var(--brass) 86%, #6a4a1b 100%);
}
.wheel-slice--copper {
    background: linear-gradient(to top, #4a230d 0%, #8e4a1f 52%, var(--copper) 86%, #7b3d18 100%);
}
.wheel-slice--starsteel {
    background: linear-gradient(to top, #10312e 0%, #2c6b62 52%, var(--starsteel) 88%, #3f8a7e 100%);
}
.wheel-slice.is-golden {
    background: linear-gradient(to top, #6b4703 0%, #b8830c 50%, var(--gold-gear-lit) 86%, var(--gold-gear) 100%);
}

/* ---------- Marking a wedge ----------
   The winning plate is marked by DIMMING EVERY OTHER PLATE, never by painting
   the winner. That inversion is the whole design, and it took three attempts
   to get here:

   1. `filter: brightness(1.5)` on the winner. The wedges are authored as dark
      at the hub rising to a single sheen band near the rim, which is what
      makes them read as metal plates rather than a pie chart. Multiplying that
      flattens the ramp into one neon smear.
   2. A warm gradient laid over the winner. Softer, same fault: at any alpha
      strong enough to read across the room it repaints an Iron plate the
      colour of a Bronze one, so the marking fights the tier colour that is
      already carrying meaning. At any alpha weak enough not to, it is
      invisible.
   3. Dim the others. The winner is not touched at all: it keeps its exact
      authored gradient and tier colour, and reads as lit because the wheel
      around it has fallen away. A spotlight, not a repaint.

   Also worth knowing before drawing anything on a wedge: a .wheel-slice is a
   CLIPPED TRIANGLE inside a full-size square box, so an `inset: 0`
   pseudo-element covers the whole disc rather than the wedge, and an `inset`
   box-shadow on it glows inward from the SQUARE's four edges before being
   clipped to the triangle: a halo with entirely the wrong geometry.

   The dim is keyed off container classes (`has-landed` / `has-highlight`) so
   nothing is dimmed until there is actually something to point at: on a fresh
   page load, and on the "already pulled today" page, the wheel is simply a
   wheel. */
.wheel-slice,
.wheel-label {
    transition: filter 0.4s ease, opacity 0.4s ease;
}
.wheel-disc.has-landed .wheel-slice:not(.is-landed),
.wheel-disc.has-highlight .wheel-slice:not(.is-highlighted):not(.is-landed) {
    filter: brightness(0.45) saturate(0.55);
}
.wheel-disc.has-landed .wheel-label:not(.is-landed),
.wheel-disc.has-highlight .wheel-label:not(.is-highlighted):not(.is-landed) {
    opacity: 0.42;
}
/* A hover/focus on an odds row is transient and gets a lighter touch than a
   result: enough to find the wedge while scanning the table, not enough to
   make a 12-row list feel like it is strobing the wheel. `has-landed` wins,
   so brushing the table can never dull the plate that actually won. */
.wheel-disc.has-highlight:not(.has-landed) .wheel-slice:not(.is-highlighted) {
    filter: brightness(0.62) saturate(0.7);
}
.wheel-disc.has-highlight:not(.has-landed) .wheel-label:not(.is-highlighted) {
    opacity: 0.55;
}
/* The winner's own label gets a small warm halo and NO scale: the labels sit
   close enough at 12 plates that enlarging one pushes it into its neighbours,
   and a label that pops on a wheel which has just stopped reads as the page
   still loading. */
.wheel-label.is-landed {
    text-shadow:
        0 1px 2px rgba(0, 0, 0, 0.85),
        0 0 10px rgba(255, 226, 160, 0.7);
}
.wheel-label.is-landed.is-golden {
    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
}

.wheel-disc__seams {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    pointer-events: none;
    /* A dark seam with a thin lit edge on one side: a riveted joint between two
       plates, not a divider line. At 0.5deg and flat black it was invisible
       against the plates at every size the wheel actually renders at. */
    background: repeating-conic-gradient(from calc(var(--slice-angle) / -2),
        var(--seam) 0deg 0.85deg,
        rgba(255, 255, 255, 0.16) 0.85deg 1.25deg,
        transparent 1.25deg var(--slice-angle));
}
.wheel-disc__marks { position: absolute; inset: 0; pointer-events: none; }

/* Labels and pegs are placed by polar coordinates rather than
   rotate-then-counter-rotate: the label stays upright with no wrapper element,
   which is what makes a glyph plus a number readable at the bottom of the
   wheel as well as the top. */
.wheel-label {
    --a: calc(var(--i) * var(--slice-angle));
    position: absolute;
    left: calc(50% + 34% * sin(var(--a)));
    top: calc(50% - 34% * cos(var(--a)));
    /* Rides around with its plate but stays the right way up. Without the
       counter-rotation the wheel comes to rest with half its payouts printed
       upside down and stays that way until tomorrow, which reads as a bug
       rather than as a wheel. Driven by the same interpolated --disc-rot as
       the disc, so it cannot lag by a frame. */
    transform: translate(-50%, -50%) rotate(calc(-1 * var(--disc-rot, 0deg)));
    display: flex;
    flex-direction: column;
    align-items: center;
    line-height: 1;
    /* Light ink on dark plates. The wedges are darkest toward the hub and the
       label sits at 34% out, so it is always over the mid-to-dark part of the
       ramp: a dark ink read as a smudge on iron and vanished on copper. */
    color: #fdf6ea;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85), 0 0 6px rgba(0, 0, 0, 0.5);
}
.wheel-label__icon { font-size: clamp(0.95rem, 3.6vw, 1.35rem); }
.wheel-label__amt {
    font-size: clamp(0.66rem, 2.2vw, 0.86rem);
    font-weight: 700;
    margin-top: 3px;
    letter-spacing: 0.02em;
}
/* The golden gears are the one genuinely bright plate, so they take the dark
   ink instead. */
.wheel-label.is-golden {
    color: #2b1c00;
    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.45);
}

.wheel-peg {
    --a: calc((var(--i) - 0.5) * var(--slice-angle));
    position: absolute;
    left: calc(50% + 45.5% * sin(var(--a)));
    top: calc(50% - 45.5% * cos(var(--a)));
    width: 7px;
    height: 7px;
    border-radius: 50%;
    transform: translate(-50%, -50%);
    background: radial-gradient(circle at 34% 30%, #cfc6bd, #4b4340 72%);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}

/* The hub: a stack of brass gears bolted through the middle. */
.wheel-hub {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 27%;
    aspect-ratio: 1;
    transform: translate(-50%, -50%);
}
/* Teeth hug the hub body. At inset -15% with a 61% mask they read as a dashed
   ring floating clear of the gear rather than as its teeth. */
.wheel-hub__teeth {
    position: absolute;
    inset: -9%;
    border-radius: 50%;
    /* Twenty-four 15-degree teeth close evenly around the circle. The old
       13-degree cadence left a fractional final repeat, which made the hub
       look subtly lopsided even though its bounding box was centered. */
    background: repeating-conic-gradient(
        var(--brass) 0deg 6deg, var(--brass-dark) 6deg 6.6deg, transparent 6.6deg 15deg);
    /* farthest-side, not the default farthest-corner: on a square box the
       corner radius is 1.41x the half-width, so a percentage stop lands
       outside the element and the whole ring masks away to nothing. That is
       what made the teeth invisible the first time round. */
    -webkit-mask: radial-gradient(circle farthest-side, transparent 76%, #000 78%);
    mask: radial-gradient(circle farthest-side, transparent 76%, #000 78%);
    filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.6));
}
.wheel-hub__face {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background:
        radial-gradient(circle at 34% 28%, var(--brass-lit), var(--brass) 52%, var(--brass-dark) 100%);
    border: 2px solid var(--rig-edge);
    box-shadow: inset 0 0 0 4px rgba(0, 0, 0, 0.18);
}
.wheel-hub__bolt {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 34%;
    height: 34%;
    transform: translate(-50%, -50%);
    background: linear-gradient(140deg, #6d635c, #2c2724);
    clip-path: polygon(25% 3%, 75% 3%, 100% 50%, 75% 97%, 25% 97%, 0 50%);
}

/* Steam venting from under the frame while she runs. */
.wheel-steam {
    position: absolute;
    left: 8%;
    bottom: 4%;
    width: 46%;
    height: 40%;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.4s ease;
}
.wheel-steam.is-venting { opacity: 1; }
.wheel-steam i {
    position: absolute;
    bottom: 0;
    left: calc(var(--s) * 34%);
    width: 46px;
    height: 46px;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(255, 255, 255, 0.4), transparent 68%);
    filter: blur(5px);
}
.wheel-steam.is-venting i {
    animation: steam-rise 2.1s ease-out infinite;
    animation-delay: calc(var(--s) * 0.55s);
}
@keyframes steam-rise {
    from { transform: translateY(10px) scale(0.55); opacity: 0; }
    35%  { opacity: 0.75; }
    to   { transform: translateY(-70px) scale(1.5); opacity: 0; }
}

.wheel-rig__chassis {
    display: flex;
    align-items: stretch;
    gap: 6px;
    height: 18px;
    padding: 0 12px;
}
.chassis-pipe {
    width: 26px;
    border-radius: 0 0 6px 6px;
    background: linear-gradient(to right, var(--copper), var(--copper-lit) 45%, var(--copper));
    border: 1px solid var(--rig-edge);
    border-top: none;
}
.chassis-plate {
    flex: 1;
    border-radius: 0 0 var(--radius-md) var(--radius-md);
    border: 1px solid var(--rig-edge);
    border-top: none;
    /* Both layers in ONE background-image. The plate previously set its metal
       via the `background` shorthand and then its rivets via `background-image`,
       which silently replaced the gradient: harmless against a dark page,
       but on the two light themes the bar showed the page through it and the
       machine appeared to be standing on a white slab. */
    background-image:
        radial-gradient(circle 2px at 13px 5px, rgba(255, 255, 255, 0.10) 0 60%, transparent 70%),
        linear-gradient(to bottom, var(--rig-shell), var(--rig-edge));
    background-size: 26px 16px, auto;
    background-repeat: repeat-x, no-repeat;
}

/* ---------- The lever ---------- */
.wheel-lever { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.wheel-lever__promise { margin: 0 0 2px; text-align: center; font-size: 0.86rem; color: var(--text-secondary); }
.wheel-lever__promise strong { color: var(--text-primary); }
.wheel-lever__skip { min-height: 40px; }
.btn-lever {
    display: inline-flex;
    align-items: center;
    gap: 12px;
    min-height: 48px;
    padding: 13px 26px 13px 16px;
    border: 1px solid var(--rig-edge);
    border-radius: var(--radius-pill);
    background: linear-gradient(to bottom, var(--brass-lit), var(--brass) 55%, var(--brass-dark));
    color: var(--plate-ink);
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: 1.02rem;
    letter-spacing: 0.02em;
    cursor: pointer;
    box-shadow: 0 4px 0 var(--rig-edge), var(--shadow-sm);
    transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
@media (hover: hover) { .btn-lever:hover:not(:disabled) { filter: brightness(1.06); } }
.btn-lever:active:not(:disabled) { transform: translateY(3px); box-shadow: 0 1px 0 var(--rig-edge); }
.btn-lever:disabled { opacity: 0.65; cursor: progress; transform: translateY(3px); box-shadow: 0 1px 0 var(--rig-edge); }
.btn-lever__grip {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: radial-gradient(circle at 33% 28%, #f0f0f0, #6f2c1e 72%);
    border: 1px solid var(--rig-edge);
    flex: none;
}
.wheel-lever__note { margin: 0; font-size: 0.8rem; color: var(--text-muted); }
.wheel-lever__spent { margin: 0; }
.wheel-lever__next,
.wheel-result__destination,
.wheel-last__destination { font-size: 0.82rem; font-weight: 700; color: var(--color-primary); }

/* ---------- Results ---------- */
.wheel-result-slot { margin-bottom: 24px; scroll-margin-bottom: calc(var(--tabbar-height) + 18px); }

.wheel-result {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
    gap: 20px;
    align-items: center;
    padding: 20px 22px;
    border-radius: var(--radius-lg);
    background: var(--bg-surface);
    border: 1px solid var(--border-color);
    border-left: 4px solid var(--brass);
    box-shadow: var(--shadow-md);
}
.wheel-result.is-overdrive { border-left-color: var(--gold-gear); }
.wheel-result--jackpot { border-left-color: var(--color-primary); box-shadow: var(--glow-accent), var(--shadow-md); }
.wheel-result--low_roll { border-left-color: var(--iron-lit); }
.wheel-result__prize { min-width: 0; }
.wheel-result__label {
    font-family: var(--font-display);
    font-size: clamp(1.25rem, 1rem + 1.2vw, 1.7rem);
    margin: 4px 0 10px;
}
.wheel-result__banked { margin: 10px 0 0; font-size: 0.82rem; color: var(--text-muted); }
.wheel-result__destination { display: inline-block; margin-top: 9px; }
.wheel-result__crew { min-width: 0; }
.wheel-result__line {
    margin: 0 0 8px;
    font-size: 1rem;
    color: var(--text-secondary);
    font-style: italic;
}
.wheel-result__meta { margin: 0; font-size: 0.8rem; color: var(--text-muted); }

.wheel-last {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 14px;
    padding: 14px 18px;
    border-radius: var(--radius-md);
    background: var(--bg-subtle);
    border: 1px solid var(--border-color);
    border-left: 3px solid var(--iron-lit);
}
.wheel-last.is-overdrive { border-left-color: var(--gold-gear); }
.wheel-last .eyebrow { flex-basis: 100%; }
.wheel-last__row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.wheel-last__label { font-family: var(--font-heading); font-weight: 700; }
.wheel-last__meta { margin: 0; font-size: 0.8rem; color: var(--text-muted); margin-left: auto; }
.wheel-last__destination { flex-basis: 100%; }

/* A won plate. Tier is carried by the left edge and the icon, never by colour
   alone: the amount and the currency name are always spelled out beside it. */
.prize-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
    padding: 8px 14px;
    border-radius: var(--radius-pill);
    background: var(--bg-subtle);
    border: 1px solid var(--border-color);
    border-left: 3px solid var(--iron-lit);
}
.prize-chip--bronze    { border-left-color: var(--brass); }
.prize-chip--copper    { border-left-color: var(--copper); }
.prize-chip--starsteel { border-left-color: var(--color-primary); }
.prize-chip.is-golden  { border-left-color: var(--gold-gear); }
.prize-chip__icon { font-size: 1.15rem; }
.prize-chip__amt { font-size: 1.3rem; font-weight: 700; color: var(--text-primary); }
.prize-chip__name { font-size: 0.86rem; color: var(--text-secondary); }

/* ---------- Spec sheet ---------- */
.spec-sheet__head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 6px;
}
.spec-sheet__head h2 { margin: 4px 0 0; font-size: 1.05rem; }
.spec-sheet__intro { margin: 0 0 14px; font-size: 0.87rem; color: var(--text-secondary); max-width: 70ch; }
.table-scroll {
    /* Same containing-block rule as .staff-table-scroll above, and this is the
       box that proved it: /staff/catalog/ holds 128 .sr-only price labels inside
       this scroller, the rightmost landing at x=1359 against a 1265px viewport,
       so the whole document laid out 94px wider than the screen with nothing
       visibly overflowing. See docs/ux-rules.md "Layout and overflow". */
    position: relative;
    width: 100%;
    min-width: 0;
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}
.table-scroll > .staff-catalog-table { max-width: none; }
/* min-width is what makes the scroll container actually work. At width:100%
   alone the table squeezed to the panel instead of overflowing, so
   .table-scroll saw nothing to scroll and the Pays and Chance columns were
   simply clipped off the right edge on a phone: the two columns the table
   exists for. */
.spec-sheet__table { width: 100%; min-width: 430px; border-collapse: collapse; font-size: 0.87rem; }
.spec-sheet { scroll-margin-top: calc(var(--header-height) + 16px); scroll-margin-bottom: calc(var(--tabbar-height) + 18px); }
.spec-sheet__cards { display: none; }
.spec-sheet__card {
    padding: 11px 12px;
    border-radius: var(--radius-md);
    border: 1px solid var(--border-color);
    background: var(--color-surface-2, rgba(255, 255, 255, 0.03));
}
.spec-sheet__card.is-golden {
    border-color: var(--gold-gear, #f5b426);
    background: color-mix(in srgb, var(--gold-gear, #f5b426) 12%, transparent);
}
.spec-sheet__card-head { display: flex; justify-content: space-between; gap: 10px; }
.spec-sheet__card-head .mono { color: var(--text-muted); font-size: 0.78rem; white-space: nowrap; }
.spec-sheet__card p { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; margin: 7px 0 0; color: var(--text-secondary); }
.spec-sheet__card a { display: inline-flex; align-items: center; min-height: 32px; font-weight: 700; }
.spec-sheet__table th,
.spec-sheet__table td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--border-subtle); }
.spec-sheet__table thead th {
    font-family: var(--font-mono);
    font-size: 0.68rem;
    font-weight: 400;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.spec-sheet__table tbody th { font-weight: 600; }
.spec-sheet__table .num { text-align: right; }
.spec-sheet__table tbody tr.is-golden { background: color-mix(in srgb, var(--gold-gear) 9%, transparent); }
.spec-sheet__foot { margin: 14px 0 0; font-size: 0.8rem; color: var(--text-muted); }

.plate-tag {
    display: inline-block;
    padding: 2px 9px;
    border-radius: var(--radius-pill);
    font-size: 0.72rem;
    border: 1px solid var(--border-color);
    color: var(--text-secondary);
}
.plate-tag--iron      { border-color: color-mix(in srgb, var(--iron-lit) 60%, var(--border-color)); }
.plate-tag--bronze    { border-color: color-mix(in srgb, var(--brass) 60%, var(--border-color)); }
.plate-tag--copper    { border-color: color-mix(in srgb, var(--copper) 60%, var(--border-color)); }
.plate-tag--starsteel { border-color: color-mix(in srgb, var(--color-primary) 60%, var(--border-color)); color: var(--color-primary); }
.tag--golden {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 8px;
    border-radius: var(--radius-pill);
    font-size: 0.68rem;
    font-weight: 700;
    background: color-mix(in srgb, var(--gold-gear) 22%, transparent);
    border: 1px solid color-mix(in srgb, var(--gold-gear) 55%, var(--border-color));
    color: var(--text-primary);
}

/* ---------- Light themes ----------
   The machine stays a machine: dark shell, warm metals. What changes is the
   contrast around it. On a pale page the untouched brass glares and the
   plate ink loses its shadow, so the metals drop a step and the label
   highlight comes off. */
body.theme-daylight .wheel-shop,
body.theme-comet .wheel-shop {
    --brass: #96682a;
    --brass-lit: #c8933f;
    --copper: #a5551f;
    --copper-lit: #c9743c;
    --starsteel: #4bab9c;
    --starsteel-lit: #b5e6dd;
    --gold-gear: #d99406;
    --gold-gear-lit: #f5c76a;
    --bulb: #ffcf8a;
    --plate-ink: #120d09;
}
/* No light-theme override for .wheel-label: the plates are the same dark metal
   in every theme (only the metal tones shift a step), so the label treatment
   must not change with the page either. An earlier `text-shadow: none` here
   dated from when the labels were dark ink on pale plates. */

/* ---------- Streak alarm ----------
   The one thing the wheel never told anyone: a streak breaks by being missed,
   and this page had no idea what time it was. A Commander six days into a
   seven-day cycle could sit here reading about Overdrive with no indication
   that skipping tonight sends them back to day one. Warn tone, not error: the
   pull is still available, which is the whole point of saying so. */
.wheel-alarm {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 4px 8px;
    margin: 10px 0 0;
    padding: 9px 13px;
    border-radius: var(--radius-md);
    border: 1px solid var(--color-warning, #b8830c);
    border-left-width: 4px;
    background: color-mix(in srgb, var(--color-warning, #b8830c) 12%, transparent);
    font-size: 0.85rem;
    line-height: 1.45;
}
.wheel-alarm__glyph { flex: none; }
.wheel-alarm--hot {
    border-color: var(--gold-gear, #f5b426);
    background: color-mix(in srgb, var(--gold-gear, #f5b426) 15%, transparent);
}

/* ---------- Lifetime record + recent pulls ----------
   total_spins has been on DailySpin since the wheel shipped and was rendered
   nowhere at all; longest_streak got one small line. A daily habit surface
   with no memory of the habit is the gap these two close. */
.wheel-record {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
    gap: 8px;
    margin-top: 14px;
}
.wheel-record__milestone {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-top: 14px;
    padding: 10px 12px;
    border-left: 3px solid var(--brass);
    background: var(--color-surface-2, rgba(255, 255, 255, 0.03));
}
.wheel-record__milestone strong { display: block; margin-top: 2px; }
.wheel-record__milestone p { margin: 0; font-size: 0.8rem; color: var(--text-muted); text-align: right; }
.wheel-record-panel { scroll-margin-bottom: calc(var(--tabbar-height) + 18px); }
.wheel-record__cell {
    padding: 9px 12px;
    border-radius: var(--radius-md);
    border: 1px solid var(--rig-edge, var(--color-border));
    background: var(--color-surface-2, rgba(255, 255, 255, 0.03));
    min-width: 0;
}
.wheel-record__value {
    display: block;
    font-weight: 700;
    font-size: 1.02rem;
    line-height: 1.2;
    /* Wraps rather than blowing out the grid: a best haul reads "500 Stardust",
       which does not fit a 112px column on one line at any font size. */
    overflow-wrap: anywhere;
}
.wheel-record__label {
    display: block;
    margin-top: 2px;
    font-size: 0.72rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.75;
}

.wheel-history { margin-top: 14px; }
.wheel-history__strip {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    padding-bottom: 4px;
    /* The strip scrolls sideways on a phone; the wrapper carries the keyboard
       affordance (see the odds table's own table-scroll region). */
    scrollbar-width: thin;
}
.wheel-history__item {
    flex: 0 0 auto;
    min-width: 84px;
    padding: 8px 10px;
    border-radius: var(--radius-md);
    border: 1px solid var(--rig-edge, var(--color-border));
    background: var(--color-surface-2, rgba(255, 255, 255, 0.03));
    text-align: center;
}
.wheel-history__item.is-golden {
    border-color: var(--gold-gear, #f5b426);
    background: color-mix(in srgb, var(--gold-gear, #f5b426) 14%, transparent);
}
.wheel-history__day {
    display: block;
    font-size: 0.68rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.7;
}
.wheel-history__icon { display: block; font-size: 1.15rem; margin: 2px 0 1px; }
.wheel-history__amt { display: block; font-weight: 700; font-size: 0.86rem; }

/* ---------- Overdrive swap table ----------
   The spec sheet listed today's odds and left the player to work out what a
   five-day streak had bought them by diffing two faces from memory. This shows
   the trade as a trade. Rendered whether or not the valve is currently open:
   as a promise while pressure builds, as a receipt once it is. */
.spec-sheet__swaps { margin-top: 18px; }
.swap-list { display: grid; gap: 8px; margin: 0 0 4px; padding: 0; list-style: none; }
.swap-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border-radius: var(--radius-md);
    border: 1px solid var(--rig-edge, var(--color-border));
    background: var(--color-surface-2, rgba(255, 255, 255, 0.03));
}
.swap-row__from { min-width: 0; opacity: 0.68; }
.swap-row__from .swap-row__pay { text-decoration: line-through; }
.swap-row__arrow { flex: none; opacity: 0.8; }
.swap-row__to { min-width: 0; color: var(--gold-gear-lit, #ffd977); font-weight: 600; }
.swap-row__name { display: block; font-size: 0.83rem; }
.swap-row__pay { display: block; font-size: 0.9rem; font-weight: 700; }
@media (max-width: 560px) {
    /* Three columns at 390px gives each side ~120px, which truncates a plate
       name and a payout on both sides at once. Stack, and turn the arrow. */
    .swap-row { grid-template-columns: minmax(0, 1fr); text-align: center; gap: 2px; }
    .swap-row__arrow { transform: rotate(90deg); }
}

/* ---------- Expected pull ----------
   Deliberately per-reward, never one headline number: the plates pay in two
   currencies and in items, and collapsing those needs an exchange rate this
   game does not have. See economy/wheel.py::face_expectation. */
.spec-expect {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 10px;
    margin: 0 0 14px;
    font-size: 0.86rem;
}
.spec-expect__chip {
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    padding: 3px 9px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--rig-edge, var(--color-border));
    background: var(--color-surface-2, rgba(255, 255, 255, 0.03));
}

/* ---------- Odds row <-> wedge ----------
   Focusable rows, because hover is not a route in on a phone or a keyboard.
   The outline is the standard focus treatment; the background is the "this row
   is the one lighting a wedge" cue. */
.spec-sheet__table tbody tr { transition: background-color 0.15s ease; }
.spec-sheet__table tbody tr[tabindex]:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: -2px;
}
.spec-sheet__table tbody tr.is-highlighted {
    background: color-mix(in srgb, var(--color-primary) 14%, transparent);
}
.spec-sheet__table tbody tr.is-landed {
    background: color-mix(in srgb, var(--gold-gear, #f5b426) 16%, transparent);
}
.spec-sheet__won {
    margin-left: 6px;
    font-size: 0.72rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--gold-gear-lit, #ffd977);
}

/* ---------- Reduced motion ----------
   The global body.reduced-motion / prefers-reduced-motion rules already clamp
   every duration, which is what turns the spin into an instant snap. These
   two go further and stop the idle loops entirely: a needle that judders and
   bulbs that breathe forever are exactly the ambient motion that preference
   is asking us to remove, and neither carries information. */
body.reduced-motion .marquee-bulb,
body.reduced-motion .boiler--hot .boiler__needle,
body.reduced-motion .boiler.is-running .boiler__needle,
body.reduced-motion .sparky__stencil-mark,
body.reduced-motion .wheel-steam i,
body.reduced-motion .wheel-ticker__arm { animation: none; }
body.reduced-motion .marquee-bulb { opacity: 1; }
body.reduced-motion .wheel-steam { display: none; }
@media (prefers-reduced-motion: reduce) {
    .marquee-bulb,
    .boiler--hot .boiler__needle,
    .boiler.is-running .boiler__needle,
    .sparky__stencil-mark,
    .wheel-steam i,
    .wheel-ticker__arm { animation: none; }
    .marquee-bulb { opacity: 1; }
    .wheel-steam { display: none; }
}
/* The winning wedge's sheen is NOT in that list on purpose. It is the only
   thing on the disc that says which plate won, and a calm-motion player has
   already given up the spin that would otherwise have shown them: removing the
   marking too would leave them with a result card and an anonymous wheel. It
   is a static gradient, not motion. */

/* ---------- Responsive ----------
   Below the tablet line the crew column stops being a column: Sparky shrinks
   to a bust beside the gauge so the wheel keeps the top of the screen, which
   is the thing the page exists for. */
@media (max-width: 900px) {
    .wheel-shop { grid-template-columns: minmax(0, 1fr); }
    .wheel-shop__crew {
        display: grid;
        grid-template-columns: 120px minmax(0, 1fr);
        grid-template-areas: 'portrait speech';
        gap: 12px 16px;
        align-items: start;
    }
    .sparky { grid-area: portrait; }
    .sparky-speech { grid-area: speech; min-height: 0; }
    .boiler { grid-area: gauge; }
    .boiler__dial { margin-left: auto; margin-right: auto; }
    .wheel-result { grid-template-columns: minmax(0, 1fr); }
}
/* On a phone the wheel has to be reachable without scrolling past a portrait.
   `display: contents` promotes the crew's two children into .wheel-shop's own
   flex flow, while the pressure console stays attached to the wheel assembly.
   Sparky collapses to a bust-and-nameplate strip above her line. */
@media (max-width: 560px) {
    /* align-items must be reset: the base rule sets `start` for the desktop
       grid, and the same declaration in a flex container means "size to
       content" rather than "top-align", which collapsed the rig to 197px of a
       335px column and took the wheel down to 165px with it. */
    .wheel-shop { display: flex; flex-direction: column; align-items: stretch; gap: 14px; }
    .wheel-shop__crew { display: contents; }
    .sparky { order: 1; display: flex; align-items: center; gap: 12px; }
    .sparky__frame { width: 92px; flex: none; }
    .sparky__plate { margin-top: 0; flex: 1; min-width: 0; }
    .sparky-speech { order: 2; }
    .wheel-shop__rig { order: 3; }
    .wheel-last__meta { margin-left: 0; flex-basis: 100%; }
    .wheel-stage { padding-bottom: calc(var(--tabbar-height) + 18px); }
    .wheel-legend { justify-content: flex-start; }
    .wheel-legend__title { flex-basis: 100%; }
    .spec-sheet__cards { display: grid; gap: 8px; }
    .table-scroll { display: none; }
    .wheel-record__milestone { align-items: flex-start; flex-direction: column; }
    .wheel-record__milestone p { text-align: left; }
}

