html, body {
    font-family: 'Global Monospace', Helvetica, Arial, sans-serif;
    height: 100%;
    margin: 0;
    /* Keep the whole app confined to one screen; only .main-page-content scrolls. */
    overflow: hidden;
}

/* main layout */
.main-page {
    display: flex;
    flex-direction: column;
    max-width: 100%;
    min-width: 100%;
    height: 100vh;
    height: 100dvh;
}

.main-page-content {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    display: flex;
    flex-direction: column;
}

.ktf-game {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

.ktf-loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.6em;
    padding: 2em;
    text-align: center;
}

.ktf-loading-icon {
    position: static;
    width: 56px;
    height: auto;
}

/*scoreboard*/
.score-tray {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: center;
    gap: 0.9em;
    max-width: 480px;
    width: max-content;
    margin: 0.3em auto;
    padding: 0.35em 0.8em;
    background: #fff7ec;
    border: 1px solid #e0c9a6;
    border-radius: 999px;
    box-shadow: 0 1px 3px rgba(0,0,0,.15);
    font-weight: bold;
    font-size: 0.95em;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

.score-icon {
    width: auto;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.3em;
    flex-shrink: 0;
    white-space: nowrap;
}

/* On narrow screens, shrink gaps/font/icon size instead of wrapping to a second line */
@media (max-width: 480px) {
    .score-tray {
        gap: 0.45em;
        padding: 0.3em 0.5em;
        font-size: 0.8em;
    }

    .score-icon img {
        width: 20px !important;
        height: 20px !important;
    }
}

.score-coords {
    color: #4a4033;
    border-left: 1px solid #e0c9a6;
    padding-left: 0.9em;
}

/*banner*/
.banner-photo {
    max-width: 100%;
    min-width: 100%;
    max-height: 12vh;
    object-fit: cover;
    flex: 0 0 auto;
}

/*photo manager (admin tool to upload/slice map photos)*/
.photo-manager {
    max-width: 500px;
    width: 100%;
    box-sizing: border-box;
    margin: 1em auto;
    padding: 0 0.6em 1em;
    /* Let the single .main-page-content scroll container handle scrolling (same
       pattern as .weather-forecast) instead of nesting a second overflow:auto
       box in here - having two independent scroll containers made scrolling feel
       broken/janky, with content getting clipped between the two. */
}

.photo-manager-section {
    background: rgba(0, 0, 0, 0.035);
    border: 1px solid rgba(0, 0, 0, 0.08);
    border-radius: 10px;
    padding: 0.8em;
    margin-bottom: 1em;
}

.photo-manager-section-title {
    margin: 0 0 0.6em;
    font-size: 1em;
}

.photo-manager-toolbar {
    margin-top: 0.6em;
}

.photo-manager-banner-preview {
    display: block;
    max-width: 100%;
    max-height: 120px;
    object-fit: cover;
    border-radius: 6px;
    margin-bottom: 0.6em;
}

.photo-manager-toolbar-group {
    display: flex;
    flex-direction: column;
    gap: 0.35em;
}

.photo-manager-select-row {
    display: flex;
    align-items: center;
    gap: 0.4em;
}

.photo-manager-select {
    flex: 1 1 auto;
    min-width: 0;
    padding: 0.4em 0.5em;
    border-radius: 6px;
    border: 1px solid rgba(0, 0, 0, 0.2);
    background: #fff;
    font-size: 0.95em;
}

.photo-manager-divider {
    height: 1px;
    background: rgba(0, 0, 0, 0.1);
    margin: 0.8em 0;
}

/* Small, round icon-only action buttons (used next to selects/inputs where a
   full text .photo-button would be too heavy). */
.icon-button {
    flex: 0 0 auto;
    cursor: pointer;
    border: none;
    border-radius: 6px;
    background: #e0e0e0;
    width: 2.2em;
    height: 2.2em;
    font-size: 1em;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.icon-button:hover {
    background: #cfcfcf;
}

.icon-button:disabled {
    opacity: 0.5;
    cursor: default;
}

.icon-button-primary {
    background: #dd8787;
}

.icon-button-primary:hover {
    background: #f47c20;
}

.icon-button-danger {
    background: #c65b5b;
    color: #fff;
}

.icon-button-danger:hover {
    background: #a83f3f;
}

/* File input disguised as a button + filename label, replacing the raw/ugly
   native file picker (which renders very differently and clunkily across browsers). */
.icon-file-button {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.4em;
    cursor: pointer;
    background: #eee;
    border: 1px dashed rgba(0, 0, 0, 0.3);
    border-radius: 6px;
    padding: 0.4em 0.6em;
    font-size: 0.9em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    flex: 1 1 auto;
    min-width: 0;
}

.icon-file-button:hover {
    background: #e2e2e2;
}

.icon-file-button-wide {
    width: 100%;
    box-sizing: border-box;
}

.photo-manager-file-input-hidden {
    position: absolute;
    inset: 0;
    opacity: 0;
    cursor: pointer;
    width: 100%;
}

.photo-manager-upload-grid {
    display: flex;
    gap: 0.8em;
    align-items: flex-start;
    flex-wrap: wrap;
}

.photo-manager-upload-controls {
    flex: 1 1 160px;
    min-width: 160px;
    display: flex;
    flex-direction: column;
    gap: 0.5em;
}

.photo-manager-form {
    display: flex;
    flex-direction: column;
    gap: 0.7em;
}

.photo-manager-field {
    display: flex;
    flex-direction: column;
    gap: 0.3em;
}

.photo-manager-label {
    font-size: 0.85em;
    font-weight: bold;
    opacity: 0.85;
}

.photo-manager-slider-label {
    display: flex;
    align-items: center;
    gap: 0.5em;
    font-size: 0.85em;
}

.photo-manager-slider-label input[type="range"] {
    flex: 1 1 auto;
    min-width: 0;
}

.photo-manager-error {
    color: #b32121;
}

.photo-manager-hint {
    font-size: 0.85em;
    opacity: 0.8;
    margin: 0.2em 0 0;
}

.photo-crop-preview {
    width: 100%;
    max-width: 140px;
    aspect-ratio: 1/1;
    overflow: hidden;
    margin: 0;
    border-radius: 6px;
    flex: 0 0 auto;
}

.photo-crop-preview img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.photo-manager-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5em;
}

.photo-manager-name-input {
    width: 100%;
    box-sizing: border-box;
    min-width: 0;
    padding: 0.4em 0.5em;
    border-radius: 6px;
    border: 1px solid rgba(0, 0, 0, 0.2);
}

/* Ad-tile admin grid: 22x22 = the 20x20 playable map (shown as a plain visual
   reference) surrounded by a 1-tile ring of clickable "wall" squares, matching
   the exact layout the game renders (see MapService.MAP_SIZE + emptyChar). */
.ad-grid-wrapper {
    width: 100%;
    max-width: 420px;
    margin: 0.5em auto 0;
    box-sizing: border-box;
}

.ad-grid {
    display: grid;
    grid-template-columns: repeat(22, 1fr);
    grid-gap: 1px;
    width: 100%;
    aspect-ratio: 1/1;
}

.ad-grid-map-tile {
    width: 100%;
    height: 100%;
    aspect-ratio: 1/1;
    object-fit: cover;
    display: block;
}

.ad-grid-map-tile-empty {
    background-color: #d8e8c8;
}

.ad-grid-map-tile-wrapper {
    position: relative;
    aspect-ratio: 1/1;
    width: 100%;
    height: 100%;
}

.ad-grid-player-marker {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.9em;
    line-height: 1;
    text-shadow: 0 0 2px #000, 0 0 2px #000;
    pointer-events: none;
}

.ad-grid-wall {
    aspect-ratio: 1/1;
    background-color: brown;
    cursor: pointer;
    display: block;
}

.ad-grid-wall img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.ad-grid-wall-has-ad {
    outline: 2px solid gold;
    outline-offset: -2px;
}

.ad-grid-wall-selected {
    outline: 3px solid dodgerblue;
    outline-offset: -3px;
}

@media (max-width: 420px) {
    .ad-grid-wrapper {
        max-width: 100%;
    }
}

/* photos button */

.photo-button {
    cursor: pointer;
    background: #dd8787;
    text-align: center;
    padding: .5em 2em .55em;
    font-size: large;
    text-shadow: 0 1px 1px rgba(0,0,0,.3);
    border-radius: 0;
}

.photo-button:hover {
    background: #f47c20;
    background: -webkit-gradient(linear, left top, left bottom, from(#f88e11), to(#f06015));
    background: -moz-linear-gradient(top,  #f88e11,  #f06015);
    filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#f88e11', endColorstr='#f06015');
}

.photo-button:active {
    position: relative;
    top: 1px;
}

.photo-button:disabled {
    opacity: 0.55;
    cursor: default;
    filter: none;
}

/* Online/Local mode toggle: a fixed "Online" label + an actual on/off switch knob, so
   it's clear that "on" = Online and "off" = Local, instead of the label text itself
   flipping between "Modo: Online"/"Modo: Local" (which was less immediately readable). */
.game-mode-toggle {
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 0.35em;
    background: transparent;
    padding: 0;
    box-shadow: none;
}

.game-mode-label {
    font-size: 0.85em;
    font-weight: bold;
}

.game-mode-switch {
    position: relative;
    display: inline-block;
    width: 2.2em;
    height: 1.2em;
    border-radius: 999px;
    background: #9aa5b1;
    transition: background-color 0.2s ease;
    flex: 0 0 auto;
}

.game-mode-switch.on {
    background: #2ecc71;
}

.game-mode-switch-knob {
    position: absolute;
    top: 0.12em;
    left: 0.12em;
    width: 0.96em;
    height: 0.96em;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 2px rgba(0,0,0,.3);
    transition: left 0.2s ease;
}

.game-mode-switch.on .game-mode-switch-knob {
    left: 1.12em;
}

/* The logout tab is a real <button> (so it stays keyboard-reachable) that has to look
   exactly like the emoji <div> tabs beside it: no native border/font, no grey box, and
   the same flex/padding/background .menu-bar .photo-button already gives every tab. */
.logout-button {
    border: none;
    font-family: inherit;
    color: inherit;
}

.menu-bar {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    align-items: stretch;
    /* Setting overflow-x:auto implicitly makes overflow-y auto too, which removes the
       flex item's automatic minimum size protection and lets tall sibling content (e.g. the
       Weather tab) shrink this bar to 0 height in the vertical .main-page-content flex column.
       Prevent that by disallowing shrink and forcing a sane minimum height. */
    flex-shrink: 0;
    min-height: 2.6em;
}

.menu-bar .photo-button {
    flex: 1 1 0;
    min-width: 0;
    padding: .5em 1em .55em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.menu-icon {
    width: 1.4em;
    height: 1.4em;
    vertical-align: middle;
    image-rendering: pixelated;
}

/* Forces the top emoji/icon menu bar to always stay on one line, even on narrow phones with
   several tabs enabled (Jugar/Pronóstico/Mis olas/Tablero/Admin) — shrinks padding/font instead
   of wrapping to a second row, with horizontal scroll as a last-resort fallback if it still can't
   all fit. */
@media (max-width: 480px) {
    .menu-bar .photo-button {
        padding: .35em .5em .4em;
        font-size: 0.85em;
    }

    .menu-bar .menu-icon {
        width: 1.15em;
        height: 1.15em;
    }
}

/* Login/logout is one more tab of the menu bar, so it shares the bar's flex sizing
   instead of being a fixed-width island next to it. display:contents takes this wrapper
   out of the layout entirely, so the button inside it is a direct flex item of .menu-bar
   and gets exactly the same width as every other tab. Nesting it in a flex box instead
   made it ~2em wider than its neighbours (the tab padding is added on top of the shared
   flex basis), which is the kind of "almost aligned" the report was about. */
.google-signin-area {
    display: contents;
}

/* Hosts Google's own rendered button (visible, as its branding guidelines require) and
   makes it sit like the emoji tabs around it: same flex share, same background, centred.
   Scoped to .menu-bar so it can actually override the vertical padding of
   `.menu-bar .photo-button` (same specificity, declared later): the rendered button is a
   fixed 40px tall, and the tab's usual .5em/.55em would be added on top of that and make
   this tab ~18px taller than the rest of the bar. */
.menu-bar .google-signin-tab {
    padding: 0 1em;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Google's button has a fixed size; clip it rather than let it overhang (and swallow
       clicks meant for) the tab next door on very narrow screens. */
    overflow: hidden;
}

.google-signin-unavailable {
    border: none;
    font-family: inherit;
    color: inherit;
    opacity: .55;
}

.photo-button.active {
    background: #b85c5c;
    box-shadow: inset 0 0 0 3px rgba(0,0,0,.25);
}

.player-list {
    width: 100%;
    border-collapse: collapse;
}

.player-list th,
.player-list td {
    border: 1px solid #ccc;
    padding: .4em .6em;
    text-align: left;
}

.player-list td {
    overflow-wrap: break-word;
}

/* An email has no spaces to break on, so it is the one thing allowed to split mid-word;
   without it a single long address stretches its column and pushes the table sideways.
   The guid and the status labels must not break, or they turn into ragged stacks. */
.player-google small {
    overflow-wrap: anywhere;
}

.player-list td[data-label="Registrado"],
.player-list td[data-label="Estado"] {
    white-space: nowrap;
}

.player-no-google {
    color: #999;
}

.player-identity {
    display: flex;
    align-items: center;
    gap: .5em;
}

.player-avatar {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    object-fit: cover;
    flex: 0 0 auto;
    background: #ddd;
}

.player-avatar-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: .8em;
    font-weight: 700;
    color: #777;
}

.player-guid {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.player-google {
    display: flex;
    flex-direction: column;
}

/*leaderboard (own layout: a three-field ranking fits on one line at any width, so unlike
  the admin .player-list it never needs to collapse into cards)*/
.leaderboard-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.leaderboard-row {
    display: flex;
    align-items: center;
    gap: .7em;
    padding: .55em .7em;
    border: 1px solid #e0c9a6;
    border-radius: 8px;
    margin-bottom: .4em;
    background: #fff7ec;
}

.leaderboard-rank {
    flex: 0 0 auto;
    min-width: 1.9em;
    height: 1.9em;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    background: #ece2d3;
    color: #4a4033;
    font-weight: 700;
    font-size: .9em;
}

.leaderboard-rank-1 { background: #f4c542; color: #4a3a00; }
.leaderboard-rank-2 { background: #d8d8d8; color: #3d3d3d; }
.leaderboard-rank-3 { background: #d9a06a; color: #40260f; }

/* The name is the only elastic part, so it absorbs every width change and truncates
   rather than pushing the score off the row. */
.leaderboard-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.leaderboard-score {
    flex: 0 0 auto;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

/* Below this width the five columns cannot coexist, so each row becomes its own card and
   the per-cell data-label stands in for the (now hidden) shared header row. */
@media (max-width: 640px) {
    .player-list,
    .player-list tbody,
    .player-list tr,
    .player-list td {
        display: block;
        width: 100%;
        /* The card's own padding and border must fit inside that 100%, otherwise each row
           ends up wider than the table and spills past the right edge of the screen. */
        box-sizing: border-box;
    }

    .player-list thead {
        display: none;
    }

    .player-list tr {
        border: 1px solid #ccc;
        border-radius: 8px;
        margin-bottom: .6em;
        padding: .1em .7em;
    }

    .player-list td {
        border: none;
        border-bottom: 1px solid #eee;
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: .8em;
        padding: .5em 0;
        text-align: right;
    }

    /* The label keeps its intrinsic width; the value takes the rest and is allowed to
       shrink below its content size, which is what lets long emails wrap instead of
       widening the row. */
    .player-list td > * {
        min-width: 0;
    }

    .player-list td[data-label="Jugador"] .player-identity {
        justify-content: flex-end;
        flex: 1 1 auto;
    }

    .player-list td:last-child {
        border-bottom: none;
    }

    .player-list td::before {
        content: attr(data-label);
        font-weight: 600;
        color: #666;
        text-align: left;
        flex: 0 0 auto;
    }
}

/*blazor errors*/
#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

/*grid*/

.grid-square-wrapper {
    display: grid;
    place-items: center;
    flex: 1 1 auto;
    min-height: 0;
    min-width: 0;
    overflow: hidden;
}

.grid-container {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(3, 1fr);
    grid-gap: 0;
    overflow: hidden;
}

.grid-item {
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
    position: relative;
    /*padding: 10px;*/
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
}
.map-wall {
    background-color: brown;
    width: 100%;
    height: 100%;
}

/* Premium feature: shows a player's chosen display name under their sprite,
   visible to everyone in the shared world (see GameEntity.DisplayName). */
.player-name-tag {
    position: absolute;
    bottom: 2px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 0.55em;
    line-height: 1;
    color: #fff;
    background: rgba(0, 0, 0, 0.55);
    padding: 1px 4px;
    border-radius: 3px;
    white-space: nowrap;
    max-width: 95%;
    overflow: hidden;
    text-overflow: ellipsis;
    pointer-events: none;
}

/* ---------------------------------------------------------------------------
   Boot screen (wwwroot/index.html)

   What a visitor stares at while the Blazor WASM runtime downloads - before any
   component, service or stylesheet-of-ours can decide anything. It is therefore
   plain markup + these rules, themed per scenario through the [data-scenario]
   attribute the inline script in index.html sets from the hostname.

   Adding a scenario means adding a palette block below; a scenario with no block
   falls back to the matalamosca palette declared on .boot, the same way the
   markup itself defaults to the fly theme.
   --------------------------------------------------------------------------- */

.boot {
    /* matalamosca: a warm room with a lamp on - the default, see above. */
    --boot-sky-top: #fff8ea;
    --boot-sky-bottom: #edd6ae;
    --boot-glow: rgba(248, 142, 17, 0.42);
    --boot-accent: #f06015;
    --boot-accent-soft: #f8a72c;
    --boot-ink: #4a4033;
    --boot-card: rgba(255, 252, 245, 0.86);
    --boot-edge: #e0c9a6;
    --boot-horizon: rgba(184, 132, 74, 0.35);

    position: fixed;
    inset: 0;
    z-index: 900;
    display: flex;
    justify-content: center;
    box-sizing: border-box;
    padding: 1.25rem;
    /* The card is centred with margin:auto rather than align-items:center so that a
       viewport too short for it (a phone held sideways) scrolls instead of clipping
       the top off. The backdrop is painted on this fixed box and stays put. */
    overflow-x: hidden;
    overflow-y: auto;
    color: var(--boot-ink);
    background: linear-gradient(180deg, var(--boot-sky-top) 0%, var(--boot-sky-bottom) 100%);
    animation: boot-fade-in 0.45s ease both;
}

/* puntagruesa: sea under morning light. */
.boot[data-scenario="puntagruesa"] {
    --boot-sky-top: #eaf7fd;
    --boot-sky-bottom: #79b4d6;
    --boot-glow: rgba(255, 216, 138, 0.5);
    --boot-accent: #0e6c9c;
    --boot-accent-soft: #3fb0d8;
    --boot-ink: #123a52;
    --boot-card: rgba(248, 253, 255, 0.86);
    --boot-edge: #a9d0e4;
    --boot-horizon: rgba(16, 82, 118, 0.3);
}

/* marcagol: a pitch seen from the stands. */
.boot[data-scenario="marcagol"] {
    --boot-sky-top: #f4fcf0;
    --boot-sky-bottom: #93c97c;
    --boot-glow: rgba(255, 255, 255, 0.55);
    --boot-accent: #2c7a30;
    --boot-accent-soft: #6fbf4e;
    --boot-ink: #234023;
    --boot-card: rgba(251, 255, 249, 0.88);
    --boot-edge: #bfe0ad;
    --boot-horizon: rgba(40, 96, 40, 0.24);
}

/* parapente: high desert air over the coast, seen from above. */
.boot[data-scenario="parapente"] {
    --boot-sky-top: #e9f4fd;
    --boot-sky-bottom: #f0c489;
    --boot-glow: rgba(255, 236, 176, 0.55);
    --boot-accent: #c2681f;
    --boot-accent-soft: #f0a850;
    --boot-ink: #4b3320;
    --boot-card: rgba(255, 251, 243, 0.88);
    --boot-edge: #e3c69c;
    --boot-horizon: rgba(150, 96, 46, 0.28);
}

/* tumorcito: warm tissue lit from inside, the way an endoscope sees it. */
.boot[data-scenario="tumorcito"] {
    --boot-sky-top: #fdeef2;
    --boot-sky-bottom: #d0788f;
    --boot-glow: rgba(255, 214, 224, 0.55);
    --boot-accent: #a8203c;
    --boot-accent-soft: #e2506b;
    --boot-ink: #4a2530;
    --boot-card: rgba(255, 246, 248, 0.9);
    --boot-edge: #e8bcc7;
    --boot-horizon: rgba(120, 40, 62, 0.26);
}

/* tioweed: an indoor grow with the lamps on. */
.boot[data-scenario="tioweed"] {
    --boot-sky-top: #f6fbea;
    --boot-sky-bottom: #86ab5f;
    --boot-glow: rgba(255, 196, 92, 0.48);
    --boot-accent: #3f7a2c;
    --boot-accent-soft: #8dc45a;
    --boot-ink: #2b3d1e;
    --boot-card: rgba(252, 255, 246, 0.88);
    --boot-edge: #c3dda6;
    --boot-horizon: rgba(52, 82, 34, 0.26);
}

/* Backdrop: a light source overhead (::before) and a ground/horizon band (::after).
   Both are decoration only - the card below stays legible without them. */
.boot-scene {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
}

.boot-scene::before {
    content: "";
    position: absolute;
    top: -32vmin;
    left: 50%;
    width: 88vmin;
    height: 88vmin;
    transform: translateX(-50%);
    border-radius: 50%;
    background: radial-gradient(circle, var(--boot-glow) 0%, transparent 70%);
    animation: boot-breathe 6s ease-in-out infinite;
}

.boot-scene::after {
    content: "";
    position: absolute;
    right: -30%;
    bottom: 0;
    left: -30%;
    height: 36%;
    opacity: 0.75;
    /* Floorboards for the house the flies are loose in. */
    background: repeating-linear-gradient(90deg, transparent 0 46px, var(--boot-horizon) 46px 50px);
    -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 80%);
    mask-image: linear-gradient(180deg, transparent 0%, #000 80%);
    animation: boot-drift 16s linear infinite;
}

/* Swell lines rolling in, rather than boards running away. */
.boot[data-scenario="puntagruesa"] .boot-scene::after {
    height: 44%;
    background: repeating-linear-gradient(0deg, transparent 0 16px, var(--boot-horizon) 16px 20px);
    animation: boot-swell 5s ease-in-out infinite;
}

/* Mown stripes across the pitch. */
.boot[data-scenario="marcagol"] .boot-scene::after {
    height: 42%;
    background: repeating-linear-gradient(90deg, transparent 0 60px, var(--boot-horizon) 60px 120px);
    animation: boot-drift 26s linear infinite;
}

/* Ridge lines of the coastal range falling away below the wing. */
.boot[data-scenario="parapente"] .boot-scene::after {
    height: 46%;
    background: repeating-linear-gradient(0deg, transparent 0 22px, var(--boot-horizon) 22px 25px);
    animation: boot-swell 7s ease-in-out infinite;
}

/* Vessels crossing the field of view, the loading screen's version of a horizon. */
.boot[data-scenario="tumorcito"] .boot-scene::after {
    height: 42%;
    background: repeating-linear-gradient(74deg, transparent 0 26px, var(--boot-horizon) 26px 29px);
    animation: boot-swell 9s ease-in-out infinite;
}

/* Rows of pots lined up under the lamps. */
.boot[data-scenario="tioweed"] .boot-scene::after {
    height: 40%;
    background: repeating-linear-gradient(90deg, transparent 0 34px, var(--boot-horizon) 34px 40px);
    animation: boot-drift 20s linear infinite;
}

/* Dust in the lamplight / foam on the water / grass kicked up: one shape, themed
   by the palette, so a new scenario gets its own ambience for free. */
.boot-mote {
    position: absolute;
    bottom: -10%;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    opacity: 0;
    background: radial-gradient(circle at 35% 30%, #fff 0%, var(--boot-glow) 70%, transparent 100%);
    animation: boot-rise 11s linear infinite;
}

.boot-mote:nth-child(1) { left: 12%; animation-duration: 12s; animation-delay: 0s; }
.boot-mote:nth-child(2) { left: 31%; width: 6px; height: 6px; animation-duration: 9s; animation-delay: 1.8s; }
.boot-mote:nth-child(3) { left: 54%; animation-duration: 14s; animation-delay: 3.4s; }
.boot-mote:nth-child(4) { left: 72%; width: 13px; height: 13px; animation-duration: 10s; animation-delay: 0.9s; }
.boot-mote:nth-child(5) { left: 88%; width: 7px; height: 7px; animation-duration: 13s; animation-delay: 2.6s; }

.boot-card {
    position: relative;
    z-index: 1;
    box-sizing: border-box;
    width: min(420px, 100%);
    margin: auto;
    padding: 1.5rem 1.4rem 1.4rem;
    text-align: center;
    background: var(--boot-card);
    border: 1px solid var(--boot-edge);
    border-radius: 22px;
    box-shadow: 0 18px 42px rgba(0, 0, 0, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.75);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    animation: boot-rise-in 0.55s cubic-bezier(0.2, 0.8, 0.3, 1) both;
}

.boot-title {
    margin: 0;
    font-size: clamp(1.5rem, 7vw, 2.1rem);
    line-height: 1.1;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--boot-accent);
    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.85);
}

/* The quarry, lit and casting a shadow, so it reads as a thing in a place rather
   than a loading glyph. */
.boot-stage {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 122px;
    margin: 0.5rem 0 1rem;
}

.boot-halo {
    position: absolute;
    width: 138px;
    height: 138px;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(255, 255, 255, 0.95) 0%, transparent 68%);
    animation: boot-halo-breathe 3.4s ease-in-out infinite;
}

.boot-ring {
    position: absolute;
    width: 116px;
    height: 116px;
    border: 2px dashed var(--boot-edge);
    border-radius: 50%;
    opacity: 0.85;
    animation: boot-spin 14s linear infinite;
}

.boot-quarry {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100px;
    height: auto;
    filter: drop-shadow(0 6px 8px rgba(0, 0, 0, 0.18));
}

.boot-shadow {
    position: absolute;
    bottom: 2px;
    width: 68px;
    height: 11px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.18);
    filter: blur(4px);
    animation: boot-shadow-pulse 3.4s ease-in-out infinite;
}

/* Real progress: Blazor sets --blazor-load-percentage on :root as the runtime
   downloads. The fallback width keeps the bar from looking broken during the
   first moments, before the runtime has reported anything. */
.boot-progress {
    position: relative;
    height: 10px;
    overflow: hidden;
    background: rgba(0, 0, 0, 0.09);
    border-radius: 999px;
    box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.16);
}

.boot-progress-fill {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: var(--blazor-load-percentage, 10%);
    min-width: 10%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--boot-accent-soft), var(--boot-accent));
    transition: width 0.25s ease-out;
}

.boot-progress-fill::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent);
    animation: boot-sheen 1.7s ease-in-out infinite;
}

.boot-meta {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.6rem;
    margin: 0.55rem 0 0;
    font-size: 0.78rem;
    letter-spacing: 0.02em;
    opacity: 0.85;
}

.boot-percent::after {
    content: var(--blazor-load-percentage-text, "");
    font-weight: bold;
    color: var(--boot-accent);
}

.boot-copy {
    margin-top: 1rem;
    padding-top: 0.9rem;
    border-top: 1px solid var(--boot-edge);
    font-size: 0.86rem;
    line-height: 1.45;
}

.boot-copy p {
    margin: 0;
}

.boot-copy p + p {
    margin-top: 0.5rem;
    font-weight: bold;
    color: var(--boot-accent);
}

@keyframes boot-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes boot-rise-in {
    from {
        opacity: 0;
        transform: translateY(14px) scale(0.97);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

@keyframes boot-breathe {
    0%, 100% { opacity: 0.65; transform: translateX(-50%) scale(1); }
    50% { opacity: 1; transform: translateX(-50%) scale(1.06); }
}

/* The halo is centred by the flex stage, so unlike the backdrop glow it must not
   carry a translateX of its own. */
@keyframes boot-halo-breathe {
    0%, 100% { opacity: 0.55; transform: scale(0.94); }
    50% { opacity: 1; transform: scale(1.06); }
}

@keyframes boot-drift {
    from { transform: translateX(0); }
    to { transform: translateX(-50px); }
}

@keyframes boot-swell {
    0%, 100% { transform: translateY(0); opacity: 0.55; }
    50% { transform: translateY(-8px); opacity: 0.85; }
}

@keyframes boot-rise {
    0% { opacity: 0; transform: translateY(0) translateX(0); }
    12% { opacity: 0.8; }
    80% { opacity: 0.5; }
    100% { opacity: 0; transform: translateY(-115vh) translateX(28px); }
}

@keyframes boot-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

@keyframes boot-shadow-pulse {
    0%, 100% { transform: scaleX(0.85); opacity: 0.75; }
    50% { transform: scaleX(1.1); opacity: 1; }
}

@keyframes boot-sheen {
    0% { transform: translateX(-110%); }
    60%, 100% { transform: translateX(110%); }
}

@media (max-height: 560px) {
    .boot {
        padding: 0.75rem;
    }

    .boot-card {
        padding: 1rem 1.1rem;
    }

    .boot-stage {
        height: 92px;
        margin: 0.3rem 0 0.7rem;
    }

    .boot-quarry {
        width: 74px;
    }

    .boot-quarry svg {
        width: 74px;
        height: 74px;
    }

    .boot-halo { width: 104px; height: 104px; }
    .boot-ring { width: 88px; height: 88px; }

    .boot-copy {
        margin-top: 0.7rem;
        padding-top: 0.6rem;
        font-size: 0.78rem;
    }
}

/* Ambience is decoration; anyone who asked the OS for stillness gets the same
   screen without a single thing moving on it. */
@media (prefers-reduced-motion: reduce) {
    .boot,
    .boot *,
    .boot *::before,
    .boot *::after,
    .boot-scene::before,
    .boot-scene::after {
        animation: none !important;
        transition: none !important;
    }

    .boot-mote {
        display: none;
    }
}

/* Fly "prey" idle animation for matalamosca: a subtle side-to-side buzz/wiggle
   (like a fly hovering in place) instead of a full dizzying spin. */
.spinner-icon {
    animation: fly-buzz 0.6s ease-in-out infinite;
}

/* Wave "prey" idle animation for the puntagruesa scenario: the swell grows and
   shrinks in place (rising/falling) instead of spinning like a fly. */
.wave-pulse-icon {
    animation: wave-pulse 1.4s ease-in-out infinite;
    transform-origin: center bottom;
}

@keyframes wave-pulse {
    0%, 100% {
        transform: scale(0.85);
    }
    50% {
        transform: scale(1.15);
    }
}

/* A goal does not spin or swell - it stands there. What moves is the net, so rock it
   gently as if a breeze were catching it. */
.goal-sway-icon {
    animation: goal-sway 2.6s ease-in-out infinite;
    transform-origin: center bottom;
}

@keyframes goal-sway {
    0%, 100% {
        transform: rotate(-2.5deg) scale(0.97);
    }
    50% {
        transform: rotate(2.5deg) scale(1.03);
    }
}

/* Thermal "quarry" idle animation for the parapente scenario: a gentle upward drift
   that suggests rising warm air. */
.thermal-rise-icon {
    animation: thermal-rise 2s ease-in-out infinite;
    transform-origin: center center;
}

@keyframes thermal-rise {
    0%, 100% {
        transform: translateY(3px) scale(0.92);
        opacity: 0.8;
    }
    50% {
        transform: translateY(-5px) scale(1.08);
        opacity: 1;
    }
}

/* A cogollo does not move: it ripens on the branch. What you are actually hunting is the frost
   of trichomes on it, so what animates is the light rather than the position - a slow glint
   with the faintest breathing, which at tile size reads as "this one is ready". */
.trichome-shimmer-icon {
    animation: trichome-shimmer 2.2s ease-in-out infinite;
    transform-origin: center bottom;
}

@keyframes trichome-shimmer {
    0%, 100% {
        transform: scale(0.97);
        filter: brightness(1) saturate(1);
    }
    50% {
        transform: scale(1.04);
        filter: brightness(1.2) saturate(1.12);
    }
}

/* A tumour does not travel and it does not sway: it grows. So the quarry animation is
   growth itself - a slow, uneven swell that never quite returns to where it started
   within one beat, which at tile size reads as "this is getting worse". */
.tumor-growth-icon {
    animation: tumor-growth 3s ease-in-out infinite;
    transform-origin: center bottom;
}

@keyframes tumor-growth {
    0%, 100% {
        transform: scale(0.9);
    }
    45% {
        transform: scale(1.1) rotate(1.5deg);
    }
    70% {
        transform: scale(1.02) rotate(-1deg);
    }
}

/* The one hazard in the game that is not out to get you: healthy tissue, which you are
   the danger to. A throb would say "careful, this bites"; what this has to say is
   "careful, this is alive", so it beats - a quick double pulse and then a long rest,
   at about a resting heart rate. */
.vital-sign-icon {
    animation: vital-sign 1.15s ease-in-out infinite;
    transform-origin: center center;
}

@keyframes vital-sign {
    0% {
        transform: scale(1);
        filter: none;
    }
    12% {
        transform: scale(1.11);
        filter: drop-shadow(0 0 9px rgba(140, 156, 255, 0.75));
    }
    24% {
        transform: scale(1.01);
    }
    36% {
        transform: scale(1.07);
        filter: drop-shadow(0 0 7px rgba(140, 156, 255, 0.55));
    }
    52%, 100% {
        transform: scale(1);
        filter: none;
    }
}

/* The rival prowls: a slow, wide sweep with a slight lean, so it reads as something
   hunting across the tile rather than sitting on it like the quarry does. Deliberately
   slower than every quarry animation - the rival should feel deliberate, not frantic. */
.rival-prowl-icon {
    animation: rival-prowl 2.2s ease-in-out infinite;
    transform-origin: center bottom;
}

@keyframes rival-prowl {
    0%, 100% {
        transform: translateX(-4px) rotate(-3deg) scale(1);
    }
    50% {
        transform: translateX(4px) rotate(3deg) scale(1.04);
    }
}

/* A hazard does not move on the map, so its animation is a warning rather than motion:
   a slow throb that never fully settles, which reads as "careful" without competing
   with the quarry for attention. */
.hazard-throb-icon {
    animation: hazard-throb 1.8s ease-in-out infinite;
    transform-origin: center center;
}

@keyframes hazard-throb {
    0%, 100% {
        transform: scale(0.94);
        filter: none;
    }
    50% {
        transform: scale(1.08);
        filter: drop-shadow(0 0 6px rgba(220, 40, 40, 0.65));
    }
}

@keyframes fly-buzz {
    0%, 100% {
        transform: translate(0, 0) rotate(-3deg);
    }
    25% {
        transform: translate(2px, -2px) rotate(3deg);
    }
    50% {
        transform: translate(-2px, 1px) rotate(-2deg);
    }
    75% {
        transform: translate(1px, 2px) rotate(2deg);
    }
}

/* swatter animation */
.something {
    z-index: 1;
    position: absolute;
    top: 15%;
    left: 22%;
    width: 60%;
    height: auto;
    transition: transform 120ms ease-out;
}

/* Dead fly peeking out from behind the swatter when the player stands on top of it,
   so kills stay visible instead of being fully hidden by the player's own tile. */
.captured-quarry-under-player {
    z-index: 0;
    width: 40%;
    top: 40%;
    left: 45%;
}

.animate-UpLeft {
    animation-name: slide-UpLeft;
    animation-duration: 180ms;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
    animation-fill-mode: forwards;
}

@keyframes slide-UpLeft {
    0% {
        transform: translate(0, 0);
    }
    100% {
        transform: translate(-150%, -150%);
    }
  }
  
.animate-Up {
    animation-name: slide-Up;
    animation-duration: 180ms;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
    animation-fill-mode: forwards;
}

@keyframes slide-Up {
    0% {
        transform: translateY(0);
    }
    100% {
        transform: translateY(-150%);
    }
}

.animate-UpRight {
    animation-name: slide-UpRight;
    animation-duration: 180ms;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
    animation-fill-mode: forwards;
}

@keyframes slide-UpRight {
    0% {
        transform: translate(0, 0);
    }
    100% {
        transform: translate(150%, -150%);
    }
}
  
.animate-Left {
    animation-name: slide-Left;
    animation-duration: 180ms;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
    animation-fill-mode: forwards;
}

@keyframes slide-Left {
    0% {
        transform: translateX(0);
    }
    100% {
        transform: translateX(-150%);
    }
}
.animate-Right {
    animation-name: slide-Right;
    animation-duration: 180ms;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
    animation-fill-mode: forwards;
}

@keyframes slide-Right {
    0% {
        transform: translateX(0);
    }
    100% {
        transform: translateX(150%);
    }
}
.animate-DownLeft {
    animation-name: slide-DownLeft;
    animation-duration: 180ms;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
    animation-fill-mode: forwards;
}

@keyframes slide-DownLeft {
    0% {
        transform: translate(0, 0);
    }
    100% {
        transform: translate(-150%, 150%);
    }
}
.animate-Down {
    animation-name: slide-Down;
    animation-duration: 180ms;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
    animation-fill-mode: forwards;
}

@keyframes slide-Down {
    0% {
        transform: translateY(0);
    }
    100% {
        transform: translateY(150%);
    }
}

.animate-DownRight {
    animation-name: slide-DownRight;
    animation-duration: 180ms;
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
    animation-fill-mode: forwards;
}

@keyframes slide-DownRight {
    0% {
        transform: translate(0, 0);
    }
    100% {
        transform: translate(150%, 150%);
    }
}

/* Tutorial arrows */

.tutorial-overlay {
    z-index: 2;
    position: absolute;
    display: block;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

.tutorial-opacity-5 {
    opacity: 0.7;
}

.tutorial-opacity-4 {
    opacity: 0.6;
}

.tutorial-opacity-3 {
    opacity: 0.5;
}

.tutorial-opacity-2 {
    opacity: 0.4;
}

.tutorial-opacity-1 {
    opacity: 0.3;
}

.tutorial-opacity-0 {
    opacity: 0.2;
}

/* Surf videos tab */

.surf-videos-subtabs {
    margin-bottom: 0.6em;
}

.surf-videos-hint {
    margin-bottom: 0.6em;
    opacity: 0.8;
}

.surf-videos-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 1em;
}

.surf-video-card {
    display: flex;
    flex-direction: column;
    gap: 0.4em;
    background: #f4f4f4;
    border-radius: 4px;
    padding: 0.5em;
}

.surf-video-card video {
    width: 100%;
    border-radius: 3px;
    background: #000;
}

.surf-video-meta {
    display: flex;
    justify-content: space-between;
    font-size: 0.85em;
    opacity: 0.75;
}

/* ---------------------------------------------------------------------------
   Admin sprite grant grid: players down the side, sprites across the top.
   Unlike the player list this one deliberately does NOT restack into cards on
   narrow screens - a matrix loses its meaning once the axes are gone. It scrolls
   sideways instead, with both axes pinned so the cell you are checking always
   has a visible row and column header.
   --------------------------------------------------------------------------- */
.player-manager-section {
    margin: 1.4em 0 .6em;
    font-size: 1.05em;
}

.sprite-grid-scroll {
    overflow: auto;
    /* Cap the height so the sticky header has something to stick against; without a
       scroll container of its own, position:sticky on <th> resolves against the page
       and the header scrolls away with it. */
    max-height: 70vh;
    /* Shrink to the table on wide screens so the border traces the grid instead of
       trailing off into empty space, but still cap at the viewport so it scrolls on a phone. */
    width: fit-content;
    max-width: 100%;
    border: 1px solid #ccc;
    border-radius: 8px;
}

.sprite-grid {
    border-collapse: separate;
    border-spacing: 0;
}

.sprite-grid th,
.sprite-grid td {
    border-right: 1px solid #ddd;
    border-bottom: 1px solid #ddd;
    padding: .4em .6em;
    background: #fff;
}

/* The first column pins horizontally, the header row vertically, and the corner cell
   does both - so it needs the higher stacking order of the two to stay on top of each. */
.sprite-grid-player {
    position: sticky;
    left: 0;
    z-index: 2;
    text-align: left;
    font-weight: 500;
    white-space: nowrap;
    box-shadow: 1px 0 0 #ccc;
}

.sprite-grid-head {
    position: sticky;
    top: 0;
    z-index: 2;
    min-width: 72px;
    box-shadow: 0 1px 0 #ccc;
}

.sprite-grid-corner {
    position: sticky;
    left: 0;
    top: 0;
    z-index: 3;
    text-align: left;
    box-shadow: 1px 0 0 #ccc, 0 1px 0 #ccc;
}

.sprite-grid-head img {
    display: block;
    width: 34px;
    height: 34px;
    object-fit: contain;
    margin: 0 auto .2em;
}

.sprite-grid-head span {
    display: block;
    font-size: .72em;
    font-weight: 500;
    line-height: 1.1;
    max-width: 76px;
}

.sprite-grid-player {
    /* The avatar and the name sit side by side; the cell is sticky, so its contents
       cannot use flex on the <th> itself without breaking the pin in some browsers. */
    vertical-align: middle;
}

.sprite-grid-player-name {
    display: inline-block;
    vertical-align: middle;
    margin-left: .4em;
    max-width: 9em;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* .player-avatar-empty is display:flex so its letter centres; inside the sticky first
   column that makes it block-level and drops the name onto its own line. */
.sprite-grid-player .player-avatar {
    display: inline-flex;
    vertical-align: middle;
}

.sprite-grid-cell {
    text-align: center;
}

.sprite-grid-cell input {
    width: 20px;
    height: 20px;
}

/* Scenario defaults sit in the same grid as the grantable extras, but read as state rather than
   as a control: the star is always-on, the dash is not-applicable. Both are non-interactive. */
/* The one "Por defecto" column: tinted so it reads as a fixed fact about the player rather than
   another thing to click, and it sits right next to the sticky name column. */
.sprite-grid-cell-default,
.sprite-grid-head-default {
    background: #fdf6e6;
}

.sprite-grid-cell-default-img {
    width: 28px;
    height: 28px;
    object-fit: contain;
    vertical-align: middle;
}

.sprite-grid-error,
.locker-room-error {
    color: #b03030;
    margin-top: .6em;
}

/* ---------------------------------------------------------------------------
   Locker room: the player's own rack of sprites.
   --------------------------------------------------------------------------- */
.locker-room {
    padding: .8em;
}

.locker-room-rack {
    display: grid;
    /* auto-fill rather than a fixed count: the rack holds anywhere from two sprites to
       the whole catalogue, and should fill the width either way. */
    grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
    gap: .8em;
}

.locker-room-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .3em;
    padding: .7em .4em;
    border: 2px solid #ccc;
    border-radius: 10px;
    background: #fff;
    cursor: pointer;
    font: inherit;
}

.locker-room-slot:disabled {
    opacity: .6;
    cursor: progress;
}

.locker-room-slot.is-selected {
    border-color: #b85c5c;
    box-shadow: 0 0 0 3px rgba(184, 92, 92, .2);
}

.locker-room-slot img {
    width: 64px;
    height: 78px;
    object-fit: contain;
}

.locker-room-name {
    font-size: .85em;
    text-align: center;
    line-height: 1.15;
}

.locker-room-tag {
    font-size: .68em;
    color: #777;
}

.locker-room-empty {
    color: #777;
}

/* The Google avatar is a photo among sprites, so round it off to read as a person
   rather than as another game icon. */
.score-avatar {
    border-radius: 50%;
    object-fit: cover;
}

/* ---------------------------------------------------------------------------
   Module administration: the roles matrix and each module's own panel.
   These reuse the sprite-grant grid (same shape: accounts down the side, the
   thing being granted across the top), so only the extras live here.
   --------------------------------------------------------------------------- */
.sprite-grid-wrap {
    padding: .2em 0 .8em;
}

/* Four modules is the whole point of this matrix, so it is tuned to fit them all at
   once inside the 500px column the rest of the admin lives in, rather than making the
   reader scroll sideways to find out who runs what. */
.module-admin-grid .sprite-grid-head {
    min-width: 0;
    font-size: .8em;
    padding: .3em .25em;
}

.module-admin-grid .sprite-grid-cell {
    padding: .4em .25em;
}

.module-admin-grid .sprite-grid-player-name {
    max-width: 8.5em;
}

.module-admin-emoji {
    display: block;
    font-size: 1.3em;
    line-height: 1.1;
}

/* The email is only ever rendered in the super-admin's own grid - the module panels
   show a first name and an avatar - so it is styled to sit quietly under the name. */
.module-admin-email {
    display: block;
    font-size: .72em;
    color: #777;
    font-weight: 400;
}

.module-admin-days {
    width: 3.2em;
    font: inherit;
    padding: .2em .1em;
    text-align: center;
    /* The spinner arrows eat most of a 3-character box and the value ends up clipped;
       the field is typed into, not nudged. */
    -moz-appearance: textfield;
    appearance: textfield;
}

.module-admin-days::-webkit-outer-spin-button,
.module-admin-days::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.module-admin-locked {
    opacity: .65;
}

/* ---------------------------------------------------------------------------
   Clip upload + storage meter (one permission, so one block).
   --------------------------------------------------------------------------- */
.surf-upload {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .6em;
    padding: .6em .8em;
    margin: .4em 0;
    border: 1px solid #ddd;
    border-radius: 10px;
    background: #fafafa;
}

.surf-upload-meter {
    flex: 1 1 220px;
    min-width: 180px;
}

.surf-upload-meter-bar {
    height: 8px;
    border-radius: 4px;
    background: #e2e2e2;
    overflow: hidden;
}

.surf-upload-meter-fill {
    height: 100%;
    background: #4a90a4;
    transition: width .3s ease;
}

.surf-upload-meter-full {
    background: #b03030;
}

.surf-upload-meter-label {
    display: block;
    font-size: .78em;
    color: #555;
    margin-top: .25em;
}

.surf-upload-pick {
    cursor: pointer;
}

.surf-upload-hint {
    font-size: .75em;
    color: #777;
}

.surf-upload-message {
    flex-basis: 100%;
    margin: 0;
    color: #2c7a4b;
    font-size: .85em;
}

/* ---- Historia (Components/Historia.razor) ------------------------------- */

/* The one screen in the app that is meant to be read rather than played, so it breaks the
   app-wide layout rules on purpose: a measured column instead of full width, and normal
   text sizing instead of the compact game chrome. Everything here is styled by element
   rather than by class, because the markup comes from Markdown (see ScenarioStory) and the
   person writing a story cannot add classes to it. */
.historia {
    /* A flex child's default min-width is its content's intrinsic width, so the widest thing
       inside (the conditions table, which does not wrap) would push the whole app sideways -
       and .main-page-content hides the overflow, so it would be cut off rather than scrollable.
       min-width: 0 is what lets this column actually obey the viewport. */
    width: 100%;
    max-width: 44rem;
    min-width: 0;
    box-sizing: border-box;
    align-self: center;
    margin: 0 auto;
    padding: 6px 16px 48px;
    /* The game font is a monospace; a page of prose in it is punishing to read. */
    font-family: Helvetica, Arial, sans-serif;
    line-height: 1.65;
    color: #2c2c2c;
}

.historia-status {
    padding: 24px 0;
    text-align: center;
    color: #666;
}

.historia h1 {
    margin: 18px 0 4px;
    font-size: 2rem;
    line-height: 1.15;
    letter-spacing: -0.01em;
}

.historia h2 {
    margin: 34px 0 8px;
    font-size: 1.35rem;
    line-height: 1.25;
    padding-top: 14px;
    border-top: 1px solid #e6ded0;
}

.historia h3 {
    margin: 24px 0 6px;
    font-size: 1.1rem;
}

.historia p {
    margin: 0 0 14px;
}

/* The opening paragraph carries the page: slightly larger, so the top of the story reads
   as an introduction without the writer having to mark it up as one. */
.historia h1 + p {
    font-size: 1.1rem;
    color: #3c3c3c;
}

.historia a {
    color: #0e6ba8;
}

.historia ul,
.historia ol {
    margin: 0 0 16px;
    padding-left: 1.4em;
}

.historia li {
    margin-bottom: 6px;
}

.historia blockquote {
    margin: 18px 0;
    padding: 10px 16px;
    border-left: 4px solid #c9a227;
    background: #fdf8ec;
    color: #55492f;
    font-size: 0.95rem;
}

/* Figures go full-bleed inside the column and carry their caption underneath. The image is
   width-constrained rather than height-constrained because the illustrations are all the
   same aspect ratio and a phone in portrait is the narrow case that decides the layout. */
.historia-figure {
    margin: 22px 0;
}

.historia-figure img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: 12px;
    background: #f2ede2;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.14);
}

.historia-figure figcaption {
    margin-top: 8px;
    color: #6b6355;
    font-size: 0.85rem;
    line-height: 1.45;
    text-align: center;
}

/* An image that is part of a sentence rather than a figure of its own still must not
   overflow the column. */
.historia p img {
    max-width: 100%;
    height: auto;
}

.historia table {
    width: 100%;
    margin: 8px 0 20px;
    border-collapse: collapse;
    font-size: 0.92rem;
}

.historia th,
.historia td {
    padding: 8px 10px;
    border-bottom: 1px solid #e6ded0;
    text-align: left;
    vertical-align: top;
}

.historia th {
    background: #fbf6ea;
    font-weight: 700;
}

.historia hr {
    margin: 30px 0 18px;
    border: 0;
    border-top: 1px solid #e6ded0;
}

/* The credits/footnote line at the end of every story. */
.historia hr + p {
    color: #857b6a;
    font-size: 0.82rem;
}

.historia code {
    padding: 1px 5px;
    border-radius: 4px;
    background: #f1ece1;
    font-family: 'Global Monospace', monospace;
    font-size: 0.88em;
}

@media (max-width: 480px) {
    .historia {
        padding: 4px 12px 40px;
        line-height: 1.6;
    }

    .historia h1 {
        font-size: 1.6rem;
    }

    .historia h2 {
        font-size: 1.2rem;
        margin-top: 26px;
    }

    /* Wrapping beats sideways scrolling here: the overflow is clipped by an ancestor, so a
       table that is too wide is silently cut off rather than swipeable. A fixed layout with
       a narrow label column keeps the long prose cells readable on a phone. */
    .historia table {
        table-layout: fixed;
        font-size: 0.86rem;
    }

    .historia th:first-child,
    .historia td:first-child {
        width: 34%;
    }

    .historia th,
    .historia td {
        padding: 7px 8px;
        word-break: break-word;
    }
}


/* ---------------------------------------------------------------------------
   Traído desde main: diario, historial de capturas, racha, medidor de clips y el
   ícono dibujado del botón de salir.
   --------------------------------------------------------------------------- */


.logout-icon {
    width: 1em;
    height: 1em;
    display: block;
}

.logout-button:hover {
    background: #6b6b6b;
}

/* Storage gauge. Deliberately above the clip grid rather than tucked under it: it is a
   warning that clips get deleted, and a warning below the thing it warns about is not read. */
.surf-videos-storage {
    display: flex;
    flex-direction: column;
    gap: 0.3em;
    margin-bottom: 0.8em;
}

.surf-videos-storage-bar {
    height: 8px;
    border-radius: 999px;
    background: #e8ddcb;
    overflow: hidden;
}

.surf-videos-storage-fill {
    height: 100%;
    background: #5b9c5b;
    transition: width 0.3s ease;
}

.surf-videos-storage-fill-mid {
    background: #d8a33a;
}

.surf-videos-storage-fill-high {
    background: #c1523f;
}

.surf-videos-storage-label {
    font-size: 0.85em;
    opacity: 0.8;
}

.surf-videos-storage-warning {
    font-size: 0.85em;
    color: #a33a2a;
}

/* Capture history. Matches the leaderboard's card look, since both are lists a player
   scans top-down; the details block is indented so a capture with extras still reads as
   one row and not as two entries. */
.capture-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.capture-row {
    border: 1px solid #e0c9a6;
    border-radius: 8px;
    margin-bottom: .5em;
    background: #fff7ec;
    padding: .55em .7em;
}

.capture-when {
    display: flex;
    align-items: center;
    gap: .6em;
}

.capture-sprite {
    width: 1.6em;
    height: 1.6em;
    object-fit: contain;
    flex: 0 0 auto;
}

.capture-date {
    font-weight: 600;
    color: #4a4033;
}

.capture-details {
    margin-top: .45em;
    padding-top: .45em;
    padding-left: 2.2em;
    border-top: 1px dashed #e0c9a6;
}

.capture-details-group {
    font-size: .8em;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: #8a7a63;
    margin-bottom: .2em;
}

.capture-detail {
    display: flex;
    align-items: baseline;
    gap: .4em;
    font-size: .92em;
}

.capture-detail-icon {
    flex: 0 0 auto;
}

.capture-detail-label {
    color: #6b5d4a;
}

.capture-detail-value {
    font-weight: 600;
    color: #3a3228;
    margin-left: auto;
}

.capture-history-error {
    color: #a33;
}

/* ---- Racha (Components/StreakBadge.razor) --------------------------------- */

.streak {
    display: flex;
    align-items: center;
    gap: 10px;
    max-width: 420px;
    margin: 6px auto;
    padding: 7px 12px;
    border: 1px solid #e2d4a8;
    border-radius: 999px;
    background: #fdf7e3;
    font-size: 0.85rem;
}

/* The one state where saying something changes what the player does, so it is the only
   one that gets a colour. A streak that is already safe needs no urgency. */
.streak.is-at-risk {
    border-color: #e0b78a;
    background: #fdeee0;
}

.streak-flame {
    font-size: 1.3rem;
    line-height: 1;
}

.streak-body {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
    /* min-width:0 lets the flex child shrink instead of forcing the pill wider than the
       screen when the note is long. */
    min-width: 0;
    flex: 1 1 auto;
}

.streak-count {
    font-weight: 600;
}

.streak-best {
    margin-left: 5px;
    padding: 1px 6px;
    border-radius: 999px;
    background: #2f7d32;
    color: #fff;
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    vertical-align: 1px;
}

.streak-note {
    color: #7a6a45;
    font-size: 0.75rem;
}

.streak-dots {
    display: flex;
    gap: 3px;
    flex: 0 0 auto;
}

.streak-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #e4dcc4;
}

.streak-dot.is-on {
    background: #2f7d32;
}

/* Today gets a ring so the strip has a "you are here" - without it a run ending yesterday
   and a run ending today look identical. */
.streak-dot.is-today {
    box-shadow: 0 0 0 2px #fdf7e3, 0 0 0 3px #b9a97e;
}

/* Below ~360px the dots are the first thing worth dropping: the number and the call to
   action carry the whole message on their own. */
@media (max-width: 359px) {
    .streak-dots {
        display: none;
    }
}

/* ---- Diario (Components/Diary.razor) ------------------------------------ */

.diary {
    padding: 0 10px 20px;
}

.diary-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin: 10px 0 18px;
}

.diary-stat {
    flex: 1 1 110px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 10px 8px;
    border: 1px solid #ddd;
    border-radius: 10px;
    background: #fafafa;
    text-align: center;
}

.diary-stat-value {
    font-size: 1.7rem;
    font-weight: 700;
    line-height: 1.1;
}

/* A date is far longer than a count; shrink it so the tiles stay the same height. */
.diary-stat-value-small {
    font-size: 1.1rem;
}

.diary-stat-label {
    font-size: 0.78rem;
    color: #666;
}

.diary-heading {
    margin: 18px 0 6px;
    font-size: 1rem;
}

.diary-days,
.diary-hours {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 110px;
}

.diary-day,
.diary-hour {
    /* Equal flex basis of 0 makes every column the same width regardless of how many
       there are, so the 30-day and 24-hour charts line up with each other. */
    flex: 1 1 0;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
}

.diary-day-bar {
    background: #2f7d32;
    border-radius: 2px 2px 0 0;
    min-height: 0;
}

/* A day with no captures still gets a hairline, so the chart reads as a timeline with
   gaps rather than as a chart that starts wherever the player happened to begin. */
.diary-day-bar.is-empty {
    height: 2px !important;
    background: #e0e0e0;
}

.diary-axis {
    display: flex;
    justify-content: space-between;
    font-size: 0.75rem;
    color: #777;
    margin-top: 3px;
}

.diary-hour-stack {
    position: relative;
    height: 100%;
    display: flex;
    align-items: flex-end;
}

/* The map's shape sits behind the player's as a pale backdrop: same baseline, so the two
   silhouettes can be compared at a glance without a second axis. */
.diary-hour-all {
    position: absolute;
    inset: auto 0 0 0;
    background: #cfe3d0;
    border-radius: 2px 2px 0 0;
}

.diary-hour-mine {
    position: relative;
    width: 100%;
    background: #2f7d32;
    border-radius: 2px 2px 0 0;
}

.diary-hour-label {
    font-size: 0.7rem;
    color: #777;
    text-align: center;
}

/* The streak line at the top of the diary tab: the same fact the badge shows above the
   board, restated where the player came looking for it. */
.diary-streak {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 10px 0 4px;
    padding: 9px 12px;
    border: 1px solid #e2d4a8;
    border-radius: 10px;
    background: #fdf7e3;
}

.diary-streak-at-risk {
    border-color: #e0b78a;
    background: #fdeee0;
}

.diary-streak-none {
    border-color: #ddd;
    background: #fafafa;
}

.diary-streak-icon {
    font-size: 1.5rem;
    line-height: 1;
}

.diary-streak-note {
    display: block;
    color: #7a6a45;
    font-size: 0.78rem;
}

.diary-streak-none .diary-streak-note {
    color: #666;
}
