/* ============================================================
   RoboKyle Arcade, hub stylesheet.

   The page is one idea: a dark room, and the games are the only
   things lit in it. Everything in the environment is cool and
   dim, every warm colour comes from a cabinet, and each card
   throws its own light onto the floor. That is why the palette
   splits into a cold half and a per card --accent rather than
   one page wide tint.

   Self contained: this page does not load index.css, so the
   shared bits (skip link, focus ring, sr-only) are repeated
   here on purpose.
   ============================================================ */

:root {
    /* the room */
    --void:      #04050A;
    --void-2:    #070A12;
    --panel:     #0C1018;
    --line:      #202839;
    --line-soft: #161C29;
    --grid:      rgba(122,172,238,.32);
    --grid-hot:  rgba(168,212,255,.8);

    /* type */
    --ink:       #F1E4D2;
    --ink-soft:  #A8B2C4;
    --ink-mute:  #67717F;

    /* the cabinets */
    --orange:     #E3552B;
    --orange-hot: #FF7A3D;
    --gold:       #E0B44C;
    --sky:        #4FB8D8;
    --green:      #7BC59A;
}

*, *::before, *::after { box-sizing: border-box; }

body.hub-body {
    margin: 0;
    min-height: 100vh;
    background: var(--void);
    color: var(--ink);
    font-family: 'Inter', system-ui, -apple-system, "Segoe UI", sans-serif;
    -webkit-font-smoothing: antialiased;
    position: relative;
    overflow-x: hidden;
    display: flex;
    flex-direction: column;
}

/* ===== The room ===================================================== */

.stage {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    overflow: hidden;
}

/* Air above the floor. Cool and almost empty, so the cards read as
   the only source of warmth on the page. */
.stage-haze {
    position: absolute;
    inset: 0;
    background:
        radial-gradient(100% 62% at 50% 4%, rgba(74,116,186,.16) 0%, transparent 64%),
        radial-gradient(72% 46% at 50% 58%, rgba(58,92,152,.14) 0%, transparent 70%),
        linear-gradient(to bottom, var(--void) 0%, var(--void-2) 48%, var(--void) 100%);
}

/* A tiled floor running back to a vanishing point.

   The origin has to be the BOTTOM edge. Tipping from the top rotates the
   near end away from the viewer and the plane reads as streaks in the
   corners rather than a floor. Pinned at the bottom, the far end lays
   down toward the horizon, which is what we want. */
.stage-floor {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 68%;
    overflow: hidden;
    mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.35) 26%, #000 58%, #000 88%, transparent 100%);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.35) 26%, #000 58%, #000 88%, transparent 100%);
}
.stage-grid {
    position: absolute;
    left: -60%;
    right: -60%;
    bottom: 0;
    height: 200%;
    transform: perspective(440px) rotateX(64deg);
    transform-origin: 50% 100%;
    background-image:
        linear-gradient(to right,  var(--grid) 1px, transparent 1px),
        linear-gradient(to bottom, var(--grid) 1px, transparent 1px);
    background-size: 94px 94px;
}

/* The line where the floor meets the dark, plus its bloom. */
.stage-horizon {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 68%;
    height: 1px;
    background: linear-gradient(to right, transparent, var(--grid-hot) 26%, var(--grid-hot) 74%, transparent);
    opacity: .55;
}
.stage-horizon::before {
    content: "";
    position: absolute;
    left: -20%;
    right: -20%;
    top: -120px;
    height: 260px;
    background: radial-gradient(46% 100% at 50% 50%, rgba(132,184,250,.26) 0%, transparent 76%);
}

/* Dust in the air. */
.stage-dust { position: absolute; inset: 0; }
.stage-dust i {
    position: absolute;
    left: var(--x);
    top: var(--y);
    width: calc(var(--s) * 2px);
    height: calc(var(--s) * 2px);
    border-radius: 50%;
    background: rgba(178,208,255,.7);
    opacity: 0;
    animation: dustDrift 19s ease-in-out infinite;
    animation-delay: calc(var(--d) * -1.9s);
}
@keyframes dustDrift {
    0%   { opacity: 0;   transform: translate3d(0, 14px, 0); }
    18%  { opacity: .55; }
    72%  { opacity: .3; }
    100% { opacity: 0;   transform: translate3d(18px, -52px, 0); }
}

/* A soft light that follows the pointer. hub.js sets --mx / --my; with no
   script the light simply sits in the middle and nothing looks broken. */
.stage-cursor {
    position: absolute;
    inset: 0;
    background: radial-gradient(
        320px 320px at var(--mx, 50%) var(--my, 38%),
        rgba(126,172,240,.11) 0%,
        transparent 70%);
    transition: opacity .4s ease;
}

/* ===== Overlays, above the content ================================== */

/* Scanlines and a vignette. Both very light: enough to suggest a screen,
   not enough to fight the card art underneath. */
body.hub-body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    background:
        repeating-linear-gradient(to bottom,
            rgba(0,0,0,.16) 0 1px, transparent 1px 3px),
        radial-gradient(130% 96% at 50% 42%, transparent 48%, rgba(0,0,0,.86) 100%);
}

.crt-roll {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    height: 44vh;
    z-index: 5;
    pointer-events: none;
    background: linear-gradient(to bottom,
        transparent 0%, rgba(184,216,255,.032) 48%, transparent 100%);
    animation: crtRoll 16s linear infinite;
}
@keyframes crtRoll {
    from { transform: translateY(-56vh); }
    to   { transform: translateY(152vh); }
}

/* Grain. This is the layer that stops a CSS gradient reading as one. */
body.hub-body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 4;
    pointer-events: none;
    opacity: .055;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* ===== Furniture ==================================================== */

.skip-link {
    position: absolute;
    top: -100px;
    left: 12px;
    z-index: 100;
    background: var(--ink);
    color: var(--void);
    padding: .6rem .9rem;
    border-radius: 8px;
    text-decoration: none;
    font-weight: 600;
}
.skip-link:focus { top: 12px; }

.hub-body :focus-visible {
    outline: 3px solid var(--orange);
    outline-offset: 3px;
    border-radius: 4px;
}

.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

.hub-shell {
    position: relative;
    z-index: 1;
    width: 100%;
    max-width: 1180px;
    margin: 0 auto;
    padding: clamp(2rem, 5vw, 4rem) clamp(1.1rem, 4vw, 2.5rem) 3rem;
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

/* ===== Marquee ====================================================== */

.hub-head {
    margin-bottom: clamp(2.2rem, 5vw, 3.4rem);
    text-align: center;
}

.hub-logo {
    font-family: 'Black Ops One', 'Fraunces', Impact, sans-serif;
    font-size: clamp(2.05rem, 8.6vw, 5.6rem);
    line-height: .92;
    letter-spacing: .01em;
    margin: 0;
    text-transform: uppercase;
    display: inline-flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: center;
    gap: .08em;
}
.hub-logo .robo,
.hub-logo .kyle { transform: skew(-6deg); }

.hub-logo .robo {
    color: #FBF3E6;
    -webkit-text-stroke: 1.5px #000;
    text-shadow:
        0 3px 0 #000,
        0 0 18px rgba(214,232,255,.35),
        0 0 46px rgba(120,170,240,.22);
}

/* The one thing on the page that behaves like a neon tube. */
.hub-logo .kyle {
    color: var(--orange-hot);
    -webkit-text-stroke: 1.5px #000;
    text-shadow:
        0 3px 0 #000,
        0 0 10px rgba(255,122,61,.85),
        0 0 30px rgba(227,85,43,.7),
        0 0 70px rgba(227,85,43,.45),
        0 0 120px rgba(180,35,28,.35);
    animation: neonStrike 1.1s ease-out both,
               tubeHum 5.5s ease-in-out 1.1s infinite;
}
@keyframes neonStrike {
    0%   { opacity: .55; filter: brightness(.6); }
    30%  { opacity: .92; filter: brightness(1.18); }
    52%  { opacity: .7;  filter: brightness(.8); }
    100% { opacity: 1;   filter: brightness(1); }
}
@keyframes tubeHum {
    0%, 100%      { filter: brightness(1); }
    42%           { filter: brightness(1.12); }
    46%           { filter: brightness(.9); }
    49%           { filter: brightness(1.14); }
    54%           { filter: brightness(.97); }
}

.hub-logo .arcade {
    font-size: .3em;
    letter-spacing: .52em;
    text-indent: .52em;
    color: var(--ink-mute);
    -webkit-text-stroke: 0;
    text-shadow: none;
    transform: skew(-6deg);
    align-self: center;
    margin-left: .5em;
}

/* A thin rule with a lit centre, like light spilling under a door. */
.hub-head::after {
    content: "";
    display: block;
    height: 1px;
    margin: clamp(1.2rem, 3vw, 1.9rem) auto 0;
    max-width: 640px;
    background: linear-gradient(to right,
        transparent, var(--line), rgba(160,200,255,.45), var(--line), transparent);
}

/* ===== The lineup =================================================== */

.game-list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    gap: clamp(1.1rem, 2.6vw, 1.9rem);
    list-style: none;
    padding: 0;
    margin: 0;
}

.game-item {
    perspective: 1200px;
    animation: cabinetIn .7s cubic-bezier(.2,.7,.3,1) backwards;
    animation-delay: calc(var(--i, 0) * 130ms + 120ms);
}
@keyframes cabinetIn {
    from { opacity: 0; transform: translateY(26px); }
    to   { opacity: 1; transform: none; }
}

.game-card {
    --accent: var(--orange);
    --lift: 0px;
    --rx: 0deg;
    --ry: 0deg;
    position: relative;
    display: flex;
    flex-direction: column;
    height: 100%;
    background: linear-gradient(var(--panel), var(--void-2));
    border: 1px solid var(--line-soft);
    border-radius: 18px;
    overflow: hidden;
    text-decoration: none;
    color: inherit;
    transform: translateY(var(--lift)) rotateX(var(--rx)) rotateY(var(--ry));
    transform-style: preserve-3d;
    box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 12px 30px rgba(0,0,0,.6);
    box-shadow:
        inset 0 1px 0 rgba(255,255,255,.05),
        0 26px 52px -28px color-mix(in srgb, var(--accent) 62%, transparent),
        0 12px 30px rgba(0,0,0,.6);
    transition: transform .25s cubic-bezier(.2,.7,.3,1),
                border-color .25s ease, box-shadow .25s ease;
}
.game-card.card-undead { --accent: var(--orange); }
.game-card.card-heist  { --accent: var(--gold); }
.game-card.card-fly    { --accent: var(--sky); }

a.game-card:hover,
a.game-card:focus-visible {
    --lift: -6px;
    border-color: color-mix(in srgb, var(--accent) 58%, transparent);
    box-shadow:
        inset 0 1px 0 rgba(255,255,255,.1),
        0 40px 74px -26px color-mix(in srgb, var(--accent) 82%, transparent),
        0 18px 38px rgba(0,0,0,.66);
}
/* While the pointer is steering a card, drop the easing so it tracks. */
.game-card.is-tilting { transition: border-color .25s ease, box-shadow .25s ease; }

/* ===== Thumbnail ==================================================== */

.card-thumb {
    position: relative;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    background: #06050A;
    border-bottom: 1px solid var(--line-soft);
}

.thumb-art {
    position: absolute;
    inset: 0;
    transition: transform .5s cubic-bezier(.2,.7,.3,1);
    will-change: transform;
}
.thumb-art .art { display: block; width: 100%; height: 100%; }

a.game-card:hover .thumb-art,
a.game-card:focus-visible .thumb-art { transform: scale(1.06); }

/* Darkens the bottom left so the wordmark always has something to sit on,
   which is why the busy half of both scenes is drawn to the right. */
.thumb-scrim {
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
        linear-gradient(to top, rgba(4,3,8,.92) 0%, rgba(4,3,8,.62) 14%, rgba(4,3,8,.18) 32%, rgba(4,3,8,0) 46%),
        linear-gradient(to right, rgba(4,3,8,.58) 0%, rgba(4,3,8,0) 48%);
}

.thumb-mark {
    position: absolute;
    left: clamp(.85rem, 3.5%, 1.15rem);
    bottom: clamp(.7rem, 3.5%, 1rem);
    right: 4.7rem;
}
.mark-title { margin: 0; }
.mark-brand {
    display: inline-flex;
    gap: .08em;
    font-family: 'Black Ops One', Impact, sans-serif;
    font-size: clamp(.9rem, 2.7vw, 1.05rem);
    letter-spacing: .02em;
    line-height: 1;
    text-transform: uppercase;
    filter: drop-shadow(0 2px 6px rgba(0,0,0,.85));
}
.mark-brand .robo {
    color: #FFF6E8;
    text-shadow: 0 1.5px 0 #000, 0 0 8px rgba(0,0,0,.9);
    transform: skew(-6deg);
}
.mark-brand .kyle {
    color: var(--orange-hot);
    text-shadow: 0 1.5px 0 #000, 0 0 8px rgba(0,0,0,.9), 0 0 16px rgba(227,85,43,.7);
    transform: skew(-6deg);
}
.mark-name {
    display: block;
    font-family: 'Black Ops One', Impact, sans-serif;
    /* Sized so the longest name still fits one line in a three column
       grid. It wrapped and crowded the Play button at 1.72rem. */
    font-size: clamp(.98rem, 3.2vw, 1.3rem);
    line-height: 1.02;
    letter-spacing: .015em;
    text-transform: uppercase;
    color: #F6ECDC;
    margin-top: .16em;
    transform: skew(-6deg);
    -webkit-text-stroke: 1px #000;
    text-shadow: 0 2px 0 #000, 0 0 22px rgba(180,35,28,.55);
}
.mark-name.gold {
    color: #F2CE72;
    text-shadow: 0 2px 0 #000, 0 0 20px rgba(224,180,76,.6);
}
.mark-name.sky {
    color: #EDF8FF;
    text-shadow: 0 2px 0 #000, 0 0 20px rgba(79,184,216,.75);
}

/* Play button, quiet until the cabinet is hovered. */
.card-cta {
    position: absolute;
    right: clamp(.7rem, 3%, 1rem);
    bottom: clamp(.7rem, 3.5%, 1rem);
    display: inline-flex;
    align-items: center;
    gap: .38rem;
    font-family: 'Black Ops One', Impact, sans-serif;
    font-size: .72rem;
    letter-spacing: .12em;
    text-transform: uppercase;
    padding: .48rem .78rem;
    border-radius: 8px;
    color: var(--ink);
    background: rgba(9,7,12,.72);
    border: 1px solid rgba(241,228,210,.22);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
    transition: color .18s ease, background-color .18s ease,
                border-color .18s ease, transform .18s ease;
}
.card-cta svg { width: .62em; height: .62em; }
a.game-card:hover .card-cta,
a.game-card:focus-visible .card-cta {
    color: #08070B;
    background: var(--accent);
    border-color: var(--accent);
    transform: translateX(-2px);
}

/* Glass. A specular highlight that tracks the pointer across the cabinet,
   which is what sells the tilt as a physical object rather than a skew. */
.card-sheen {
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    border-radius: inherit;
    opacity: 0;
    transition: opacity .3s ease;
    background: radial-gradient(
        380px 380px at var(--px, 50%) var(--py, 0%),
        rgba(255,255,255,.1) 0%,
        rgba(255,255,255,.03) 34%,
        transparent 62%);
}
a.game-card:hover .card-sheen { opacity: 1; }

/* Status flag. Not used while everything on the page is playable, kept for
   the first game that ships unfinished. */
.card-flag {
    position: absolute;
    top: .7rem;
    left: .7rem;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    gap: .38rem;
    font-size: .6rem;
    font-weight: 700;
    letter-spacing: .13em;
    text-transform: uppercase;
    padding: .28rem .58rem;
    border-radius: 999px;
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
}
.card-flag i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.card-flag.is-live {
    color: var(--green);
    background: rgba(9,24,17,.8);
    border: 1px solid rgba(123,197,154,.38);
}
.card-flag.is-soon {
    color: var(--ink-mute);
    background: rgba(20,15,16,.85);
    border: 1px solid var(--line);
}

/* ===== Card text ==================================================== */

.card-body {
    padding: 1rem 1.15rem 1.15rem;
    flex: 1;
}

.card-desc {
    color: var(--ink-soft);
    font-size: .87rem;
    line-height: 1.6;
    margin: 0;
}

.game-card.is-placeholder {
    border-style: dashed;
    opacity: .7;
    pointer-events: none;
}
.game-card.is-placeholder .mark-name { color: var(--ink-mute); }

/* ============================================================
   Thumbnail art animation

   Everything below drives the inline SVG in game.html. Two rules
   throughout: a CSS transform overrides an element's transform
   attribute, so anything animated is either wrapped in a plain
   translated <g> or positioned by geometry instead; and
   transform-box says which coordinate system the origin is in.
   ============================================================ */

/* Undead Nightmare: embers rising off the street */
.un-ember {
    transform-box: fill-box;
    transform-origin: center;
    opacity: 0;
    animation: unEmber 7s linear infinite;
    animation-delay: calc(var(--i) * -1.1s);
}
@keyframes unEmber {
    0%   { opacity: 0;   transform: translateY(8px)   scale(.7); }
    16%  { opacity: .95; }
    72%  { opacity: .45; }
    100% { opacity: 0;   transform: translateY(-38px) scale(.4); }
}

/* Muzzle flash: bursts, holds, bursts again. */
.un-shot {
    transform-box: view-box;
    transform-origin: 235px 138px;
    animation: unShot 3.4s ease-out infinite;
}
@keyframes unShot {
    0%          { opacity: 1;   transform: scale(1); }
    12%         { opacity: .55; transform: scale(.9); }
    24%         { opacity: 1;   transform: scale(1.04); }
    40%, 100%   { opacity: .7;  transform: scale(.95); }
}

/* Grand Heist: the door works, the torch sweeps, the gold winks */
.gh-ring {
    transform-box: view-box;
    transform-origin: 312px 100px;
    animation: ghSpin 38s linear infinite;
}
.gh-dial {
    transform-box: view-box;
    transform-origin: 312px 100px;
    animation: ghSpin 24s linear infinite reverse;
}
@keyframes ghSpin { to { transform: rotate(360deg); } }

.gh-beam {
    transform-box: view-box;
    transform-origin: 44px 0;
    animation: ghSweep 11s ease-in-out infinite alternate;
}
@keyframes ghSweep {
    from { transform: rotate(-11deg); }
    to   { transform: rotate(13deg); }
}

.gh-glint {
    transform-box: fill-box;
    transform-origin: center;
    animation: ghTwinkle 3.6s ease-in-out infinite;
    animation-delay: calc(var(--i) * -.72s);
}
@keyframes ghTwinkle {
    0%, 100% { opacity: .12; transform: scale(.45); }
    50%      { opacity: 1;   transform: scale(1); }
}

/* Fly Game: clouds drifting and balloons bobbing. Both slow, because this
   card sits next to two dark ones and a busy bright card would shout. */
.fg-cloud {
    transform-box: fill-box;
    transform-origin: center;
    animation: fgDrift 26s ease-in-out infinite alternate;
    animation-delay: calc(var(--i) * -3.4s);
}
@keyframes fgDrift {
    from { transform: translateX(-5px); }
    to   { transform: translateX(7px); }
}
.fg-balloon {
    transform-box: fill-box;
    transform-origin: center;
    animation: fgBob 7s ease-in-out infinite alternate;
    animation-delay: calc(var(--i) * -1.6s);
}
@keyframes fgBob {
    from { transform: translateY(2.5px); }
    to   { transform: translateY(-3px); }
}
.fg-plane {
    transform-box: fill-box;
    transform-origin: center;
    animation: fgBank 9s ease-in-out infinite alternate;
}
@keyframes fgBank {
    from { transform: translateY(1.6px) rotate(-1.4deg); }
    to   { transform: translateY(-1.6px) rotate(1.4deg); }
}

/* Hovering a card spins its vault up, the one bit of art that reacts. */
a.game-card:hover .gh-ring,
a.game-card:focus-visible .gh-ring { animation-duration: 9s; }
a.game-card:hover .gh-dial,
a.game-card:focus-visible .gh-dial { animation-duration: 6s; }

/* ===== Footer ======================================================= */

.back-strip {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: clamp(1rem, 4vw, 2.2rem);
    margin-top: clamp(2.4rem, 5vw, 3.4rem);
    padding-top: 1.6rem;
    border-top: 1px solid var(--line-soft);
}
.back-strip a {
    color: var(--ink-soft);
    text-decoration: none;
    font-size: .85rem;
    transition: color .15s ease;
}
.back-strip a:hover { color: var(--orange); }

/* ===== Narrow screens =============================================== */

@media (max-width: 460px) {
    .game-list { grid-template-columns: 1fr; }
    .thumb-mark { right: 4.6rem; }
    .card-cta { font-size: .66rem; padding: .42rem .62rem; }
    .hub-logo .arcade { letter-spacing: .34em; text-indent: .34em; }
    .stage-floor { height: 54%; }
    .stage-horizon { bottom: 54%; }
}

@media (prefers-reduced-motion: reduce) {
    .game-card, .thumb-art, .card-cta, .card-sheen { transition: none; }
    a.game-card:hover .thumb-art,
    a.game-card:focus-visible .thumb-art { transform: none; }
    .game-item, .stage-dust i, .hub-logo .kyle, .crt-roll,
    .fg-cloud, .fg-balloon, .fg-plane,
    .un-ember, .un-shot,
    .gh-ring, .gh-dial, .gh-beam, .gh-glint { animation: none; }
    .stage-dust i { opacity: .4; }
    .un-ember { opacity: .8; }
    .gh-glint { opacity: .85; }
}
