/* ============================================================
   modals.css: dialogs, the revolver stage, toasts, server pill
   ============================================================ */

.modal {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: none;
    align-items: center;
    justify-content: center;
    padding: var(--sp-4);
}

.modal.is-open {
    display: flex;
}

.modal__backdrop {
    position: absolute;
    inset: 0;
    /* flat ink over the plate, not a frosted pane of glass */
    background: rgba(6, 5, 5, .9);
    opacity: 0;
    transition: opacity var(--t-med) var(--ease-out);
}

.modal.is-visible .modal__backdrop {
    opacity: 1;
}

.modal__box {
    position: relative;
    width: 100%;
    max-width: 640px;
    max-height: calc(100vh - var(--sp-6));
    overflow-y: auto;
    padding: var(--sp-6) var(--sp-6) var(--sp-5);
    text-align: center;
    background-color: var(--ink-850);
    background-image: var(--hatch-fine);
    border: 1px solid var(--rule);
    border-radius: var(--r-md);
    box-shadow: var(--sh-4);
    /* same inset rule as the lobby panel, so dialogs read as the same object.
       outline rather than a pseudo-element: this box scrolls. */
    outline: 1px solid var(--frame-line);
    outline-offset: calc(var(--frame-inset) * -1);
    opacity: 0;
    transform: translateY(14px);
    transition: opacity var(--t-med) var(--ease-out), transform var(--t-med) var(--ease-out);
}

.modal.is-visible .modal__box {
    opacity: 1;
    transform: translateY(0);
}

.modal__box--narrow {
    max-width: 470px;
}

.modal__title {
    font-family: var(--font-engraved);
    font-size: clamp(32px, 5.4vw, 48px);
    font-weight: 900;
    line-height: 1.05;
    letter-spacing: .01em;
    color: var(--gold-hot);
    text-shadow: 3px 3px 0 #000;
}

.modal__title.is-danger {
    color: var(--oxblood-hot);
}

.modal__title.is-good {
    color: #9dbf94;
}

.modal__sub {
    margin-top: var(--sp-2);
    font-size: 15.5px;
    color: var(--text-dim);
}

.modal__actions {
    display: flex;
    gap: var(--sp-3);
    justify-content: center;
    margin-top: var(--sp-5);
    flex-wrap: wrap;
}

.modal__actions .btn {
    min-width: 172px;
}

/* ---------------- Roulette modal ---------------- */

.roulette-consequence {
    margin-top: var(--sp-3);
    font-size: 16.5px;
    font-weight: 700;
    color: var(--text);
}

.roulette-consequence b {
    color: var(--gold);
}

.revolver-stage {
    position: relative;
    display: grid;
    place-items: center;
    /* sized off viewport height so the whole scene, Continue button included,
       still fits on a 720p screen without the dialog scrolling */
    height: clamp(150px, 26vh, 236px);
    margin: var(--sp-3) auto var(--sp-2);
}

.revolver-svg {
    width: clamp(140px, 24vh, 214px);
    height: clamp(140px, 24vh, 214px);
    overflow: visible;
}

/* the challenge dialog is the tallest one, so keep its parts trim */
.modal--roulette .revealed-cards .card {
    width: clamp(52px, 7vh, 76px);
    height: clamp(78px, 10.5vh, 114px);
}

.modal--roulette .modal__box {
    padding: var(--sp-5) var(--sp-5) var(--sp-4);
}

/* the indexing itself is driven by the Web Animations API in roulette.js */
.revolver-cylinder {
    transform-origin: 100px 108px;
}

/*
 * The index mark stands in for the hammer: fixed at twelve o'clock while the
 * cylinder turns under it. It dips just before the trigger, which is the only
 * warning you get.
 */
.revolver-mark {
    transform-origin: 100px 26px;
    transition: transform .12s var(--ease-out);
}

.revolver-mark.is-striking {
    transform: translateY(6px) scaleY(1.12);
}

/*
 * A round still in the cylinder, and the moment it is spent.
 *
 * No transition on either this or the strike below: the round goes and the
 * cross lands on the same frame as the shot. Fading them took 300ms, which
 * read as the mark arriving late, and while it ran the round and the cross
 * were both half-visible on top of each other. A gunshot should be a cut.
 */
.revolver-round {
    transform-box: fill-box;
    transform-origin: center;
}

.revolver-round.is-spent {
    opacity: 0;
}

/*
 * The cross marking a fired chamber. Present on every chamber but only painted
 * where one has gone off, so the chamber that fires during the sequence is
 * struck out on the same frame the round leaves it.
 */
.revolver-strike {
    opacity: 0;
}

.revolver-strike.is-shown {
    opacity: 1;
}

/*
 * Recoil. Two things were wrong with easing this out of a 16% keyframe: the
 * whole move was vertical, and the ease meant the gun arrived at its extreme
 * gently. A shot is an impulse, so the kick is on the first frame and everything
 * after it is the gun coming back: 0% is already thrown, and the overshoot at
 * 62% is the settle rather than a second hit.
 *
 * --recoil-x is re-rolled per shot in roulette.js. Without it the gun jumps the
 * same direction every time, which is the same problem the old screen shake had.
 */
.revolver-stage.is-firing .revolver-svg {
    animation: recoil .42s cubic-bezier(.22, .9, .3, 1) both;
}

@keyframes recoil {
    0% {
        transform:
            translate(var(--recoil-x, -9px), 15px) scale(1.16)
            rotate(calc(var(--recoil-x, -9px) * .28));
    }

    38% {
        transform: translate(calc(var(--recoil-x, -9px) * -.18), -5px) scale(.975) rotate(0deg);
    }

    62% {
        transform: translate(0, 2px) scale(1.012) rotate(0deg);
    }

    100% {
        transform: translate(0, 0) scale(1) rotate(0deg);
    }
}

/*
 * The chromatic punch: on for about 130ms across the shot.
 *
 * Blowing out the brightness and pulling the channels apart says the camera
 * could not cope with the light, which is a statement about the room. Making the
 * gun brighter is a statement about the gun, and that is the difference between
 * this and a glow. The filter itself is #fx-chroma in index.html.
 */
.revolver-stage.is-punched {
    filter: url(#fx-chroma) brightness(1.5) contrast(1.2) saturate(1.12);
}

/*
 * The muzzle flash is an SVG circle inside the revolver, pinned to the chamber
 * under the index mark, so it goes off over the round that fired rather than the
 * centre pin. Nothing here positions it: that is the point of moving it into the
 * SVG, and the reason it cannot drift out of place again.
 *
 * transform-box: fill-box makes the scale grow from the circle's own centre. On
 * an SVG element the default origin is the viewBox origin, so without it the
 * flash would sling away from the top-left corner as it blooms.
 */
.revolver-flash {
    opacity: 0;
    pointer-events: none;
    transform-box: fill-box;
    transform-origin: center;
}

/*
 * The rest of the flash, in the order it is felt: the blown-out core, the spikes
 * it throws, the shockwave leaving the muzzle, and the sheet of light that hides
 * the gun while all of that happens.
 *
 * These all take transform-origin in viewBox coordinates rather than
 * `transform-box: fill-box` like the bloom above, because the ray group's own box
 * is whatever the spikes happen to span this shot, so fill-box would grow it from
 * a centre that moves per shot. Naming the muzzle point outright is the only
 * version that stays correct when the geometry is re-rolled.
 */
.revolver-core,
.revolver-rays,
.revolver-shock,
.revolver-occlude {
    opacity: 0;
    pointer-events: none;
    transform-origin: 100px 62px;
}

/*
 * The shockwave stays a hairline as it expands. Scaling a stroked circle scales
 * its stroke too, so the ring got fatter as it grew and read as another bloom;
 * non-scaling-stroke is what makes it read as a geometric edge travelling
 * outwards, which is the one thing in the whole effect that is crisp.
 */
.revolver-shock {
    vector-effect: non-scaling-stroke;
}

/* the bloom itself is fireMuzzleFlash() in roulette.js, driven by the Web
   Animations API so its timing cannot be rewritten from a stylesheet */

/*
 * The single deliberate exception to the no-glow rule in tokens.css. A muzzle
 * flash is an actual light source, and it is on screen for 300ms; drawing it
 * as linework would read as a mistake rather than as a choice.
 */

.roulette-result {
    min-height: 36px;
    margin: var(--sp-2) 0 var(--sp-4);
    font-family: var(--font-engraved);
    font-weight: 900;
    font-size: 34px;
    letter-spacing: .04em;
    opacity: 0;
    transform: translateY(8px);
    transition: opacity var(--t-med), transform var(--t-med) var(--ease-back);
}

.roulette-result.is-shown {
    opacity: 1;
    transform: translateY(0);
}

.roulette-result.is-bang {
    color: var(--oxblood-hot);
    text-shadow: 3px 3px 0 #000;
}

.roulette-result.is-click {
    color: #9dbf94;
    text-shadow: 3px 3px 0 #000;
}

/* ---------------- Game over ---------------- */

/* deco divider above the result, tinted by outcome */
.gameover-ornament {
    display: block;
    width: 220px;
    height: 40px;
    margin: 0 auto var(--sp-3);
    fill: none;
    stroke: currentColor;
    stroke-width: 1.2;
    color: var(--gold);
    opacity: .85;
}

.gameover-ornament.is-good {
    color: var(--amber-hot);
}

.gameover-ornament.is-danger {
    color: var(--red-hot);
}

/* ---------------- The ledger ---------------- */

/*
 * The night, accounted for. Set left-aligned inside a centred dialog on
 * purpose: the title above it is a verdict and reads as one, but this is a
 * document, and a centred column of figures is unreadable as either.
 */
.ledger {
    margin-top: var(--sp-5);
    padding-top: var(--sp-4);
    text-align: left;
    border-top: 1px solid var(--line);
}

.ledger[hidden] {
    display: none;
}

.ledger__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-3);
    margin-bottom: var(--sp-3);
}

.ledger__title {
    font-family: var(--font-engraved);
    font-size: 19px;
    font-weight: 700;
    letter-spacing: .02em;
    color: var(--gold);
}

.ledger__rounds {
    font-size: 12px;
    font-weight: 800;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--text-faint);
}

/* ---- honours ---- */

.ledger__honours:not(:empty) {
    display: grid;
    gap: var(--sp-2);
    margin-bottom: var(--sp-4);
}

.honour {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 2px var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    background: var(--ink-800);
    background-image: var(--hatch-fine);
    border: 1px solid var(--rule-faint);
    border-left: 3px solid var(--seat-color, var(--rule));
}

.honour__label {
    grid-row: 1;
    align-self: baseline;
    font-size: 11px;
    font-weight: 800;
    letter-spacing: .16em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--text-faint);
}

.honour__name {
    grid-row: 1;
    font-family: var(--font-engraved);
    font-size: 16px;
    font-weight: 700;
    color: var(--seat-color, var(--gold));
}

/* runs the full width under both, rather than squeezing into the second column */
.honour__line {
    grid-row: 2;
    grid-column: 1 / -1;
    font-size: 13px;
    font-style: italic;
    color: var(--text-dim);
}

/* ---- the table ---- */

/*
 * The figures are never truncated and never given a scroller of their own. The
 * dialog box is already the one thing on this screen that scrolls, and a second
 * scrollport nested inside it meant a short window could hide rows behind an
 * inner scrollbar most people never found. Everything is rendered at full
 * length; `.modal__box` carries the overflow for all of it.
 */
.ledger__figures {
    border: 1px solid var(--rule-faint);
}

.ledger__table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

.ledger__table th {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: 8px 10px;
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: .14em;
    text-transform: uppercase;
    text-align: left;
    white-space: nowrap;
    color: var(--text-faint);
    background: var(--ink-800);
    border-bottom: 1px solid var(--rule);
}

.ledger__table td {
    padding: 8px 10px;
    border-bottom: 1px solid var(--rule-faint);
}

.ledger__table tr:last-child td {
    border-bottom: 0;
}

.ledger__num {
    width: 1%;
    text-align: right;
    font-family: var(--font-mono);
    font-size: 12px;
    white-space: nowrap;
}

.ledger__table th.ledger__num {
    text-align: right;
}

.ledger__place {
    color: var(--text-faint);
}

/* first place is the only row that gets the engraved voice */
.ledger__table tr:first-child .ledger__place {
    font-family: var(--font-engraved);
    font-size: 15px;
    color: var(--gold);
}

.ledger__who {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-weight: 600;
    color: var(--text);
}

.ledger__chip {
    width: 3px;
    height: 15px;
    flex: none;
    background: var(--seat-color, var(--rule));
}

.ledger__table tr.is-me .ledger__who {
    color: var(--gold-hot);
}

.rematch {
    margin-top: var(--sp-5);
    padding-top: var(--sp-4);
    border-top: 1px solid var(--line);
}

.rematch__label {
    display: block;
    margin-bottom: var(--sp-3);
    font-size: 12px;
    font-weight: 800;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--text-faint);
}

.rematch__list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    justify-content: center;
}

.rematch__chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 15px;
    font-size: 13.5px;
    font-weight: 600;
    background: var(--ink-800);
    border: 1px solid var(--rule-faint);
    border-left: 3px solid var(--seat-color, var(--rule));
    border-radius: 0;
    transition: color var(--t-med), background var(--t-med), border-color var(--t-med);
}

.rematch__chip .ico {
    width: 12px;
    height: 12px;
    stroke-width: 2.4;
}

.rematch__chip.is-ready {
    color: #9dbf94;
    background: rgba(110, 143, 106, .12);
    border-color: rgba(110, 143, 106, .5);
}

/* ---------------- Rules ---------------- */

.rules {
    margin: var(--sp-4) 0 var(--sp-5);
    text-align: left;
}

.rules__intro {
    margin-bottom: var(--sp-4);
    font-size: 16px;
    font-style: italic;
    text-align: center;
    color: var(--text-dim);
}

.rule {
    display: flex;
    gap: var(--sp-3);
    padding: var(--sp-3) 0;
    border-top: 1px solid var(--line);
}

.rule__num {
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    flex: none;
    font-family: var(--font-engraved);
    font-weight: 700;
    font-size: 17px;
    color: var(--gold);
    background: transparent;
    border: 1px solid var(--rule);
    border-radius: 0;
}

.rule h3 {
    margin-bottom: 4px;
    font-size: 15.5px;
    color: var(--text);
}

.rule p,
.rule li {
    font-size: 15px;
    line-height: 1.55;
    color: var(--text-dim);
}

.rule p+p {
    margin-top: var(--sp-2);
}

/* ---------------- Toasts ---------------- */

#toast-stack {
    position: fixed;
    top: max(var(--sp-4), env(safe-area-inset-top));
    left: 50%;
    transform: translateX(-50%);
    z-index: 1200;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-2);
    width: min(470px, calc(100vw - 32px));
    pointer-events: none;
}

.toast {
    max-width: 100%;
    padding: 12px 18px;
    font-size: 14.5px;
    font-weight: 600;
    text-align: center;
    color: var(--text);
    background: #191520;
    border: 1px solid var(--line-strong);
    border-left: 3px solid var(--amber);
    border-radius: 0;
    box-shadow: var(--sh-3);
    animation: toastIn .26s var(--ease-out) both;
}

.toast.is-leaving {
    animation: toastOut .24s var(--ease-out) forwards;
}

.toast--error {
    border-left-color: var(--red);
    color: #ffd8dd;
}

.toast--success {
    border-left-color: var(--green);
    color: #d7ffe8;
}

.toast--info {
    border-left-color: var(--cyan);
}

@keyframes toastIn {
    from {
        opacity: 0;
        transform: translateY(-12px);
    }
}

@keyframes toastOut {
    to {
        opacity: 0;
        transform: translateY(-8px);
    }
}

/* ---------------- Server status pill ---------------- */

.server-status {
    position: fixed;
    right: var(--sp-4);
    bottom: max(var(--sp-4), env(safe-area-inset-bottom));
    z-index: 1100;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 9px 16px;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-dim);
    background: #16131d;
    border: 1px solid var(--line-strong);
    border-radius: 0;
    box-shadow: var(--sh-2);
    pointer-events: none;
    transition: opacity var(--t-med), transform var(--t-med) var(--ease-out);
}

.server-status.is-hidden {
    opacity: 0;
    transform: translateY(10px);
}

.server-status__dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--gold);
    animation: dotPulse 1.3s var(--ease-in-out) infinite;
}

.server-status[data-state="on"] .server-status__dot {
    background: var(--verdigris);
    animation: none;
}

.server-status[data-state="off"] .server-status__dot {
    background: var(--oxblood);
}

@keyframes dotPulse {

    0%,
    100% {
        opacity: .35;
    }

    50% {
        opacity: 1;
    }
}

/*
 * Connected, but not to the table. The one state the pill exists for: a socket
 * that came back without its room membership looks fine at the transport level
 * and is doing nothing at all for the game.
 */
.server-status[data-state="warn"] .server-status__dot {
    background: var(--gold);
}

.server-status[data-state="warn"] {
    color: var(--gold-hot);
}

/*
 * The action bar while the wire is down. Plays made here are queued rather than
 * lost, so this greys rather than disables: taking the buttons away would be
 * claiming to know the connection is not coming back in the next second, which
 * it usually is.
 */
body[data-link="offline"] #dock,
body[data-link="reseating"] #dock {
    opacity: .62;
}

/* Why the rematch is not starting, when it isn't. */
.rematch__note {
    font-size: 12px;
    color: var(--text-dim);
}
