/* Shell Challenge — jumbotron module styles.
   Extracted from shell.html. Edit this file AND regenerate shell.min.css:
   the page loads the .min version. */

/* ------------------------------------------------------------------
   Animation character — all themable from the per-game JSON
   (data.fr.intro.*) so a client can dial the feel without a code
   branch. JS reads these back via getComputedStyle at build time.
   ------------------------------------------------------------------ */
body {
    --shell-arc-height: 90px;
    /* how far a shell arcs off its path mid-swap */
    --shell-depth-scale: 0.12;
    /* front/back size difference that sells the crossing */
    --shell-squash: 0.07;
    /* squash & stretch amount on landing */
    --shell-motion-blur-max: 7px;
    /* blur ceiling at top swap velocity */
    --shell-lift-height: 210px;
    /* how high the shell rises on reveal (clamped at runtime to the
       headroom actually available above the shell — see safeLift()) */
    --shell-tilt: 11deg;
    /* shell tilt-back on reveal */
    --shell-place-hold: 450ms;
    /* how long the ball sits visible under the raised shell before the
       shell drops and the shuffle begins. This is the entire perceived
       wait after the operator presses Space, so keep it short. */
    --shell-shadow-color: rgba(0, 0, 0, 0.45);
    /* Shadow geometry is themable because client shell art differs:
       it must sit at the visual base and spread WIDER than the
       silhouette, or the shell simply covers it up. */
    /* Font stack for any UI string that may carry accents — the
       AgencyFB brand font renders them broken. */
    /* Overall size of the shell prop, set from intro.shell_scale. */
    --shell-scale: 1;

    --shell-shadow-top: 486px;
    --shell-shadow-width: 380px;
    --shell-shadow-height: 52px;

    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* !important is required, not decorative: shell.html also loads
       fanwall.min.css, which carries `body{height:120vh!important}`.
       Combined with overflow:hidden below that pushed the bottom 20vh of
       the layout — the letters and the crowd-vote rows — off screen. */
    height: 100vh !important;
    margin: 0;
    overflow: hidden;
    background-color: var(--background-color, #000);
    background-image: var(--background-image);
    background-position: center !important;
    background-size: cover !important;
}

/* Bottom-anchored: the shells sit low on screen, which puts them in the
   jumbotron's most-visible band and leaves headroom above for the
   reveal lift (see safeLift()). */
.game-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    text-align: center;
    width: 100%;
    height: 100%;
    padding-bottom: 2vh;
    box-sizing: border-box;
    position: relative;
}

/* Stage wrapper: scales the fixed 1240px layout to the actual screen
   so the module is not locked to a 1080p jumbotron. */
/* The shell art sits at top:100px inside a 450px box, so it overflows
   100px below. The stage is sized to contain the art AND the letter
   band underneath it, rather than letting either spill. */
/* ---------------- Standby screen ----------------
   Browsers block audio until a real user gesture. The jumbotron
   operator clicks this once at the start of the session, which
   unlocks the AudioContext for every round that follows. */
.standby {
    position: fixed;
    inset: 0;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2.5rem;
    text-align: center;
    cursor: pointer;
    background: radial-gradient(ellipse at center,
            rgba(0, 0, 0, 0.72) 0%,
            rgba(0, 0, 0, 0.94) 100%);
    transition: opacity 0.5s ease, visibility 0.5s;
}

.standby.is-hidden {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
}

.standby-logo {
    max-width: 340px;
    max-height: 170px;
    object-fit: contain;
}

.standby-title {
    font-family: var(--font-family);
    color: var(--secondary-color, #fff);
    font-size: 4.4rem;
    line-height: 1.05;
    text-transform: uppercase;
    margin: 0;
}

.standby-cta {
    /* NOT var(--font-family): the AgencyFB brand font has broken GPOS
       and mangles accented glyphs, and this string is accented in
       fr/pt/tr/el/sv/hr. Same reason as #instruction below. */
    font-family: var(--font-family);
    color: var(--primary-color);
    font-size: 2.1rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    padding: 1rem 3rem;
    border: 3px solid var(--primary-color);
    border-radius: 999px;
    animation: standbyPulse 1.9s ease-in-out infinite;
}

@keyframes standbyPulse {

    0%,
    100% {
        transform: scale(1);
        opacity: 0.82;
    }

    50% {
        transform: scale(1.06);
        opacity: 1;
    }
}

/* ---------------- Top band ----------------
   Bottom-anchoring the shells freed this space; it carries the live
   player count and the guess prompt. */
#players {
    position: absolute;
    top: 22px;
    left: 26px;
    z-index: 60;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-family: var(--font-family);
    font-size: 1.9rem;
    color: var(--primary-color);
    opacity: 0;
}

#players svg {
    width: 1.6rem;
    height: 1.6rem;
    fill: currentColor;
}

/* Sponsor lockup — occupies the headroom above the shells and fades in on
   the reveal, when the whole room is looking at the result. Uses the same
   intro.sponsor_image / intro.sponsor_text keys as aicam-jumbotron, and is
   only shown when the client actually configured an image. */
#sponsor {
    position: absolute;
    top: 6vh;
    left: 0;
    right: 0;
    z-index: 45;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.7rem;
    opacity: 0;
    pointer-events: none;
}

#sponsor_text {
    /* May carry accents ("Offert par…") — keep off the brand font. */
    font-family: var(--safe-font);
    font-size: 1.5rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--secondary-color, #fff);
    opacity: 0.75;
}

#sponsor_logo {
    max-width: 380px;
    max-height: 130px;
    object-fit: contain;
}

#instruction {
    position: absolute;
    /* Bottom band, where #message used to sit. Not `top`, so the height of
       the stage above it never pushes it around. */
    bottom: 4vh;
    left: 0;
    right: 0;
    z-index: 40;
    /* shell.guess is accented in several locales ("Où est-elle ?"),
       so this must not use the brand font. */
    font-family: var(--font-family);
    font-size: 2.6rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--secondary-color);
    /* Dark until the preload ends — shellPreloadDone() fades in "Be ready!",
       and showInstruction() owns the opacity from there on. Announcing
       "ready" over a model that is still downloading would be a lie. */
    opacity: 0;
    pointer-events: none;
}

.stage {
    /* 1280 = 3 shells x 400px + 2 x 40px gap. At 1240 the third shell was
       clipped by 40px and the group sat 20px left of centre. Must match
       .helmets below. */
    width: 1280px;
    height: 750px;
    /* flex-shrink must be off: .game-container is a COLUMN flex box, so the
       stage's height is on the main axis and flexbox was quietly shrinking
       it (to ~697px) whenever the column overflowed. fitStage() reads this
       height to size everything, so a height that moves under it made the
       scale and the margin compensation disagree. */
    flex: none;
    position: relative;
    margin: 0 auto;
    transform: scale(var(--stage-scale, 1));
    transform-origin: center bottom;
}

.helmets {
    width: 1280px;
    height: 700px;
    position: relative;
    margin: 0 auto;
}

/* The playfield stays hidden until introDrop() runs, which is only after
   the operator dismisses the standby screen.
   This is not belt-and-braces: .helmet is absolutely positioned at left:0
   and gets its slot from anime's translateX, which introDrop() is the
   first thing to set. Until then all three shells are stacked in slot A —
   and the standby overlay is deliberately semi-transparent, so that stack
   was showing through it as a single stray shell.
   visibility (not opacity/display) so it covers the 3D canvas too and
   leaves layout intact for the measurements safeLift() takes. */
.helmets:not(.is-ready) {
    visibility: hidden;
}

/* ---------------- 3D renderer (optional) ----------------
   Enabled only when intro.shell_model points at a .glb. The canvas sits
   behind the .helmet boxes, which stay in the DOM as invisible "puppets":
   anime.js keeps animating them exactly as in 2D, and the 3D layer mirrors
   their transforms each frame. Letters, vote bars and the ball glow remain
   DOM on top. */
#shell3d {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 0;
    pointer-events: none;
    display: none;
}

.helmets.is-3d #shell3d {
    display: block;
}

/* Hide the 2D artwork — but keep the boxes themselves, since they carry the
   letters, the vote bars, the click targets and the animation state. */
.helmets.is-3d .helmet-lift,
.helmets.is-3d .helmet-shadow,
.helmets.is-3d .ball img {
    visibility: hidden;
}

/* .helmet owns slot position + swap arc + depth scale (driven by
   anime.js). Nothing else may write its transform. */
.helmet {
    width: 400px;
    height: 450px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: default;
    position: absolute;
    top: 0;
    left: 0;
    backface-visibility: hidden;
}

/* will-change is toggled around the shuffle only — leaving three
   permanent compositor layers pinned costs GPU memory for nothing. */
.helmets.is-animating .helmet,
.helmets.is-animating .helmet-lift {
    will-change: transform;
}

/* .helmet-lift owns the reveal lift + tilt, pivoting at the base. */
.helmet-lift {
    position: absolute;
    inset: 0;
    transform-origin: 50% 92%;
    z-index: 2;
}

/* .helmet-squash owns landing squash & stretch, pivoting at the base. */
.helmet-squash {
    position: absolute;
    inset: 0;
    transform-origin: 50% 92%;
}

.helmet img.helmet-img {
    width: 100%;
    height: 100%;
    max-width: 550px;
    object-fit: contain;
    position: relative;
    top: 100px;
    transform: scale(var(--shell-scale, 1));
    transform-origin: 50% 92%;
    filter: blur(var(--motion-blur, 0px));
    transition: filter 110ms linear, transform 220ms ease-out;
}

/* Hover lives on the <img>, which anime.js never touches, so it can
   never fight the swap/lift transforms. Only offered while guessing. */
.helmets.can-guess .helmet {
    cursor: pointer;
}

.helmets.can-guess .helmet:hover img.helmet-img {
    transform: scale(calc(var(--shell-scale, 1) * 1.05));
}

/* Ground shadow — this is what makes a lift read as 3D rather than
   as a sprite sliding upward. */
.helmet-shadow {
    position: absolute;
    left: 50%;
    top: var(--shell-shadow-top, 486px);
    width: calc(var(--shell-shadow-width, 380px) * var(--shell-scale, 1));
    height: calc(var(--shell-shadow-height, 52px) * var(--shell-scale, 1));
    margin-left: calc(var(--shell-shadow-width, 380px) * var(--shell-scale, 1) / -2);
    border-radius: 50%;
    background: radial-gradient(ellipse at center,
            var(--shell-shadow-color) 0%,
            rgba(0, 0, 0, 0.22) 45%,
            rgba(0, 0, 0, 0) 72%);
    z-index: 0;
    pointer-events: none;
}

/* Letters sit UNDER each shell, below the ground shadow, so they read
   as labels for the shells and never collide with the lifted shell
   on reveal. */
.helmet .letter {
    position: absolute;
    top: var(--shell-letter-top, 556px);
    font-size: 3.2rem;
    line-height: 1;
    font-weight: bold;
    font-family: var(--font-family);
    color: var(--primary-color);
    width: 100%;
    text-align: center;
    z-index: 3;
    text-shadow: 0 3px 12px rgba(0, 0, 0, 0.65);
}

/* Crowd vote bars — the phones already publish every guess on
   shellTopics.answers; this renders what the room actually picked.
   Sits under its own shell, so it travels with it just like the
   letter does. */
.helmet .votes {
    position: absolute;
    /* Anchored BELOW the letter rather than at an independent offset: the
       letter box is ~90px tall, so a fixed 620px collided with it. Deriving
       from --shell-letter-top also means a client that moves the letter
       (animation_letter_top) moves the vote rows with it. --shell-votes-top
       still overrides outright if one ever needs to be placed by hand. */
    top: var(--shell-votes-top, calc(var(--shell-letter-top, 556px) + 104px));
    left: 50%;
    width: 300px;
    margin-left: -150px;
    z-index: 3;
    opacity: 0;
    pointer-events: none;
}

.votes-bar {
    height: 14px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.14);
    overflow: hidden;
}

.votes-bar i {
    display: block;
    height: 100%;
    width: 0;
    border-radius: 999px;
    background: var(--primary-color);
}

.helmet.is-winner .votes-bar i {
    background: var(--third-color, #3ad07a);
}

/* Share and head-count sit on ONE row. A second line would push the block
   past the 700px .helmets box on the shortest configured stages, and the
   two numbers describe the same thing anyway. */
.votes-figures {
    margin-top: 0.45rem;
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 0.7rem;
}

.votes-pct {
    font-family: var(--font-family);
    font-size: 1.7rem;
    font-weight: bold;
    color: var(--secondary-color);
}

/* Deliberately quieter than the percentage: at jumbotron distance the
   share is the headline and the head-count is the supporting detail. */
.votes-count {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-family: var(--font-family);
    font-size: 1.15rem;
    line-height: 1;
    color: var(--secondary-color);
    opacity: 0.72;
}

.votes-count svg {
    width: 1.05em;
    height: 1.05em;
    fill: currentColor;
    flex: none;
}

.votes-count i {
    font-style: normal;
}

.helmet.is-winner .votes-pct,
.helmet.is-winner .votes-count {
    color: var(--third-color, #3ad07a);
    opacity: 1;
}

/* The ball sits UNDER the shell (z-index below .helmet-lift) so the
   reveal exposes it rather than floating it on top. */
.ball {
    width: 200px;
    height: 200px;
    position: absolute;
    left: 50%;
    margin-left: -100px;
    top: 320px;
    z-index: 1;
    display: none;
    opacity: 0;
}

.ball img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    position: relative;
    z-index: 2;
}

/* Rim-light glow behind the ball on reveal. */
.ball-glow {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 320px;
    height: 320px;
    margin: -160px 0 0 -160px;
    border-radius: 50%;
    background: radial-gradient(circle at center,
            var(--primary-color, #fff) 0%,
            rgba(255, 255, 255, 0) 62%);
    opacity: 0;
    z-index: 1;
    pointer-events: none;
    mix-blend-mode: screen;
}


h1 {
    font-size: 3rem;
    color: #222;
    position: absolute;
    top: 1rem;
    margin: 0;
    /* Above #players (60) and the stage layers: the title is the fullscreen
       toggle, so nothing may sit over its hit area and swallow the click. */
    z-index: 70;
    cursor: pointer;
}

#header_logo {
    position: absolute;
    top: 20px;
    right: 20px;
    width: 180px;
    height: auto;
    object-fit: contain;
    z-index: 50;
}

/* Reduced motion: keep the game readable, drop the flourish. The JS
   also zeroes arcs/blur/squash and shortens durations. */
@media (prefers-reduced-motion: reduce) {

    .standby-cta {
        animation: none;
    }

    .helmet img.helmet-img {
        filter: none;
        transition: none;
    }

    .helmets.can-guess .helmet:hover img.helmet-img {
        transform: none;
    }
}
