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;
    /* Paired with viewport-fit=cover in index.html: the page now paints edge to edge, so the
       layout has to keep its own content out from under the notch and the home indicator.
       border-box keeps the total at 100dvh, so the insets eat into the app instead of adding
       to it - otherwise the page becomes taller than the screen and scrolls by exactly the
       height of the notch. */
    box-sizing: border-box;
    padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}

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

.pull-refresh-indicator {
    --pull-distance: 0px;
    position: absolute;
    left: 50%;
    top: env(safe-area-inset-top);
    transform: translate(-50%, calc(-100% + min(var(--pull-distance), 4rem)));
    z-index: 20;
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    padding: .45rem .8rem;
    border-radius: 999px;
    background: rgba(255, 255, 255, .92);
    color: #24526c;
    box-shadow: 0 8px 18px rgba(0, 0, 0, .15);
    font-size: .85rem;
    pointer-events: none;
    opacity: 0;
    transition: transform .18s ease, opacity .18s ease;
}

.pull-refresh-indicator[data-pull-state="pulling"],
.pull-refresh-indicator[data-pull-state="ready"],
.pull-refresh-indicator[data-pull-state="refreshing"] {
    opacity: 1;
}

.pull-refresh-indicator[data-pull-state="ready"] {
    color: #176f3d;
}

.pull-refresh-indicator[data-pull-state="refreshing"] .pull-refresh-icon {
    animation: pull-refresh-spin .9s linear infinite;
}

.pull-refresh-icon {
    font-size: 1rem;
    line-height: 1;
}

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

.ktf-game {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    /* The board is sized to fit (squareGrid.js takes min(width,height) of .grid-square-wrapper),
       so the game never needs to scroll - and scrolling it is actively wrong, because dragging
       the page is how you miss a tap on a tile. Without this, anything the stack above the board
       adds (ScoreTray, tools, an open panel, the tutorial) can overflow .ktf-game, and the
       overflow escapes to .main-page-content - which does scroll, because the prose tabs need it
       - so the phone opens the game taller than the screen. Clipping here keeps that single
       scroll container for the other tabs while making this one behave like a fixed screen.
       Measured down to a 360x420 viewport, the flex column shrinks the board (377px -> 147px)
       rather than overflowing, so this clips nothing today; it is the guard that keeps a future
       block added above the board from silently turning the game into a scrolling page. */
    overflow: hidden;
}

.ktf-loading {
    --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: relative;
    display: flex;
    justify-content: center;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
    box-sizing: border-box;
    padding: 1.25rem;
    color: var(--boot-ink);
    background: linear-gradient(180deg, var(--boot-sky-top) 0%, var(--boot-sky-bottom) 100%);
    isolation: isolate;
}

.ktf-loading[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);
}

.ktf-loading[data-scenario="huayquique"] {
    --boot-sky-top: #e6f8ff;
    --boot-sky-bottom: #6bb6dc;
    --boot-glow: rgba(255, 229, 159, 0.52);
    --boot-accent: #087094;
    --boot-accent-soft: #31b8d6;
    --boot-ink: #103c52;
    --boot-card: rgba(247, 253, 255, 0.88);
    --boot-edge: #9dd5e7;
    --boot-horizon: rgba(7, 92, 124, 0.3);
}

.ktf-loading[data-scenario="urraca"] {
    --boot-sky-top: #edf7fb;
    --boot-sky-bottom: #5fa0bf;
    --boot-glow: rgba(214, 241, 255, 0.48);
    --boot-accent: #0e5c83;
    --boot-accent-soft: #45a9c6;
    --boot-ink: #12384b;
    --boot-card: rgba(246, 252, 255, 0.88);
    --boot-edge: #94c4d5;
    --boot-horizon: rgba(13, 78, 112, 0.32);
}

.ktf-loading[data-scenario="marino"] {
    --boot-sky-top: #f4fbff;
    --boot-sky-bottom: #86c8e2;
    --boot-glow: rgba(255, 244, 196, 0.52);
    --boot-accent: #137a9e;
    --boot-accent-soft: #5cc5dc;
    --boot-ink: #164052;
    --boot-card: rgba(250, 254, 255, 0.9);
    --boot-edge: #b6dbe8;
    --boot-horizon: rgba(22, 100, 130, 0.28);
}

.ktf-loading[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);
}

.ktf-loading[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);
}

.ktf-loading[data-scenario="titoloco"] {
    --boot-sky-top: #dff3ff;
    --boot-sky-bottom: #ef9b43;
    --boot-glow: rgba(255, 240, 148, 0.65);
    --boot-accent: #d84e16;
    --boot-accent-soft: #ffb22e;
    --boot-ink: #4a260d;
    --boot-card: rgba(255, 248, 238, 0.9);
    --boot-edge: #efbf79;
    --boot-horizon: rgba(172, 78, 24, 0.32);
}

.ktf-loading[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);
}

.ktf-loading[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);
}

.ktf-loading[data-scenario="limpiapiscinas"] {
    --boot-sky-top: #e8fbff;
    --boot-sky-bottom: #65c8df;
    --boot-glow: rgba(218, 251, 255, 0.62);
    --boot-accent: #08779b;
    --boot-accent-soft: #35c7dd;
    --boot-ink: #123b4c;
    --boot-card: rgba(248, 254, 255, 0.9);
    --boot-edge: #9bd9e5;
    --boot-horizon: rgba(7, 104, 132, 0.28);
}

.ktf-loading[data-scenario="cortapasto"] {
    --boot-sky-top: #f4ffe7;
    --boot-sky-bottom: #75b954;
    --boot-glow: rgba(255, 242, 147, 0.5);
    --boot-accent: #2f7d32;
    --boot-accent-soft: #8ccf4e;
    --boot-ink: #243c1d;
    --boot-card: rgba(252, 255, 247, 0.9);
    --boot-edge: #bfe3a0;
    --boot-horizon: rgba(46, 98, 34, 0.3);
}

.ktf-loading[data-scenario="cuidahuerta"] {
    --boot-sky-top: #fbfff0;
    --boot-sky-bottom: #a9d879;
    --boot-glow: rgba(196, 244, 136, 0.48);
    --boot-accent: #2f8a4d;
    --boot-accent-soft: #75c96a;
    --boot-ink: #274126;
    --boot-card: rgba(254, 255, 249, 0.9);
    --boot-edge: #cce8ad;
    --boot-horizon: rgba(71, 100, 38, 0.26);
}

.ktf-loading[data-scenario="lenador"] {
    --boot-sky-top: #fff5e6;
    --boot-sky-bottom: #c8874a;
    --boot-glow: rgba(255, 198, 110, 0.5);
    --boot-accent: #8a451f;
    --boot-accent-soft: #d4873c;
    --boot-ink: #4a2b18;
    --boot-card: rgba(255, 249, 240, 0.9);
    --boot-edge: #e4c097;
    --boot-horizon: rgba(112, 61, 28, 0.3);
}

.ktf-loading[data-scenario="unicornio"] {
    --boot-sky-top: #fff0fb;
    --boot-sky-bottom: #c7b5ff;
    --boot-glow: rgba(255, 255, 190, 0.62);
    --boot-accent: #8b5cf6;
    --boot-accent-soft: #f472b6;
    --boot-ink: #4c1d95;
    --boot-card: rgba(255, 248, 253, 0.9);
    --boot-edge: #e9c5ff;
    --boot-horizon: rgba(126, 87, 194, 0.24);
}

.ktf-loading[data-scenario="patuto"] {
    --boot-sky-top: #eff6ff;
    --boot-sky-bottom: #93b7e6;
    --boot-glow: rgba(255, 255, 255, 0.55);
    --boot-accent: #2563eb;
    --boot-accent-soft: #f97316;
    --boot-ink: #1e3a8a;
    --boot-card: rgba(248, 251, 255, 0.9);
    --boot-edge: #bfdbfe;
    --boot-horizon: rgba(37, 99, 235, 0.22);
}

.ktf-loading[data-scenario="cerrocaracol"] {
    --boot-sky-top: #f1faef;
    --boot-sky-bottom: #6d9c72;
    --boot-glow: rgba(255, 226, 150, 0.5);
    --boot-accent: #1f6f4a;
    --boot-accent-soft: #f59e0b;
    --boot-ink: #1c3a2a;
    --boot-card: rgba(247, 253, 246, 0.9);
    --boot-edge: #bcdcb8;
    --boot-horizon: rgba(28, 74, 46, 0.28);
}

.ktf-loading .boot-card {
    width: min(430px, 100%);
    margin: auto;
}

.ktf-loading .boot-stage {
    height: 108px;
}

.ktf-loading-player {
    display: block;
    width: 78px;
    height: auto;
}

.ktf-loading .boot-progress-fill {
    width: 46%;
    min-width: 34%;
    animation: ktf-loading-progress 1.8s ease-in-out infinite;
}

.ktf-loading-meta {
    font-size: 0.84rem;
    font-weight: 700;
}

@keyframes ktf-loading-progress {
    0%, 100% {
        transform: translateX(-12%);
        width: 34%;
    }
    50% {
        transform: translateX(128%);
        width: 44%;
    }
}


.module-loading {
    --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: relative;
    box-sizing: border-box;
    width: 100%;
    max-width: 100%;
    margin: 0.75rem 0 1rem;
    overflow: hidden;
    color: var(--boot-ink);
    background: radial-gradient(circle at 12% -28%, var(--boot-glow) 0%, transparent 42%),
        linear-gradient(135deg, rgba(255, 255, 255, 0.68), rgba(255, 255, 255, 0.18)),
        var(--boot-card);
    border: 1px solid var(--boot-edge);
    border-radius: 20px;
    box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.72);
    isolation: isolate;
}

.module-loading[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);
}

.module-loading[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);
}

.module-loading[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);
}

.module-loading[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);
}

.module-loading[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);
}

.module-loading-panel {
    padding: 0.85rem;
}

.module-loading-shell {
    position: relative;
    z-index: 1;
    display: flex;
    min-width: 0;
    flex-direction: column;
    gap: 0.72rem;
}

.module-loading-ambience {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
}

.module-loading-ambience::after {
    content: "";
    position: absolute;
    right: -20%;
    bottom: 0;
    left: -20%;
    height: 34%;
    opacity: 0.48;
    background: repeating-linear-gradient(90deg, transparent 0 36px, var(--boot-horizon) 36px 40px);
    -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 100%);
    mask-image: linear-gradient(180deg, transparent 0%, #000 100%);
}

.module-loading-mote {
    position: absolute;
    bottom: -12px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    opacity: 0;
    background: radial-gradient(circle at 35% 30%, #fff 0%, var(--boot-glow) 72%, transparent 100%);
    animation: boot-rise 9s linear infinite;
}

.module-loading-mote:nth-child(1) { left: 16%; animation-delay: 0.2s; }
.module-loading-mote:nth-child(2) { left: 52%; width: 6px; height: 6px; animation-delay: 1.4s; animation-duration: 11s; }
.module-loading-mote:nth-child(3) { left: 84%; width: 10px; height: 10px; animation-delay: 2.7s; animation-duration: 10s; }

.module-loading-header {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    gap: 0.72rem;
    min-width: 0;
}

.module-loading-stage {
    position: relative;
    display: grid;
    width: 58px;
    height: 52px;
    place-items: center;
    flex: 0 0 auto;
}

.module-loading-halo,
.module-loading-ring {
    position: absolute;
    border-radius: 50%;
}

.module-loading-halo {
    width: 54px;
    height: 54px;
    background: radial-gradient(circle, rgba(255, 255, 255, 0.92) 0%, transparent 70%);
    animation: boot-halo-breathe 3.6s ease-in-out infinite;
}

.module-loading-ring {
    width: 46px;
    height: 46px;
    border: 1px dashed var(--boot-edge);
    animation: boot-spin 16s linear infinite;
}

.module-loading-player {
    position: relative;
    display: block;
    max-width: 36px;
    max-height: 36px;
    filter: drop-shadow(0 5px 6px rgba(0, 0, 0, 0.18));
    animation: module-loading-bob 2.6s ease-in-out infinite;
}

.module-loading-copy {
    min-width: 0;
}

.module-loading-kicker {
    display: block;
    margin-bottom: 0.12rem;
    color: var(--boot-accent);
    font-size: 0.68rem;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.module-loading h3 {
    margin: 0;
    color: var(--boot-ink);
    font-size: clamp(1rem, 4.6vw, 1.22rem);
    line-height: 1.15;
}

.module-loading-copy p {
    margin: 0.22rem 0 0;
    font-size: 0.82rem;
    line-height: 1.35;
    opacity: 0.78;
}

.module-loading-progress {
    position: relative;
    height: 7px;
    overflow: hidden;
    background: rgba(0, 0, 0, 0.08);
    border-radius: 999px;
    box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12);
}

.module-loading-progress span {
    position: absolute;
    inset: 0 auto 0 0;
    width: 42%;
    min-width: 32%;
    border-radius: inherit;
    background: linear-gradient(90deg, var(--boot-accent-soft), var(--boot-accent));
    animation: ktf-loading-progress 1.9s ease-in-out infinite;
}

.module-loading-skeleton {
    display: grid;
    gap: 0.55rem;
    min-width: 0;
}

.module-loading-line,
.module-loading-avatar,
.module-loading-card-media,
.module-loading-grid-cell,
.module-loading-grid-icon,
.module-loading-chart-bar {
    position: relative;
    overflow: hidden;
    background: linear-gradient(90deg, rgba(255, 255, 255, 0.48), rgba(255, 255, 255, 0.76), rgba(255, 255, 255, 0.48));
}

.module-loading-line::after,
.module-loading-avatar::after,
.module-loading-card-media::after,
.module-loading-grid-cell::after,
.module-loading-grid-icon::after,
.module-loading-chart-bar::after,
.module-loading-compact-line::after {
    content: "";
    position: absolute;
    inset: 0;
    transform: translateX(-110%);
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.72), transparent);
    animation: boot-sheen 1.8s ease-in-out infinite;
}

.module-loading-line {
    display: block;
    width: 74%;
    height: 0.68rem;
    border-radius: 999px;
}

.module-loading-line-1 { width: 58%; }
.module-loading-line-2 { width: 86%; }
.module-loading-line-3 { width: 42%; }
.module-loading-line-muted { height: 0.54rem; opacity: 0.72; }
.module-loading-line-score { justify-self: end; width: min(56px, 100%); align-self: center; }

.module-loading-list-row,
.module-loading-table-row,
.module-loading-table-head,
.module-loading-card-skeleton {
    min-width: 0;
    background: rgba(255, 255, 255, 0.34);
    border: 1px solid rgba(255, 255, 255, 0.48);
    border-radius: 14px;
}

.module-loading-list-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) minmax(36px, 0.24fr);
    align-items: center;
    gap: 0.55rem;
    padding: 0.62rem;
}

.module-loading-avatar {
    width: 2.05rem;
    height: 2.05rem;
    border-radius: 50%;
}

.module-loading-list-lines {
    display: grid;
    min-width: 0;
    gap: 0.34rem;
}

.module-loading-skeleton-cards {
    grid-template-columns: repeat(auto-fit, minmax(135px, 1fr));
}

.module-loading-card-skeleton {
    display: grid;
    gap: 0.48rem;
    min-height: 112px;
    padding: 0.62rem;
}

.module-loading-card-media {
    min-height: 58px;
    border-radius: 12px;
}

.module-loading-skeleton-grid {
    grid-template-columns: repeat(auto-fit, minmax(82px, 1fr));
}

.module-loading-grid-cell {
    display: grid;
    min-height: 84px;
    place-items: center;
    gap: 0.45rem;
    padding: 0.62rem;
    border-radius: 15px;
    background: rgba(255, 255, 255, 0.36);
    border: 1px solid rgba(255, 255, 255, 0.48);
}

.module-loading-grid-icon {
    width: 2.6rem;
    height: 2.6rem;
    border-radius: 15px;
}

.module-loading-grid-caption {
    width: 70%;
}

.module-loading-table-head,
.module-loading-table-row {
    display: grid;
    grid-template-columns: 2rem 1.2fr repeat(3, minmax(0, 0.8fr));
    align-items: center;
    gap: 0.5rem;
    padding: 0.58rem;
}

.module-loading-table-head {
    grid-template-columns: 1.2fr repeat(3, minmax(0, 0.8fr));
    padding-left: 0.72rem;
}

.module-loading-table-cell {
    width: 100%;
}

.module-loading-chart-panel {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
    gap: 0.75rem;
    align-items: stretch;
}

.module-loading-chart-bars,
.module-loading-chart-legend {
    min-width: 0;
    background: rgba(255, 255, 255, 0.34);
    border: 1px solid rgba(255, 255, 255, 0.48);
    border-radius: 14px;
}

.module-loading-chart-bars {
    display: flex;
    height: 118px;
    align-items: end;
    gap: 0.42rem;
    padding: 0.7rem 0.65rem;
}

.module-loading-chart-bar {
    flex: 1 1 0;
    min-width: 0;
    border-radius: 999px 999px 6px 6px;
}

.module-loading-chart-legend {
    display: grid;
    align-content: center;
    gap: 0.55rem;
    padding: 0.7rem;
}

.module-loading-compact {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    width: auto;
    margin: 0;
    padding: 0.34rem 0.55rem;
    border-radius: 999px;
    box-shadow: none;
    font-size: 0.82rem;
    vertical-align: middle;
}

.module-loading-compact-dot {
    display: grid;
    width: 1.45rem;
    height: 1.45rem;
    place-items: center;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.58);
    border: 1px solid rgba(255, 255, 255, 0.72);
}

.module-loading-compact-dot img {
    display: block;
    max-width: 1rem;
    max-height: 1rem;
}

.module-loading-compact-text {
    font-weight: 700;
    white-space: nowrap;
}

.module-loading-compact-line {
    position: relative;
    display: block;
    overflow: hidden;
    width: 3.4rem;
    height: 0.44rem;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.58);
}

@keyframes module-loading-bob {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-4px); }
}

@media (max-width: 520px) {
    .module-loading-panel {
        padding: 0.72rem;
        border-radius: 17px;
    }

    .module-loading-header {
        grid-template-columns: auto minmax(0, 1fr);
        gap: 0.58rem;
    }

    .module-loading-stage {
        width: 48px;
        height: 46px;
    }

    .module-loading-halo { width: 46px; height: 46px; }
    .module-loading-ring { width: 39px; height: 39px; }
    .module-loading-player { max-width: 30px; max-height: 30px; }

    .module-loading-list-row {
        grid-template-columns: auto minmax(0, 1fr);
    }

    .module-loading-list-row > .module-loading-line-score {
        display: none;
    }

    .module-loading-skeleton-cards,
    .module-loading-chart-panel {
        grid-template-columns: 1fr;
    }

    .module-loading-skeleton-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .module-loading-table-head {
        display: none;
    }

    .module-loading-table-row {
        grid-template-columns: auto minmax(0, 1fr) minmax(0, 0.55fr);
    }

    .module-loading-table-row .module-loading-table-cell:nth-child(n+4) {
        display: none;
    }

    .module-loading-compact {
        max-width: 100%;
    }

    .module-loading-compact-text {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
    }
}

@media (prefers-reduced-motion: reduce) {
    .module-loading,
    .module-loading *,
    .module-loading *::before,
    .module-loading *::after {
        animation: none !important;
        transition: none !important;
    }

    .module-loading-mote {
        display: none;
    }
}


/*scoreboard*/
.score-tray {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.9em;
    box-sizing: border-box;
    max-width: min(480px, calc(100% - 0.8rem));
    width: fit-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: visible;
}

.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 so wrapped rows stay compact. */
@media (max-width: 480px) {
    .score-tray {
        gap: 0.35em 0.45em;
        max-width: calc(100% - 0.5rem);
        padding: 0.3em 0.5em;
        font-size: 0.8em;
    }

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

    .sound-volume input[type="range"] {
        width: 4.2em;
    }

    .sound-volume-value {
        min-width: 2.4em;
    }
}

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

/* Online-only tools now live inside the game instead of the global top menu. Sólo el icono:
   el nombre va en el tooltip (title/aria-label), igual que las pestañas de la barra de arriba,
   así la fila no le come ancho al tablero. */
.game-tools {
    display: flex;
    justify-content: center;
    gap: 0.35em;
    margin: 0.1em auto 0.35em;
    max-width: min(520px, 100%);
    width: 100%;
    box-sizing: border-box;
    padding: 0 0.4em;
}

.game-tool-button {
    flex: 0 0 auto;
    min-width: 0;
    border: none;
    border-radius: 999px;
    padding: 0.35em 0.7em;
    font-family: inherit;
    font-size: 1.15em;
    line-height: 1;
    color: inherit;
    white-space: nowrap;
}

.game-panel {
    box-sizing: border-box;
    flex: 0 1 auto;
    width: min(520px, calc(100% - 0.8em));
    max-height: min(44vh, 420px);
    overflow-y: auto;
    margin: 0.15em auto 0.45em;
    padding: 0.75em;
    background: rgba(255, 247, 236, 0.96);
    border: 1px solid #e0c9a6;
    border-radius: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.16);
}

.game-panel-heading {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.8em;
    margin-bottom: 0.55em;
}

.game-panel-heading h3 {
    margin: 0;
    font-size: 1em;
}

@media (max-width: 480px) {
    .game-tool-button {
        padding: 0.32em 0.45em;
        font-size: 0.8em;
    }

    .game-tool-button span {
        display: none;
    }
}

/*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;
}

/* Encabezado: el puesto y el elegido, uno debajo del otro y a la misma anchura, para
   poder compararlos antes de reemplazar nada. */
.photo-manager-banner-figure {
    margin: 0 0 0.6em 0;
}

.photo-manager-banner-figure .photo-manager-banner-preview {
    margin-bottom: 0.2em;
}

.photo-manager-banner-figure-new .photo-manager-banner-preview {
    outline: 2px dashed #7aa7ff;
    outline-offset: 2px;
}

/* Carteles: lo que está puesto y lo que está por subirse, lado a lado y al cuadrado,
   que es como el juego los recorta. */
.photo-manager-ad-previews {
    display: flex;
    align-items: center;
    gap: 0.5em;
    margin-bottom: 0.2em;
}

.photo-manager-ad-preview {
    margin: 0;
    text-align: center;
}

.photo-manager-ad-preview img {
    display: block;
    width: 64px;
    height: 64px;
    object-fit: cover;
    border-radius: 4px;
    background-color: brown;
}

.photo-manager-ad-preview-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 64px;
    height: 64px;
    border-radius: 4px;
    background-color: brown;
    font-size: 1.6em;
}

.photo-manager-ad-preview figcaption {
    font-size: 0.72em;
    color: #888;
    margin-top: 0.15em;
}

.photo-manager-ad-preview-new img {
    outline: 2px dashed #7aa7ff;
    outline-offset: 2px;
}

.photo-manager-ad-preview-arrow {
    color: #888;
}

.photo-manager-ad-warning {
    margin: 0.4em 0 0 0;
    font-size: 0.85em;
    color: #b8860b;
}

.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;
    position: relative;
    cursor: pointer;
}

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

.ad-grid-map-ad {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

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

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

.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;
}

.sound-volume {
    gap: 0.25em;
}

.sound-volume input[type="range"] {
    width: 5.6em;
    accent-color: #2ecc71;
}

.sound-volume input[type="range"]:disabled {
    opacity: 0.45;
}

.sound-volume-value {
    min-width: 2.8em;
    text-align: right;
    font-size: 0.78em;
    color: #4a4033;
}

/* 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;
}

.leaderboard-rival-card {
    display: grid;
    grid-template-columns: auto 1fr auto;
    grid-template-areas:
        "sprite badge score"
        "sprite name score";
    align-items: center;
    gap: .15em .65em;
    margin-top: .65em;
    padding: .65em .75em;
    border: 1px dashed #b98545;
    border-radius: 10px;
    background: #fffaf1;
}

.leaderboard-rival-sprite {
    grid-area: sprite;
    width: 34px;
    height: auto;
}

.leaderboard-rival-badge {
    grid-area: badge;
    font-size: .72em;
    font-weight: 700;
    color: #7a4f1f;
    text-transform: uppercase;
    letter-spacing: .03em;
}

.leaderboard-rival-name {
    grid-area: name;
    font-weight: 700;
}

.leaderboard-rival-score {
    grid-area: score;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* 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);
}

/* Huayquique: city beach light, a little brighter than the point. */
.boot[data-scenario="huayquique"] {
    --boot-sky-top: #e6f8ff;
    --boot-sky-bottom: #6bb6dc;
    --boot-glow: rgba(255, 229, 159, 0.52);
    --boot-accent: #087094;
    --boot-accent-soft: #31b8d6;
    --boot-ink: #103c52;
    --boot-card: rgba(247, 253, 255, 0.88);
    --boot-edge: #9dd5e7;
    --boot-horizon: rgba(7, 92, 124, 0.3);
}

/* Urraca: crossed wind and colder water. */
.boot[data-scenario="urraca"] {
    --boot-sky-top: #edf7fb;
    --boot-sky-bottom: #5fa0bf;
    --boot-glow: rgba(214, 241, 255, 0.48);
    --boot-accent: #0e5c83;
    --boot-accent-soft: #45a9c6;
    --boot-ink: #12384b;
    --boot-card: rgba(246, 252, 255, 0.88);
    --boot-edge: #94c4d5;
    --boot-horizon: rgba(13, 78, 112, 0.32);
}

/* Marino: brighter foam on a longboard-friendly shore. */
.boot[data-scenario="marino"] {
    --boot-sky-top: #f4fbff;
    --boot-sky-bottom: #86c8e2;
    --boot-glow: rgba(255, 244, 196, 0.52);
    --boot-accent: #137a9e;
    --boot-accent-soft: #5cc5dc;
    --boot-ink: #164052;
    --boot-card: rgba(250, 254, 255, 0.9);
    --boot-edge: #b6dbe8;
    --boot-horizon: rgba(22, 100, 130, 0.28);
}

/* 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);
}

/* Tito Loco: the same desert lift, with hot paramotor exhaust. */
.boot[data-scenario="titoloco"] {
    --boot-sky-top: #dff3ff;
    --boot-sky-bottom: #ef9b43;
    --boot-glow: rgba(255, 240, 148, 0.65);
    --boot-accent: #d84e16;
    --boot-accent-soft: #ffb22e;
    --boot-ink: #4a260d;
    --boot-card: rgba(255, 248, 238, 0.9);
    --boot-edge: #efbf79;
    --boot-horizon: rgba(172, 78, 24, 0.32);
}

/* 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);
}

.boot[data-scenario="limpiapiscinas"] {
    --boot-sky-top: #e8fbff;
    --boot-sky-bottom: #65c8df;
    --boot-glow: rgba(218, 251, 255, 0.62);
    --boot-accent: #08779b;
    --boot-accent-soft: #35c7dd;
    --boot-ink: #123b4c;
    --boot-card: rgba(248, 254, 255, 0.9);
    --boot-edge: #9bd9e5;
    --boot-horizon: rgba(7, 104, 132, 0.28);
}

.boot[data-scenario="cortapasto"] {
    --boot-sky-top: #f4ffe7;
    --boot-sky-bottom: #75b954;
    --boot-glow: rgba(255, 242, 147, 0.5);
    --boot-accent: #2f7d32;
    --boot-accent-soft: #8ccf4e;
    --boot-ink: #243c1d;
    --boot-card: rgba(252, 255, 247, 0.9);
    --boot-edge: #bfe3a0;
    --boot-horizon: rgba(46, 98, 34, 0.3);
}

.boot[data-scenario="cuidahuerta"] {
    --boot-sky-top: #fbfff0;
    --boot-sky-bottom: #a9d879;
    --boot-glow: rgba(196, 244, 136, 0.48);
    --boot-accent: #2f8a4d;
    --boot-accent-soft: #75c96a;
    --boot-ink: #274126;
    --boot-card: rgba(254, 255, 249, 0.9);
    --boot-edge: #cce8ad;
    --boot-horizon: rgba(71, 100, 38, 0.26);
}

.boot[data-scenario="lenador"] {
    --boot-sky-top: #fff5e6;
    --boot-sky-bottom: #c8874a;
    --boot-glow: rgba(255, 198, 110, 0.5);
    --boot-accent: #8a451f;
    --boot-accent-soft: #d4873c;
    --boot-ink: #4a2b18;
    --boot-card: rgba(255, 249, 240, 0.9);
    --boot-edge: #e4c097;
    --boot-horizon: rgba(112, 61, 28, 0.3);
}

.boot[data-scenario="unicornio"] {
    --boot-sky-top: #fff0fb;
    --boot-sky-bottom: #c7b5ff;
    --boot-glow: rgba(255, 255, 190, 0.62);
    --boot-accent: #8b5cf6;
    --boot-accent-soft: #f472b6;
    --boot-ink: #4c1d95;
    --boot-card: rgba(255, 248, 253, 0.9);
    --boot-edge: #e9c5ff;
    --boot-horizon: rgba(126, 87, 194, 0.24);
}

.boot[data-scenario="patuto"] {
    --boot-sky-top: #eff6ff;
    --boot-sky-bottom: #93b7e6;
    --boot-glow: rgba(255, 255, 255, 0.55);
    --boot-accent: #2563eb;
    --boot-accent-soft: #f97316;
    --boot-ink: #1e3a8a;
    --boot-card: rgba(248, 251, 255, 0.9);
    --boot-edge: #bfdbfe;
    --boot-horizon: rgba(37, 99, 235, 0.22);
}

.boot[data-scenario="cerrocaracol"] {
    --boot-sky-top: #f1faef;
    --boot-sky-bottom: #6d9c72;
    --boot-glow: rgba(255, 226, 150, 0.5);
    --boot-accent: #1f6f4a;
    --boot-accent-soft: #f59e0b;
    --boot-ink: #1c3a2a;
    --boot-card: rgba(247, 253, 246, 0.9);
    --boot-edge: #bcdcb8;
    --boot-horizon: rgba(28, 74, 46, 0.28);
}

/* 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,
.boot[data-scenario="huayquique"] .boot-scene::after,
.boot[data-scenario="urraca"] .boot-scene::after,
.boot[data-scenario="marino"] .boot-scene::after,
.ktf-loading[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;
}

.ktf-loading[data-scenario="huayquique"] .boot-scene::after,
.ktf-loading[data-scenario="urraca"] .boot-scene::after,
.ktf-loading[data-scenario="marino"] .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,
.ktf-loading[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,
.boot[data-scenario="titoloco"] .boot-scene::after,
.ktf-loading[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;
}

.ktf-loading[data-scenario="titoloco"] .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,
.ktf-loading[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,
.ktf-loading[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;
}

.boot[data-scenario="limpiapiscinas"] .boot-scene::after,
.ktf-loading[data-scenario="limpiapiscinas"] .boot-scene::after {
    height: 42%;
    background: repeating-linear-gradient(0deg, transparent 0 18px, var(--boot-horizon) 18px 22px);
    animation: boot-swell 6s ease-in-out infinite;
}

.boot[data-scenario="cortapasto"] .boot-scene::after,
.ktf-loading[data-scenario="cortapasto"] .boot-scene::after {
    height: 42%;
    background: repeating-linear-gradient(90deg, transparent 0 44px, var(--boot-horizon) 44px 74px);
    animation: boot-drift 24s linear infinite;
}

.boot[data-scenario="cuidahuerta"] .boot-scene::after,
.ktf-loading[data-scenario="cuidahuerta"] .boot-scene::after {
    height: 40%;
    background: repeating-linear-gradient(90deg, transparent 0 30px, var(--boot-horizon) 30px 36px);
    animation: boot-drift 22s linear infinite;
}

.boot[data-scenario="lenador"] .boot-scene::after,
.ktf-loading[data-scenario="lenador"] .boot-scene::after {
    height: 42%;
    background: repeating-linear-gradient(74deg, transparent 0 34px, var(--boot-horizon) 34px 39px);
    animation: boot-drift 28s linear infinite;
}

.boot[data-scenario="unicornio"] .boot-scene::after,
.ktf-loading[data-scenario="unicornio"] .boot-scene::after {
    height: 38%;
    background: repeating-linear-gradient(110deg, transparent 0 54px, var(--boot-horizon) 54px 61px);
    animation: boot-swell 5.5s ease-in-out infinite;
}

.boot[data-scenario="patuto"] .boot-scene::after,
.ktf-loading[data-scenario="patuto"] .boot-scene::after {
    height: 40%;
    background: repeating-linear-gradient(90deg, transparent 0 36px, var(--boot-horizon) 36px 42px);
    animation: boot-drift 20s linear infinite;
}

/* Senderos que suben cruzando el cerro, uno tras otro. */
.boot[data-scenario="cerrocaracol"] .boot-scene::after,
.ktf-loading[data-scenario="cerrocaracol"] .boot-scene::after {
    height: 44%;
    background: repeating-linear-gradient(58deg, transparent 0 40px, var(--boot-horizon) 40px 46px);
    animation: boot-drift 26s 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 img {
        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,
    .ktf-loading,
    .ktf-loading *,
    .ktf-loading *::before,
    .ktf-loading *::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;
    }
}

.thermal-rise-icon.tito-thermal-boost {
    animation: tito-thermal-boost 1.05s cubic-bezier(0.2, 0.75, 0.28, 1) infinite;
}

@keyframes tito-thermal-boost {
    0%, 100% {
        transform: translateY(5px) rotate(-4deg) scale(0.88);
        filter: drop-shadow(0 0 0 rgba(255, 120, 24, 0));
        opacity: 0.78;
    }
    45% {
        transform: translateY(-8px) rotate(5deg) scale(1.18);
        filter: drop-shadow(0 0 10px rgba(255, 120, 24, 0.75));
        opacity: 1;
    }
    70% {
        transform: translateY(-2px) rotate(-2deg) scale(1.02);
        opacity: 0.92;
    }
}

/* 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;
    }
}

/* Un pajarito posado no gira ni crece: pega saltitos cortos en la rama y mira para el lado.
   Por eso la presa del cerro salta en el sitio con una pausa entre saltos - al tamaño de
   una casilla se lee como "hay algo vivo ahí, mírale antes de que vuele". */
.bird-hop-icon {
    animation: bird-hop 1.6s ease-in-out infinite;
    transform-origin: center bottom;
}

@keyframes bird-hop {
    0%, 55%, 100% {
        transform: translateY(0) rotate(0deg) scale(1);
    }
    12% {
        transform: translateY(-6px) rotate(-5deg) scale(1.05);
    }
    24% {
        transform: translateY(0) rotate(0deg) scale(0.98);
    }
    36% {
        transform: translateY(-4px) rotate(5deg) scale(1.03);
    }
    46% {
        transform: translateY(0) rotate(2deg) scale(1);
    }
}

/* 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%;
}

/* El resto de un accidente: el mismo dibujo del peligro, pero apagado, torcido y a medio
   desaparecer, para que se lea como "acá chocaste hace poco" y no como un peligro que sigue
   activo. Dura lo mismo que la marca de una captura y después la casilla queda limpia. */
.accident-mark {
    opacity: 0.45;
    filter: grayscale(0.8);
    transform: rotate(24deg);
}

.rival-capture-scene {
    position: absolute;
    inset: 0;
    z-index: 5;
    pointer-events: none;
    overflow: hidden;
}

.rival-capture-scene img {
    position: absolute;
    height: auto;
}

.rival-capture-quarry {
    width: 50%;
    left: 25%;
    top: 22%;
    opacity: 0;
}

.rival-capture-mark {
    width: 46%;
    left: 27%;
    top: 36%;
    opacity: 0;
}

.rival-capture-rival {
    width: 62%;
    left: 19%;
    top: 10%;
}

.rival-capture-pounce .rival-capture-quarry {
    animation: rival-capture-quarry-vanish 700ms ease-out forwards;
}

.rival-capture-pounce .rival-capture-mark {
    animation: rival-capture-mark-pop 900ms ease-out 180ms forwards;
}

.rival-capture-pounce .rival-capture-rival {
    animation: rival-capture-rival-pounce 900ms cubic-bezier(0.2, 0.85, 0.2, 1) forwards;
    transform-origin: center bottom;
}

@keyframes rival-capture-quarry-vanish {
    0% {
        opacity: 1;
        transform: scale(1);
        filter: none;
    }
    45% {
        opacity: 1;
        transform: scale(0.9) translateY(4px);
        filter: brightness(1.25);
    }
    100% {
        opacity: 0;
        transform: scale(0.35) translateY(12px);
        filter: brightness(1.4);
    }
}

@keyframes rival-capture-mark-pop {
    0% {
        opacity: 0;
        transform: scale(0.4);
    }
    35% {
        opacity: 1;
        transform: scale(1.12);
    }
    100% {
        opacity: 0.95;
        transform: scale(0.9);
    }
}

@keyframes rival-capture-rival-pounce {
    0% {
        opacity: 0;
        transform: translate(-55%, -35%) scale(0.78) rotate(-10deg);
    }
    30% {
        opacity: 1;
    }
    58% {
        transform: translate(0, 0) scale(1.14) rotate(3deg);
    }
    100% {
        opacity: 0;
        transform: translate(8%, 8%) scale(0.92) rotate(0);
    }
}

.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 card */

.tutorial-card {
    width: min(500px, calc(100% - 1rem));
    box-sizing: border-box;
    margin: 0.35rem auto 0.5rem;
    padding: 0.65rem 0.75rem;
    color: #4a4033;
    background: rgba(255, 247, 236, 0.94);
    border: 1px solid #e0c9a6;
    border-radius: 16px;
    box-shadow: 0 7px 20px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.78);
}

.tutorial-card-header {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.45rem;
    text-align: center;
    line-height: 1.2;
}

.tutorial-eyebrow {
    display: inline-flex;
    align-items: center;
    padding: 0.22rem 0.55rem;
    border-radius: 999px;
    background: #f06015;
    color: #fff;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    box-shadow: 0 2px 6px rgba(240, 96, 21, 0.28);
}

.tutorial-card-body {
    display: grid;
    gap: 0.4rem;
    margin-top: 0.55rem;
}

.tutorial-step {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    text-align: left;
    font-size: 0.86rem;
    line-height: 1.25;
}

.tutorial-step-icon {
    flex: 0 0 auto;
    width: 1.8rem;
    height: 1.8rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: #fff;
    color: #f06015;
    border: 1px solid #e0c9a6;
    font-weight: 800;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
}

.tutorial-step-warning .tutorial-step-icon {
    background: #ffe2ce;
    color: #a83f3f;
}

@media (max-height: 620px) {
    .tutorial-card {
        margin-top: 0.25rem;
        padding: 0.5rem 0.65rem;
    }

    .tutorial-card-body {
        gap: 0.3rem;
        margin-top: 0.4rem;
    }

    .tutorial-step {
        font-size: 0.78rem;
    }
}

/* 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;
}

/* auto-fit, not auto-fill: auto-fill keeps the empty tracks, so a single clip on a wide
   screen sat at 220px with the rest of the row blank. auto-fit collapses them so the lone
   card grows instead - but left uncapped it grew to the whole 1280px viewport, a billboard
   with one wave in it, which is not an improvement over an orphan. max-width caps it: a
   lone clip reads as a card (420px) rather than a hero. A full row is unaffected, because
   at 1280px the row resolves to five tracks of 243.2px each, well under the cap. */
.surf-videos-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 1em;
}

.surf-video-card {
    box-sizing: border-box;
    max-width: 420px;
}

/* Matched to .module-loading-card-skeleton (radius 14px, padding 0.62rem) so the skeleton
   this tab shows while loading does not visibly snap into a different shape when the clips
   arrive. The surface itself follows .tutorial-card, which is the site's precedent for a
   content card: the --boot-* tokens are NOT available here (they are declared only on
   .boot, .ktf-loading and .module-loading), so var(--boot-card) would resolve to nothing
   and silently leave the card with no background at all. */
.surf-video-card {
    display: flex;
    flex-direction: column;
    gap: 0.4em;
    color: #4a4033;
    background: rgba(255, 247, 236, 0.94);
    border: 1px solid #e0c9a6;
    border-radius: 14px;
    padding: 0.62rem;
}

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

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

/* La ranura de autoría de D49 §4.3, y el punto de apoyo que D60 §5 da por existente para poner
   ahí el origen de los clips de máquina. Es una clase propia y no un tercer hijo de
   .surf-video-meta porque ese contenedor reparte con space-between: un tercer elemento le
   cambia el reparto a la duración y la hora, y además nombrar a una persona no es lo mismo que
   describir el archivo.

   Toma prestados el tamaño y la opacidad del meta a propósito: el producto ya decidió ahí cómo
   se ve el dato secundario de una tarjeta de video, y estrenar un tamaño o un color acá sería
   inventar un segundo lenguaje dentro del mismo módulo. Ni borde, ni fondo, ni peso: una
   atribución no es una alerta. */
.surf-video-uploader {
    margin-top: 0.25rem;
    font-size: 0.85em;
    opacity: 0.75;
}

/* El escenario de D49 §4.3, la tercera cosa que la tarjeta debe mostrar junto con la autoría y la
   hora. Hereda tamaño y opacidad de .surf-video-uploader por la misma razón que aquélla los tomó
   del meta: el módulo ya decidió cómo se ve un dato secundario de la tarjeta, y estrenar un
   tamaño acá sería un segundo lenguaje adentro de la misma grilla.

   Sin margin-top propio: va pegado debajo de la autoría, que ya abrió la separación con el meta.
   Darle otro 0.25rem separaría dos datos que se leen juntos —quién y dónde— tanto como cada uno
   se separa del bloque de arriba. */
.surf-video-escenario {
    font-size: 0.85em;
    opacity: 0.75;
}

/* La hora y su botón de corregir (D49 §4.4) van en un solo hijo de .surf-video-meta, por lo que
   dice el comentario de arriba: ese contenedor reparte con space-between y un tercer hijo le
   cambiaría el reparto a la duración y a la hora. */
/* El margin-left:auto lo levantó Tito revisando: es la mitad simétrica de lo que yo había
   protegido. Yo cuidé que .surf-video-meta no recibiera un TERCER hijo; el problema al revés es
   que puede quedarse con UNO solo, porque el <span> de la duración es condicional
   (VideosDeHoy.razor) y `justify-content: space-between` con un único ítem lo pega al inicio.

   No es un borde raro sino un modo de despliegue entero: ProbeDurationSecondsAsync devuelve null
   cuando ffprobe no está, y su propio comentario dice que ffprobe es opcional -- en una máquina
   sin ffprobe caen TODAS las tarjetas. Antes eso sólo corría la hora de lado; con §4.4 encima se
   lleva también el botón, o sea la única acción de la tarjeta cambiaría de borde entre una tarjeta
   y la siguiente de la misma grilla.

   `margin-left: auto` fija este bloque a la derecha haya o no hermano, y deja de depender del
   reparto. De paso arregla el salto viejo de la hora, que no venía de §4.4. */
.surf-video-hora {
    display: inline-flex;
    align-items: baseline;
    gap: 0.35rem;
    margin-left: auto;
}

/* Un botón que se lee como enlace y no como acción principal: corregir la hora es una salida de
   excepción para el clip propio, no lo que uno viene a hacer al feed. Hereda tamaño y opacidad del
   meta en vez de estrenar los suyos, por la misma razón que .surf-video-uploader: el producto ya
   decidió ahí cómo se ve el dato secundario de una tarjeta.

   Este párrafo exime de área táctil A ESTE BOTÓN Y A NINGÚN OTRO: vive dentro de una línea de
   texto de 0.85em, y estirarlo separaría la duración de la hora en todas las tarjetas, incluidas
   las que no muestran el botón. Los tres botones del editor —Guardar, Usar la hora de ingreso y
   Cancelar— NO caen bajo esta excepción: no están en ninguna línea de texto y usan .photo-button,
   el botón de acción del producto.

   Y una corrección a lo que decía acá antes, porque el número era inventado: este comentario
   afirmaba que «los 44px de área táctil son de app.css». NO EXISTEN. Medido por Tito y
   verificado por mí sobre este mismo árbol: el único `44px` de la hoja es el degradado decorativo
   del horizonte en :2260. Citar una regla que no está escrita es peor que no citar ninguna —
   parece que la decisión ya se tomó en otro lado, y nadie va a ir a buscarla. */
.surf-video-hora-boton {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-decoration: underline;
    cursor: pointer;
}

/* ACÁ NO VA UNA `opacity`, Y LA AUSENCIA ES LA DECISIÓN. La opacidad no se hereda: se compone.
   Este botón vive dentro de .surf-video-meta (:3229), que ya trae opacity 0.75, así que un
   0.8 propio no lo dejaba «un poco más tenue» sino en 0.75 x 0.8 = 0.60 — y con la tinta
   #4a4033 sobre esta tarjeta eso da 3,24:1 de contraste, bajo el 4,5:1 que pide WCAG para
   13,6px de texto normal. Era el ÚNICO elemento de la tarjeta que no llegaba, y justo el único
   control interactivo que tiene: el afordance más débil era el que hay que descubrir para poder
   corregir la hora (D49 §4.4). Sin la declaración queda en el 0.75 del meta = 4,74:1, que es lo
   que el comentario de arriba ya decía que quería. Medido por Tito sobre `e154d87` y verificado
   por mí en el markup: el botón está anidado en .surf-video-meta > .surf-video-hora
   (VideosDeHoy.razor:98-120), que es de dónde sale la composición.
   Lo ancla ContrasteDelBotonDeHoraTests. */

.surf-video-hora-editor {
    margin-top: 0.35rem;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.surf-video-hora-editor input {
    font: inherit;
    padding: 0.2rem 0.35rem;
}

/* Envuelve en vez de desbordar: son hasta tres botones y la tarjeta del feed mide 243px de ancho
   en la grilla real. */
.surf-video-hora-acciones {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

/* "No es mía" returns a clip to the common pool, and it sat right next to "Descargar"
   looking exactly the same - the kind of sameness that gets the wrong one tapped with a
   thumb, on a phone, in motion. Outlined instead of filled: enough hierarchy to tell them
   apart, no alarm colour, because returning a clip is reversible. Scoped to the card so
   the shared .photo-button keeps its look everywhere else. */
.surf-video-action-secondary {
    background: transparent;
    color: #a8544f;
    border: 1px solid #dd8787;
    text-shadow: none;
}

.surf-video-action-secondary:hover {
    background: rgba(221, 135, 135, 0.18);
    filter: none;
    color: #8f3f3a;
}

/* Calibrating the skeleton for THIS tab only. .module-loading-skeleton-cards is shared with
   Announcements (two uses) and Historia, which pair it with narrower content; recalibrating
   it globally would fix this tab by breaking those three.

   auto-FILL here, against auto-FIT in the grid above, and the difference is deliberate even
   though it looks like the inconsistency it replaces. The skeleton always emits exactly two
   cards (Rows="2"), so auto-fit would collapse the empty tracks and let those two stretch
   across the row: 621px each at 1280px, against 243.2px for a real card. auto-fill keeps the
   empty tracks, so the two skeleton cards land on a track width CLOSE TO the one a full grid
   uses - 237.4px against 243.2px at 1280px: 2.4% narrower. CLOSE, NOT EQUAL, and the gap
   between those two words is what this comment got wrong for months. The content grid has a
   variable count and should fill; the skeleton has a fixed count and must not. Don't "unify"
   these two without measuring both.

   DE QUE CLASE ES CADA NUMERO DE ARRIBA, que es lo que aca faltaba y sin lo cual una cuenta a
   mano y una medicion se leen con el mismo tono de certeza (reparo de Tito, 2026-08-25):

     243.2px  MEDIDO   tarjeta real con 5+ clips. Tito sobre 510b569; yo di 243.19.
     237.4px  MEDIDO   tarjeta del esqueleto calibrado. Tito; yo di 237.36.
     621px    MEDIDO   contrafactico: lo que darian las dos tarjetas si esta pestana NO tuviera
                       la calibracion de abajo. Tito en D62 2.1; yo di 621.00.
     1250.8   CALCULADO  ancho util = 1280 - 2x13.6 (padding de .module-loading-panel) - 2x1
                       (borde de .module-loading). De aca sale a mano (1250.8-8.8)/2 = 621, que
                       coincide con lo medido. La cuenta NO reemplaza la medicion: la explica.

   COMO SE REMIDEN: scripts/banco-esqueleto-vs-grilla.html con scripts/medir-tipografia-viento-
   cdp.js. Renata, 2026-08-25, Chrome for Testing 151.0.7922.34 headless; medido primero sobre
   7c095ab y vuelto a correr sobre la cabeza ya sincronizada con main, mismos tres numeros.

   LOS NUMEROS QUE ESTABAN ACA ANTES (632px, y "measured identical at all six widths") eran
   aritmetica sobre el viewport de 1280 en vez de sobre el ancho util, o sea les faltaban esos
   29.2px. Cuidado al copiar el marcado a un banco: si le sacas la envoltura
   .module-loading-panel/.module-loading-shell te da 635 y "identicos", que es justo la
   respuesta vieja y equivocada. Ese banco no mide el componente, mide tu copia del componente.

   Getting as close as it gets across all six widths costs one more thing, and it is not
   obvious from here: at 520px and below, the shared rule at line ~800 collapses EVERY .module-loading-
   skeleton-cards to a single column on purpose. This tab no longer obeys it - the rule below
   is outside that media query, so it keeps winning down to 390px. That is deliberate and it
   is what removes the old inversion: .surf-videos-grid (the real grid) is not in that media
   query either, so around 490px the skeleton used to show ONE card while the real clips
   showed two. If you ever narrow that 520px rule, this tab is the exception that will not
   follow you.

   EL UNIVERSO DE LOS 243.2px, QUE ARRIBA FALTA (Tito, 2026-08-24, sobre 510b569). El numero
   que decide toda la eleccion de arriba - «621px each at 1280px, against 243.2px for a real
   card» - no es el ancho de una tarjeta real: es el ancho de una tarjeta real EN UN FEED
   LLENO. Con auto-fit, el ancho del contenido depende de CUANTOS clips llegaron, y eso no se
   sabe cuando el esqueleto esta en pantalla. Medido a 1280px con chrome-headless-shell 151
   (marcado fiel de BuildCards Rows=2 dentro de .surf-videos-loading, contando cajas que
   comparten fila):

     clips:      1      2      3      4      5+
     real:     420px  420px  416px  308px  243.2px
     esqueleto (auto-fill, siempre):          237.4px

   O sea la calibracion de arriba acierta al 2.4% con 5+ clips y se va al 77% con 2. La
   eleccion sigue siendo defendible, pero es una eleccion ENTRE DOS CASOS y no la unica
   lectura posible: con auto-fit + el mismo tope de 420px el esqueleto da 420px, exacto en el
   caso ralo - que es el unico donde ademas coincide el CONTEO (Rows=2 contra 2 clips) y por
   lo tanto el unico donde el esqueleto puede acertar del todo. Con feed lleno el conteo ya
   falla (2 contra 5) y ninguna regla de columnas lo salva.

   Los dos saltos son casi iguales - 182.6px en el caso ralo con lo de hoy, 176.8px en el
   caso lleno con la alternativa - asi que cual conviene depende de cuantos clips trae un dia
   normal en un spot, que es dato de produccion y NO esta medido: es lo mismo que D49 §4.7
   deja abierto. Mientras no se mida, no lo voltees; y si lo volteas, que sea con ese dato en
   la mano y no con este comentario. Detalle en producto/D62 §2.7. */
.surf-videos-loading .module-loading-skeleton {
    gap: 1em;
}

.surf-videos-loading .module-loading-skeleton-cards {
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}

/* ---------------------------------------------------------------------------
   Video admin: the board of every clip and who claimed it. A list rather than a
   wall of <video> tags - preloading five hundred clips to read five hundred
   names is a lot of droplet bandwidth for a table. One player opens at a time,
   above the list, when a row is picked.
   --------------------------------------------------------------------------- */
.video-owner-board {
    margin-top: 1.6em;
}

.video-owner-title {
    margin: 0 0 .4em;
    font-size: 1.05em;
}

.video-owner-player {
    display: flex;
    flex-direction: column;
    gap: .4em;
    margin-bottom: .8em;
}

.video-owner-player video {
    width: 100%;
    max-height: 45vh;
    border-radius: 3px;
    background: #000;
}

.video-owner-player-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .6em;
    font-size: .9em;
}

.video-owner-cell {
    text-align: left;
    white-space: nowrap;
}

.video-owner-when {
    display: block;
    font-size: .78em;
    opacity: .7;
    font-weight: normal;
}

.video-owner-none {
    opacity: .6;
    font-style: italic;
}

/* Un nombre de Google puede ser larguísimo, y en una tabla que ya scrollea de lado
   ensancharía la columna del dueño hasta empujar el peso y el botón fuera de la vista. */
.video-owner-name {
    display: inline-block;
    max-width: 11em;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: middle;
}

/* Un outline sobre <tr> se dibuja fuera de la fila cuando la tabla tiene los bordes
   colapsados, y termina pareciendo una línea suelta entre dos filas ajenas. El fondo
   se pinta en las celdas. Va calificado con .video-owner-grid porque ".sprite-grid td"
   pinta blanco con la misma especificidad y aparece más abajo en esta hoja: sin el
   calificador la clase se aplica, no falla nada, y no se ve absolutamente nada. */
.video-owner-grid .video-owner-row-playing > th,
.video-owner-grid .video-owner-row-playing > td {
    background: #d8e9fb;
}

.video-owner-play {
    padding: .15em .6em;
    line-height: 1.4;
}

/* ---------------------------------------------------------------------------
   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;
}

/* Marca de "todavía no entra al juego": discreta, porque la fila sirve igual - se le puede
   regalar un sprite hoy y lo tendrá cuando juegue. */
.sprite-grid-player-new {
    display: inline-block;
    vertical-align: middle;
    margin-left: .4em;
    padding: 0 .4em;
    border-radius: 999px;
    background: #eceff1;
    color: #607d8b;
    font-size: .72em;
    white-space: nowrap;
}

/* .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-section {
    margin-top: 1em;
}

.locker-room-section h4 {
    margin: 0 0 .5em;
}

.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;
}

/* The module matrix is tuned to fit 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-fixed-note {
    display: block;
    margin-top: .15em;
    color: #777;
    font-size: .62em;
    line-height: 1.1;
}

.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-window-head {
    min-width: 108px;
}

.module-admin-window-summary {
    min-width: 9.5em;
    max-width: 11em;
    font-size: .76em;
    line-height: 1.15;
    text-align: left;
}

.module-admin-window-note {
    display: block;
    margin-top: .2em;
    color: #777;
    font-size: .82em;
}

.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;
}

/* ---------------------------------------------------------------------------
   Anuncios: a wall of compact post-its. Cards intentionally stay small so a
   reviewer/player can see several at once instead of one giant photo per screen.
   --------------------------------------------------------------------------- */
.announcements {
    width: 100%;
    box-sizing: border-box;
    padding: .8em;
}

.announcements-composer,
.announcements-history {
    max-width: 56rem;
    margin: 0 auto 1em;
    padding: .8em;
    border: 1px solid rgba(0, 0, 0, .08);
    border-radius: 12px;
    background: rgba(255, 255, 255, .72);
}

.announcements-composer h3 {
    margin: 0 0 .25em;
}

/* Sin formulario que enmarcar, el recuadro no encierra nada: sólo empuja el muro hacia abajo,
   que es lo primero que se viene a mirar. La mayoría de los lectores no puede publicar. */
.announcements-composer-readonly {
    padding: 0 .8em;
    border: 0;
    background: none;
}

.announcements-form {
    display: flex;
    flex-direction: column;
    gap: .55em;
}

.announcements-textarea {
    min-height: 7em;
    resize: vertical;
    border: 1px solid #d7c78f;
    border-radius: 8px;
    padding: .65em;
    font: inherit;
    background: #fffdf2;
}

.announcements-form-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5em;
}

.announcements-photo-picker {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: .35em;
    padding: .45em .7em;
    border: 1px solid #cdbb7a;
    border-radius: 999px;
    background: #fff5c6;
    cursor: pointer;
}

.announcements-photo-preview-row,
.announcement-photos {
    display: flex;
    flex-wrap: wrap;
    gap: .35em;
}

.announcements-photo-preview {
    margin: 0;
    max-width: 92px;
    font-size: .68em;
    color: #665;
}

.announcements-photo-preview img,
.announcement-photos img {
    display: block;
    width: 82px;
    height: 62px;
    object-fit: cover;
    border-radius: 6px;
    border: 1px solid rgba(0, 0, 0, .12);
    background: #eee;
}

.announcements-photo-preview figcaption {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.announcements-readonly,
.announcements-empty {
    color: #655;
}

.announcements-history-toggle {
    margin-top: .5em;
}

.announcements-wall {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
    gap: .75em;
    align-items: start;
}

.announcement-card {
    display: flex;
    flex-direction: column;
    gap: .45em;
    min-height: 9em;
    padding: .75em;
    border: 1px solid #e0cc74;
    border-radius: 8px;
    background: #fff6a8;
    box-shadow: 0 2px 6px rgba(0, 0, 0, .14);
    transform: rotate(-.35deg);
}

.announcement-card:nth-child(3n) {
    background: #e5f7c8;
    transform: rotate(.45deg);
}

.announcement-card:nth-child(4n) {
    background: #ffd6dd;
    transform: rotate(-.6deg);
}

.announcement-card-header {
    display: flex;
    justify-content: space-between;
    gap: .7em;
    font-size: .78em;
    color: #5d5230;
}

.announcement-markdown {
    font-size: .92em;
    line-height: 1.3;
    overflow-wrap: anywhere;
}

.announcement-markdown :first-child {
    margin-top: 0;
}

.announcement-markdown :last-child {
    margin-bottom: 0;
}

.announcement-markdown img {
    max-width: 100%;
    max-height: 110px;
    object-fit: contain;
}

.announcement-markdown table {
    display: block;
    overflow-x: auto;
    max-width: 100%;
    border-collapse: collapse;
    font-size: .85em;
}

.announcement-markdown th,
.announcement-markdown td {
    border: 1px solid rgba(0, 0, 0, .18);
    padding: .2em .35em;
}

.announcement-remove {
    align-self: flex-start;
    margin-top: auto;
    font-size: .8em;
}

.announcements-history h4 {
    margin: 0 0 .6em;
}

.announcements-wall-history {
    opacity: .92;
}

.announcement-card-deleted {
    background: #eee;
    border-style: dashed;
    transform: none;
}

.announcement-removed-label {
    align-self: flex-start;
    padding: .15em .45em;
    border-radius: 999px;
    background: rgba(0, 0, 0, .12);
    font-size: .72em;
}

/* ---- 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-row-hazard {
    border-color: #d59a8e;
    background: #fff1ee;
    box-shadow: inset 4px 0 0 #c1523f;
}

.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 {
    display: block;
    font-size: .88em;
    font-weight: 600;
    color: #4a4033;
}

.capture-summary {
    min-width: 0;
    flex: 1 1 auto;
}

.capture-title {
    display: block;
    font-weight: 700;
    color: #3a3228;
}

.capture-row-hazard .capture-title {
    color: #8f2f22;
}

.capture-points {
    flex: 0 0 auto;
    min-width: 2.6em;
    border-radius: 999px;
    padding: .18em .5em;
    text-align: center;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

.capture-points-positive {
    background: #e6f4df;
    color: #2f6b2f;
}

.capture-points-negative {
    background: #f6d7d1;
    color: #9d3326;
}

.capture-points-zero {
    background: #ece3d3;
    color: #6b5d4a;
}

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

.capture-row-hazard .capture-details {
    border-top-color: #d59a8e;
}

.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;
    box-sizing: border-box;
    max-width: min(420px, calc(100% - 0.8rem));
    margin: 6px auto;
    padding: 7px 12px;
    border: 1px solid #e2d4a8;
    border-radius: 999px;
    background: #fdf7e3;
    color: #4a4033;
    font-family: inherit;
    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-hide {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 1.7em;
    height: 1.7em;
    border: none;
    border-radius: 999px;
    background: rgba(122, 106, 69, 0.12);
    color: #7a6a45;
    cursor: pointer;
    font: inherit;
    font-weight: 700;
    line-height: 1;
}

.streak-hide:hover {
    background: rgba(122, 106, 69, 0.22);
}

.streak-restore {
    cursor: pointer;
}

.streak-restore-label {
    font-weight: 600;
}

.streak-restore strong {
    color: #2f7d32;
}

.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;
}

/* Último snapshot por fuente, en el panel del administrador de pronóstico.
   El semáforo se calcula en el componente contra la cadencia que declara cada fuente, así que acá
   sólo viven los colores: el CSS no sabe qué es "atrasado" y no debe saberlo. */
.forecast-history-table {
    margin-top: 0.35rem;
}

/* Más apretada que la grilla de cuentas: son cinco datos cortos por fila y el panel de
   administración es una columna angosta, así que con el relleno normal la última columna queda
   fuera del borde y hay que arrastrar la tabla para leer un número de dos cifras. */
.forecast-history-table th,
.forecast-history-table td {
    padding: 0.34em 0.45em;
    font-size: 0.93em;
}

.forecast-history-dot {
    display: inline-block;
    width: 0.62rem;
    height: 0.62rem;
    margin-right: 0.42rem;
    border-radius: 50%;
    vertical-align: baseline;
    /* Un borde interior oscuro: sobre el fondo claro de la tabla los tres colores se distinguen,
       pero el punto es chico y sin contorno el ámbar se lava. */
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.28);
}

.forecast-history-green { background: #2ca02c; }
.forecast-history-amber { background: #e8a33d; }
.forecast-history-red { background: #c8241d; }

/* La antigüedad y la marca exacta son dos datos, no una frase: sin esto la celda las parte
   donde caiga el ancho disponible y "hace 5 h 18 min" queda repartido en tres renglones. */
.forecast-history-age {
    display: block;
    white-space: nowrap;
    font-weight: 600;
}

.forecast-history-stamp {
    display: block;
    white-space: nowrap;
    font-size: 0.82em;
    opacity: 0.72;
}

.forecast-history-empty {
    color: #8a5a56;
    font-style: italic;
}

/* Panel de módulos: leyenda de los niveles de acceso y el selector de cada celda. */
.module-access-legend {
    list-style: none;
    margin: 0 0 0.75rem;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 1rem;
    font-size: 0.82rem;
    color: #4a4a4a;
}

.module-access-legend li {
    display: flex;
    align-items: baseline;
    gap: 0.3rem;
}

.module-access-select {
    width: 100%;
    min-width: 8.5rem;
    padding: 0.2rem 0.3rem;
    border: 1px solid #c9c9c9;
    border-radius: 6px;
    background: #fff;
    font-size: 0.82rem;
}

.module-access-select:disabled {
    opacity: 0.55;
}

/* Orden de los iconos de la barra, en el panel de administración. */
.menu-order-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
}

.menu-order-item {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.2rem 0.35rem;
    border: 1px solid #d8d8d8;
    border-radius: 8px;
    background: #fff;
    font-size: 0.82rem;
    white-space: nowrap;
}

/* La primera es la que abre el escenario: se marca para no tener que contar. */
.menu-order-first {
    border-color: #e07a5f;
    background: #fdf1ec;
}

/* La posición, porque la lista se envuelve en varias líneas y sin número el orden
   se vuelve ambiguo justo donde corta. */
.menu-order-index {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.15rem;
    height: 1.15rem;
    border-radius: 50%;
    background: #ececec;
    color: #555;
    font-size: 0.7rem;
    font-weight: 700;
}

.menu-order-first .menu-order-index {
    background: #e07a5f;
    color: #fff;
}

.menu-order-badge {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: #b5533a;
}

.menu-order-move {
    border: 1px solid #c9c9c9;
    border-radius: 6px;
    background: #f7f7f7;
    cursor: pointer;
    padding: 0 0.3rem;
    line-height: 1.4;
}

.menu-order-move:disabled {
    opacity: 0.35;
    cursor: default;
}

/* Aviso de mundo detenido: mismo lenguaje visual que .tutorial-card (misma tarjeta, mismo
   ancho, mismo borde), en tono de advertencia. No es un error del jugador ni algo que él pueda
   resolver, así que no lleva botón ni se puede cerrar: desaparece solo cuando el mundo vuelve
   a moverse. */
.mundo-detenido {
    width: min(500px, calc(100% - 1rem));
    box-sizing: border-box;
    margin: 0.35rem auto 0.5rem;
    padding: 0.65rem 0.75rem;
    display: flex;
    align-items: center;
    gap: 0.55rem;
    color: #4a4033;
    background: #ffe2ce;
    border: 1px solid #d8a07a;
    border-radius: 16px;
    box-shadow: 0 7px 20px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.78);
}

.mundo-detenido-icono {
    flex: 0 0 auto;
    width: 1.8rem;
    height: 1.8rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: #fff;
    color: #a83f3f;
    border: 1px solid #d8a07a;
    font-weight: 800;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
}

.mundo-detenido-texto {
    display: grid;
    gap: 0.15rem;
    text-align: left;
    font-size: 0.86rem;
    line-height: 1.25;
}
