/* Gato Arcade — hand-rolled stylesheet.
   ------------------------------------------------------------------
   Deliberately shares NO files with the cursos Ghost theme. That theme's
   homepage is a port of Pubzi (Envato Elements licence UWKCN6Z3PQ), whose
   End Product is registered as cursos.raulcontrerasherrera.com and is
   explicitly not transferable. The arcade is a second End Product, so it
   reuses the *design tokens* below — which are ours to choose — and none
   of the vendor CSS.

   The other reason is weight. The cursos homepage ships ~1.9 MB of
   Bootstrap, jQuery, Swiper, GSAP and Font Awesome. The arcade's entire
   audience arrives by a WhatsApp-forwarded link, which opens in an in-app
   browser on a mid-range Android over 4G. A grid of cards does not need a
   framework, and the first paint is the recruiting pitch. */

/* --- Tokens — copied by VALUE from the cursos theme so the two sites read
   as one brand. If the accent changes there, change it here too; nothing
   links them automatically and that is intentional. */
:root {
    --bg:         #0B0E13;
    --bg-raised:  #1C1D20;
    --border:     #ffffff1f;
    --text:       #ABABAB;
    --text-dim:   #8d8d8d;
    --text-faint: #6b6b6b;
    --accent:     #CBFE1C;
    --accent-dim: #4a5f10;
    --white:      #fff;

    --display: "Days One", system-ui, sans-serif;
    --sans: "Chakra Petch", system-ui, -apple-system, "Segoe UI", sans-serif;

    --gutter: clamp(16px, 4vw, 40px);
    --maxw: 1240px;
    --radius: 14px;
}

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

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--sans);
    font-size: 16px;
    line-height: 1.6;
}

img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }

h1, h2, h3, .display { font-family: var(--display); color: var(--white); font-weight: 400; }

.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }

.skip {
    position: absolute; left: -9999px;
    background: var(--accent); color: #000; padding: 10px 16px; z-index: 100;
}
.skip:focus { left: 12px; top: 12px; }

:where(a, button, summary, [tabindex]):focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
}

/* Sólo para lectores de pantalla. `clip-path` y no `display:none`, que sacaría
   el texto del árbol de accesibilidad, ni `text-indent:-9999px`, que en árabe
   o hebreo empuja al lado contrario y deja el texto visible. */
.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* --- WebGL field --------------------------------------------------
   TWO fixed canvases, drawn per-section by assets/js/field.js: a back layer
   behind all content for the section backgrounds, and a front layer above it
   for cabinet screens. See field.js for why one layer could not work.

   The canvas clears to exactly --bg, so the regions between scene sections are
   indistinguishable from the page background and the whole thing reads as one
   surface rather than as four embedded videos. */
.arcade-field-layer {
    position: fixed; inset: 0; z-index: 0;
    width: 100%; height: 100%;
    pointer-events: none;
    display: block;
}
/* The front layer composites OVER page furniture — it is what makes the
   cabinet screen a real hole rather than a transparent div in front of an
   opaque cabinet. Parented to <main>, so overlays that belong on the glass
   can out-stack it inside the same context. */
.field-front { z-index: 2; }

/* Positioned elements paint above non-positioned ones regardless of DOM
   order, so ALL page content — not just the scene sections — has to be
   lifted above the canvas. The canvas clears to an opaque colour, so
   anything left static is painted over and simply disappears.

   Scoping this to [data-scene] only is a trap worth naming: the scenes then
   look perfect while every ordinary section between them goes black, and on
   a game page that means the game itself vanishes. One rule on `main`
   covers the whole document; the per-section rule below is only for the
   .wrap inside a scene, which needs to clear that scene's own backdrop. */
main, .site-foot { position: relative; z-index: 1; }
[data-scene] { position: relative; }
[data-scene] > .wrap { position: relative; z-index: 1; }

/* The CSS gradients are the no-WebGL fallback and stay until something is
   actually drawing. `.has-field` is set only after a context and every
   program exist — see field.js. */
.has-field .hero::before { opacity: 0; }
.hero::before { transition: opacity .4s ease; }

/* With the field live, ::after stops being the dither texture and becomes the
   readability scrim. A shader tuned to look good and a shader tuned to sit
   under 60px type are not the same shader, and the type wins — so the copy
   side gets an explicit floor here instead of depending on shader tuning that
   any later art pass could undo. */
.has-field .hero::after {
    background-image: linear-gradient(96deg,
        #0B0E13f2 0%, #0B0E13e0 26%, #0B0E1399 48%, #0B0E1326 70%, transparent 86%);
    opacity: 1;
}
@media (max-width: 900px) {
    /* Narrow layouts put the copy over the middle of the field, so the scrim
       becomes a flat veil rather than a directional one. */
    .has-field .hero::after {
        background-image: linear-gradient(180deg, #0B0E13d9 0%, #0B0E13b3 55%, #0B0E13f0 100%);
    }
}

/* The lime CTA band is opaque by design, so when it carries a scene the flat
   fill has to go or the shader is invisible underneath it. Text flips to lime
   on dark to keep contrast on the tunnel. */
.has-field .cta-band[data-scene] { background: transparent; }
.has-field .cta-band[data-scene] h2 { color: var(--white); }
.has-field .cta-band[data-scene] p { color: var(--text); }
.has-field .cta-band[data-scene] .btn-primary { background: var(--accent); color: #07090c; }
.has-field .cta-band[data-scene] .btn-ghost { border-color: #ffffff3d; color: var(--white); }
.has-field .cta-band[data-scene] .btn-ghost:hover { border-color: var(--accent); color: var(--accent); background: transparent; }

@media (prefers-reduced-motion: reduce) {
    /* field.js already starts on the static rung here, which freezes uTime
       rather than hiding the scene — a still image is the point, not a blank. */
    .arcade-field-layer { opacity: .85; }
}

/* --- Newsletter band ---------------------------------------------- */

.newsletter { padding: clamp(56px, 9vw, 110px) 0; text-align: center; }
.newsletter h2 { font-size: clamp(26px, 5vw, 46px); margin: 0 0 14px; letter-spacing: -0.01em; }
.newsletter p { margin: 0 auto 28px; max-width: 46ch; }
.newsletter form { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.newsletter input[type="email"] {
    flex: 1 1 280px; max-width: 360px;
    padding: 13px 16px; border-radius: 8px;
    background: #0b0e13cc; border: 1px solid var(--border);
    color: var(--white); font-family: var(--sans); font-size: 15px;
}
.newsletter input[type="email"]::placeholder { color: var(--text-faint); }
.newsletter input[type="email"]:focus { outline: 2px solid var(--accent); outline-offset: 2px; border-color: transparent; }
.newsletter small { display: block; margin-top: 14px; color: var(--text-faint); font-size: 13px; }

/* --- Header ------------------------------------------------------- */

.site-head {
    position: sticky; top: 0; z-index: 20;
    background: #0B0E13ee;
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--border);
}
.site-head .wrap {
    display: flex; align-items: center; gap: 18px;
    min-height: 64px;
}

/* Type-set wordmark, same construction as the cursos header: the vendor's
   own logo file is the vendor's mark, not ours. */
.wordmark {
    display: inline-flex; align-items: baseline; gap: 9px;
    font-family: var(--display); font-size: 21px; line-height: 1;
    color: var(--white); text-decoration: none; letter-spacing: -0.01em;
    white-space: nowrap;
}
.wordmark .mark { color: var(--accent); font-size: 0.82em; }
.wordmark:hover .text { color: var(--accent); }

.site-head nav { margin-left: auto; display: flex; align-items: center; gap: clamp(10px, 2.4vw, 24px); }
/* `:not(.btn)` is load-bearing, not tidiness. `.site-head nav a` scores 0,2,1
   against `.btn-primary`'s 0,1,0, so without it the header CTA loses its own
   text colour and renders #ABABAB on lime — about 2:1 contrast, on the one
   control the whole page exists to get tapped. */
.site-head nav a:not(.btn) { color: var(--text); text-decoration: none; font-size: 15px; }
.site-head nav a:not(.btn):hover { color: var(--white); }

.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    padding: 11px 20px; border-radius: 8px; border: 1px solid transparent;
    font-family: var(--sans); font-weight: 600; font-size: 15px;
    text-decoration: none; cursor: pointer; white-space: nowrap;
}
.btn-primary { background: var(--accent); color: #07090c; }
.btn-primary:hover { filter: brightness(1.1); }
.btn-ghost { border-color: var(--border); color: var(--white); background: transparent; }
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
.btn-lg { padding: 15px 28px; font-size: 16px; }

/* --- Hero --------------------------------------------------------- */

/* The hero wash — the cursos homepage's indigo-to-crimson band, rebuilt from
   gradients instead of the template's raster art. It sits on a pseudo-element
   rather than on `body` so it spans full-bleed while the content stays in the
   grid, and so it ends at a defined edge instead of bleeding down a long page.
   `z-index: -1` keeps it under the text without making .hero a stacking
   context that the sticky header would then have to fight. */
.hero { position: relative; padding: clamp(48px, 9vw, 104px) 0 clamp(32px, 6vw, 64px); }
.hero::before {
    content: ""; position: absolute; inset: -64px 0 0; z-index: -1;
    background:
        radial-gradient(1100px 700px at 8% 0%, #3a1d8f 0%, #3a1d8f00 58%),
        radial-gradient(900px 600px at 78% 8%, #8c1440 0%, #8c144000 60%),
        linear-gradient(180deg, #11132a 0%, var(--bg) 88%);
}
/* Wide dark gradients band into visible stripes at 8-bit. A faint ordered
   pattern breaks the bands up for almost nothing — same reason the portfolio's
   field shader dithers. It has to paint AFTER the gradient, or the thing it
   smooths just gets re-quantised by whatever lands on top. */
.hero::after {
    content: ""; position: absolute; inset: -64px 0 0; z-index: -1;
    pointer-events: none; opacity: .035;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Cpath d='M0 0h1v1H0zM2 2h1v1H2z' fill='%23fff'/%3E%3C/svg%3E");
}
.hero .eyebrow {
    font-family: var(--sans); font-weight: 600; letter-spacing: 0.18em;
    text-transform: uppercase; font-size: 13px; color: var(--accent);
    margin: 0 0 18px;
}
.hero h1 {
    font-size: clamp(38px, 8.5vw, 82px); line-height: 0.98;
    letter-spacing: -0.02em; margin: 0 0 22px; text-wrap: balance;
}
.hero p.lede {
    font-size: clamp(16px, 2.1vw, 19px); max-width: 46ch; margin: 0 0 30px;
    color: var(--text);
}
.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; }

/* --- Shelves ------------------------------------------------------ */

.shelf { padding: clamp(36px, 6vw, 68px) 0; }
.shelf-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 12px 18px; margin-bottom: 8px; }
.shelf-head h2 { font-size: clamp(24px, 4.4vw, 40px); letter-spacing: -0.01em; margin: 0; }
.shelf-head .count { color: var(--text-faint); font-size: 14px; font-variant-numeric: tabular-nums; }
.shelf > .wrap > p.shelf-note { margin: 0 0 26px; color: var(--text-dim); max-width: 62ch; }

.grid {
    display: grid; gap: clamp(14px, 2.2vw, 22px);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
    list-style: none; margin: 0; padding: 0;
}
/* The student shelf gets larger tiles than the teacher's. That asymmetry is
   the recruiting argument made in layout: a visitor should register student
   work as the main event, not as an appendix to Raul's portfolio. */
.grid.featured { grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr)); }

/* --- Card --------------------------------------------------------- */

.card {
    position: relative; display: flex; flex-direction: column;
    background: var(--bg-raised); border: 1px solid var(--border);
    border-radius: var(--radius); overflow: hidden;
    text-decoration: none; color: inherit;
    transition: border-color .18s ease, transform .18s ease;
}
.card:hover { border-color: var(--accent); transform: translateY(-3px); }
.card:hover .card-title { color: var(--accent); }

.card-shot { aspect-ratio: 16 / 10; background: #0e1117; overflow: hidden; }
.card-shot img { width: 100%; height: 100%; object-fit: cover; }

/* Stands in until a real capture exists. See build.mjs — deliberately typographic
   so it can never be mistaken for a screenshot of the game. */
.shot-fallback {
    width: 100%; height: 100%; display: grid; place-items: center; padding: 18px;
    background: linear-gradient(150deg, #1a1f2e 0%, #0e1117 70%);
    border-bottom: 1px solid var(--border);
}
.shot-fallback span {
    font-family: var(--display); font-size: clamp(15px, 3.4vw, 20px);
    color: #ffffff2e; text-align: center; line-height: 1.3;
}

.card-body { padding: 15px 16px 17px; display: flex; flex-direction: column; gap: 7px; flex: 1; }
.card-title { font-family: var(--display); color: var(--white); font-size: 1.02rem; line-height: 1.35; transition: color .18s ease; }
.card-by { font-size: 13.5px; color: var(--text-dim); }
.card-by b { color: var(--text); font-weight: 600; }
.card-blurb { font-size: 14px; color: var(--text-dim); margin: 0; }
.card-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: auto; padding-top: 10px; }

.badge {
    font-size: 11.5px; font-weight: 600; letter-spacing: 0.04em;
    padding: 4px 9px; border-radius: 999px;
    border: 1px solid var(--border); color: var(--text-dim);
    white-space: nowrap;
}
.badge-accent { border-color: var(--accent-dim); color: var(--accent); }
/* The honesty badge. A game that silently does nothing when tapped is the
   single failure that discredits the whole arcade, so desktop-only titles
   say so on the card, before the tap. */
.badge-desktop { border-color: #6b5a1f; color: #ffc94d; }

/* --- Pending tile -------------------------------------------------
   An empty shelf is more persuasive than a fake screenshot, and the
   portfolio holds itself to the same rule: no invented imagery. This tile
   is visibly a reservation, dated, so it reads as anticipation instead of
   as a broken image. */
.card.is-pending { border-style: dashed; }
.card.is-pending:hover { transform: none; border-color: var(--border); }
.card.is-pending .card-shot {
    display: grid; place-items: center;
    background:
        repeating-linear-gradient(45deg, #ffffff05 0 10px, transparent 10px 20px),
        #0e1117;
}
.card.is-pending .pending-mark {
    font-family: var(--display); font-size: 13px; letter-spacing: 0.14em;
    text-transform: uppercase; color: var(--text-faint); text-align: center;
    padding: 0 18px; line-height: 1.7;
}

/* ==================================================================
   EL RACK — el selector de la portada.
   ==================================================================
   Sustituye a "la máquina": un solo mueble gigante que enseñaba dos juegos
   y cuya pantalla era un agujero al lienzo WebGL de atrás. Ese diseño tenía
   dos fallas que no eran de gusto:

   1. La pantalla salía NEGRA. El agujero dependía de que el shader estuviera
      pintando justo detrás, y en cuanto el canvas frontal no arrancaba —o el
      rect se movía— el visitante recibía un rectángulo negro del alto de la
      ventana como primer elemento del arcade.
   2. No se podía ojear. El carrete llegó a `vitrina` de dos juegos porque con
      diecinueve era una lista que nadie recorría; la solución fue enseñar dos
      y esconder quince detrás de un enlace de texto.

   El rack resuelve las dos a la vez. Todos los gabinetes están en pantalla,
   en perspectiva: el del centro grande y de frente, los demás abanicados
   hacia atrás. Se recorre arrastrando, con las flechas o tocando cualquier
   mueble. Y el CRT es CSS —scanlines, viñeta y reflejo con degradados— no
   WebGL: no hay agujero que alinear, así que no hay pantalla negra posible.

   ---- Las tres reglas de rendimiento -------------------------------
   El público llega por un enlace de WhatsApp, en un Android de gama media.
   - Sólo `transform` y `opacity` se animan. Nada que provoque layout.
   - Sólo los gabinetes visibles (`--vis` a cada lado) están en el árbol de
     composición; el resto lleva `display:none`. Con veinte juegos eso es la
     diferencia entre nueve capas y veinte.
   - `will-change` lo pone y lo quita el JS, nunca el CSS: dejarlo puesto en
     veinte elementos reserva memoria de GPU para gabinetes que no se ven.

   ---- La regla que no es de rendimiento ----------------------------
   EL IFRAME DEL JUEGO NUNCA VA DENTRO DE UN ELEMENTO CON TRANSFORM 3D.
   Un iframe dentro de un `rotateY`/`translateZ` se rasteriza a la resolución
   del padre en varios WebView de Android y el hit-testing del táctil se
   desalinea. Por eso `.rack-play` es una capa aparte, hermana de la fila y
   sin transformar: el juego siempre corre en un rectángulo plano y honesto.
   ------------------------------------------------------------------ */

/* La cabecera es pegajosa, así que un salto a #alumnos dejaría el marquee
   debajo de ella. */
.arcade { scroll-margin-top: 84px; padding: clamp(36px, 6vw, 68px) 0 clamp(30px, 5vw, 56px); }
.picker-head { margin-bottom: clamp(18px, 2.6vw, 30px); }
.picker-head .shelf-head { margin-bottom: 8px; }
.picker-head .shelf-note { margin: 0; color: var(--text-dim); max-width: 62ch; }

.rack {
    --cab-w: clamp(214px, 27vw, 356px);
    /* Con la pantalla fijada a 4:3, este factor es lo único que decide cuánta
       peana queda debajo. A 1.30 sobraban 110px y la peana dejaba de ser una
       peana para ser un panel vacío que competía con la pantalla. */
    --cab-h: calc(var(--cab-w) * 1.16);
    /* Cuántos gabinetes se dibujan a cada lado del centro. El JS lee este
       número; cambiarlo aquí cambia el abanico y el coste, en un solo sitio. */
    --vis: 4;
    position: relative;
    width: 100%;
    user-select: none;
    -webkit-user-select: none;
}

/* --- El marquee del rack -------------------------------------------
   El rótulo del arcade entero, no el de un mueble: dice qué hay en el centro
   y es lo único que cambia de texto al recorrer. Va aquí arriba y no en cada
   gabinete porque a 46° de giro un título no se lee, y repetirlo veinte veces
   en pantalla no es un rótulo, es ruido. */
.rack-marquee {
    position: relative;
    width: min(720px, calc(100% - 2 * var(--gutter)));
    margin: 0 auto clamp(10px, 1.8vw, 20px);
    padding: clamp(8px, 1.4vw, 14px) clamp(18px, 3vw, 34px);
    border-radius: 8px;
    background: linear-gradient(180deg, #eaffa0 0%, var(--accent) 42%, #a8d611 100%);
    box-shadow: 0 0 34px #cbfe1c3d, inset 0 -2px 0 #00000026;
    overflow: hidden;
    text-align: center;
}
.rack-marquee-title {
    position: relative; z-index: 2;
    display: block;
    font-family: var(--display);
    color: #10140a;
    font-size: clamp(17px, 3.4vw, 34px);
    line-height: 1.1;
    letter-spacing: -0.01em;
    text-transform: uppercase;
    /* Un título largo no puede ensanchar el rótulo ni saltar a dos renglones:
       el rack entero se movería cada vez que cambias de juego. */
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* El barrido de luz. Es el único bucle infinito de la sección. */
.rack-marquee-glow {
    position: absolute; inset: 0; z-index: 1;
    background: linear-gradient(100deg, transparent 30%, #ffffffb3 50%, transparent 70%);
    transform: translateX(-100%);
    animation: rack-sweep 4.6s ease-in-out infinite;
}
@keyframes rack-sweep {
    0%, 62% { transform: translateX(-100%); }
    100%    { transform: translateX(100%); }
}
/* Un lugar apartado no se anuncia con luces. */
.rack.is-pending .rack-marquee {
    background: linear-gradient(180deg, #2a2e36 0%, #1b1e24 100%);
    box-shadow: none;
}
.rack.is-pending .rack-marquee-title { color: #6f7787; }
.rack.is-pending .rack-marquee-glow { display: none; }

/* --- El escenario --------------------------------------------------
   Alto fijo y perspectiva. El alto NO puede depender del contenido: los
   gabinetes están posicionados absolutos justamente para que recorrer el
   rack no mueva ni un píxel de lo que hay debajo. */
.rack-stage {
    position: relative;
    height: calc(var(--cab-h) + 30px);
    /* El abanico se sale del ancho útil a propósito — así se lee como una
       fila que sigue más allá del marco — pero nunca puede provocar scroll
       horizontal en un teléfono. */
    overflow: hidden;
    transition: height .34s cubic-bezier(.22,.61,.36,1);
}

.rack-row {
    position: absolute; inset: 0;
    margin: 0; padding: 0; list-style: none;
    perspective: 1500px;
    perspective-origin: 50% 46%;
    transform-style: preserve-3d;
    cursor: grab;
    /* Vertical sí, horizontal lo tomamos nosotros: así el pulgar puede
       arrastrar el rack sin que la página se vaya de lado, y aun así puede
       seguir bajando con el mismo dedo. */
    touch-action: pan-y;
}
.rack-row.is-dragging { cursor: grabbing; }

.cab {
    position: absolute;
    left: 50%; top: 50%;
    width: var(--cab-w); height: var(--cab-h);
    margin-left: calc(var(--cab-w) / -2);
    margin-top: calc(var(--cab-h) / -2);
    transform-style: preserve-3d;
    /* El JS escribe transform, opacity y zIndex cada cuadro. Sin transición:
       la interpolación la hace el bucle, y una transición CSS encima pelearía
       con el arrastre. */
}
.cab.is-far { display: none; }

/* --- El mueble ------------------------------------------------------ */

.cab-btn {
    display: flex; flex-direction: column;
    width: 100%; height: 100%;
    padding: 0; border: 0; margin: 0;
    background: transparent;
    font-family: var(--sans);
    color: inherit;
    cursor: pointer;
    text-align: center;
}
.cab-btn:focus-visible { outline: none; }
.cab-btn:focus-visible .cab-body { outline: 2px solid var(--accent); outline-offset: 4px; }

.cab-body {
    display: flex; flex-direction: column;
    width: 100%; height: 100%;
    padding: clamp(6px, 1vw, 10px);
    gap: clamp(5px, .8vw, 9px);
    border-radius: clamp(10px, 1.6vw, 18px) clamp(10px, 1.6vw, 18px) 6px 6px;
    background:
        linear-gradient(180deg, #262b34 0%, #171b22 26%, #12151b 68%, #0a0c11 100%);
    box-shadow:
        inset 0 1px 0 #ffffff1a,
        inset 0 0 0 1px #ffffff12,
        0 22px 44px -18px #000000cc;
}

/* El rótulo del propio mueble. Apagado salvo en el del centro — así el ojo
   sabe dónde está sin leer nada. */
.cab-top {
    display: grid; place-items: center;
    height: clamp(20px, 3.2vw, 32px);
    border-radius: 5px;
    background: #1e222a;
    box-shadow: inset 0 0 0 1px #ffffff14;
    overflow: hidden;
}
.cab-name {
    font-family: var(--display);
    font-size: clamp(8px, 1.15vw, 12px);
    letter-spacing: .04em;
    text-transform: uppercase;
    color: #59616f;
    padding-inline: 8px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    max-width: 100%;
}
.cab.is-on .cab-top {
    background: linear-gradient(180deg, #eaffa0, var(--accent) 55%, #a8d611);
    box-shadow: 0 0 18px #cbfe1c4d;
}
.cab.is-on .cab-name { color: #10140a; }

/* --- La pantalla ----------------------------------------------------
   CSS puro. La captura va de fondo, las scanlines y la viñeta encima con
   degradados repetidos, y el reflejo de cristal es un tercer degradado.
   Nada de esto depende de que arranque un contexto WebGL, que es exactamente
   la dependencia que dejaba el arcade abriendo en negro. */
.cab-screen {
    position: relative;
    /* La pantalla es 4:3 —la proporción del tubo que estos muebles tenían de
       verdad— y NO se estira con el mueble. Cuando era `flex:1` se comía todo
       el alto sobrante y salía vertical, así que `cover` recortaba una captura
       apaisada por los dos lados y de cada juego se veía una tira central. El
       alto que sobra se lo queda la peana, que es relleno y no información. */
    flex: 0 0 auto;
    aspect-ratio: 4 / 3;
    width: 100%;
    min-height: 0;
    border-radius: 7px;
    overflow: hidden;
    background: #05070a;
    box-shadow: inset 0 0 0 1px #000, inset 0 0 22px #000000b3;
    display: grid;
}
.cab-shot {
    grid-area: 1 / 1;
    width: 100%; height: 100%;
    object-fit: cover;
    /* Los muebles del abanico están detrás: se ven apagados, como una pantalla
       que no te está mirando. El del centro recupera el color. */
    /* Apagados, no invisibles. Con brightness(.62) más la rampa de opacidad,
       las capturas —que ya son de juegos oscuros— salían negras y el abanico
       parecía una fila de losas grises. De un mueble ladeado tiene que leerse
       QUÉ juego es; si no, no se puede elegir, que es para lo que está. */
    filter: saturate(.72) brightness(.88);
    transition: filter .3s ease;
}
.cab.is-on .cab-shot { filter: none; }

/* Sin captura todavía. Tipografía, nunca una imagen inventada — es la misma
   regla que ya sigue la ficha: cada imagen del sitio es un cuadro del juego
   que tiene al lado, así que un juego sin captura dice su nombre y ya. */
.cab-off {
    grid-area: 1 / 1;
    display: grid; place-items: center;
    padding: 10px;
    font-family: var(--display);
    font-size: clamp(11px, 1.6vw, 17px);
    line-height: 1.2;
    color: #ffffff30;
    background: repeating-linear-gradient(135deg, #0d1117 0 8px, #0a0e14 8px 16px);
}
.cab.is-on .cab-off { color: #ffffff5c; }

/* Scanlines + viñeta + reflejo, en una sola capa. */
.cab-glass {
    grid-area: 1 / 1;
    position: relative; z-index: 3;
    pointer-events: none;
    background:
        linear-gradient(196deg, #ffffff14 0 34%, transparent 62%),
        repeating-linear-gradient(180deg, #0000002e 0 1px, transparent 1px 3px),
        radial-gradient(120% 92% at 50% 50%, transparent 56%, #000000a6 100%);
}

/* INSERT COIN. Sólo en el gabinete del centro y sólo si el juego se puede
   correr aquí dentro. */
.cab-coin {
    grid-area: 1 / 1;
    z-index: 4;
    align-self: end;
    padding: 0 0 clamp(8px, 1.4vw, 14px);
    font-family: var(--display);
    font-size: clamp(9px, 1.35vw, 14px);
    letter-spacing: .18em;
    color: var(--accent);
    text-shadow: 0 0 14px #cbfe1c99;
    opacity: 0;
    transition: opacity .25s ease;
    animation: rack-blink 1.5s steps(1) infinite;
}
.cab.is-on.can-play .cab-coin { opacity: 1; }
@keyframes rack-blink { 0%, 62% { visibility: visible; } 63%, 100% { visibility: hidden; } }

/* --- El tablero de control ------------------------------------------ */
.cab-panel {
    display: flex; align-items: center; justify-content: center;
    gap: clamp(7px, 1.1vw, 13px);
    height: clamp(18px, 2.6vw, 28px);
    border-radius: 5px;
    background: linear-gradient(180deg, #20242c, #14171d);
    box-shadow: inset 0 1px 0 #ffffff17;
}
.cab-stick {
    width: clamp(7px, 1vw, 11px); height: clamp(7px, 1vw, 11px);
    border-radius: 50%;
    background: radial-gradient(circle at 34% 28%, #ff8c8c, #b01d1d);
    box-shadow: 0 0 6px #b01d1d80;
}
.cab-btns { display: flex; gap: clamp(3px, .5vw, 6px); }
.cab-btns i {
    width: clamp(5px, .72vw, 8px); height: clamp(5px, .72vw, 8px);
    border-radius: 50%;
    display: block;
}
.cab-btns i:nth-child(1) { background: radial-gradient(circle at 34% 28%, #ffe884, #c9960c); }
.cab-btns i:nth-child(2) { background: radial-gradient(circle at 34% 28%, #a6e6ff, #1b7ba8); }
.cab-btns i:nth-child(3) { background: radial-gradient(circle at 34% 28%, #d9aeff, #6f2fa8); }
.cab-btns i:nth-child(4) { background: radial-gradient(circle at 34% 28%, #b6ff9c, #3f9c22); }

/* La peana: el cuerpo del mueble bajo el tablero. Puro relleno de silueta,
   pero es lo que hace que veinte de éstos en fila se lean como una sala de
   máquinas y no como veinte televisores flotando. El degradado de los lados
   imita el arte de costado, que es justo lo que se ve de un gabinete girado. */
.cab-base {
    flex: 1 1 auto;
    min-height: 0;
    border-radius: 4px 4px 6px 6px;
    background:
        linear-gradient(90deg, #00000059 0 6%, transparent 22% 78%, #00000059 94% 100%),
        linear-gradient(180deg, #191d24, #0c0e13);
    box-shadow: inset 0 1px 0 #ffffff0f;
    /* El sombreado de los cantos SÍ va aquí y en negro, que es lo que hace un
       canto: se ve en los muebles ladeados, que son los que enseñan costado. */
}
/* En el mueble encendido, la peana lleva UNA línea de luz arriba y nada más:
   es luz que se escapa por debajo del tablero de control.

   Antes llevaba arte de costado en lima a los lados, y estaba mal por dos
   motivos. Uno de dibujo: a un 15% de opacidad sobre casi negro, dos bandas de
   40px se leen como una mancha aceitosa, no como un mueble. Y uno de lógica:
   el costado de un gabinete sólo se ve cuando está GIRADO, o sea en los del
   abanico — ponérselo justo al que está de frente era pintarlo al revés. */
.cab.is-on .cab-base {
    background:
        linear-gradient(180deg, #cbfe1c2b 0 1px, #1a1f26 1px, #0a0d11 100%);
}
.cab.is-pending .cab-base { background: linear-gradient(180deg, #14171c, #0a0c10); }

/* El halo del gabinete encendido. Va en el mueble y no en la pantalla para
   que se lea como una máquina prendida en una sala oscura. */
.cab.is-on .cab-body {
    box-shadow:
        inset 0 1px 0 #ffffff24,
        inset 0 0 0 1px #cbfe1c33,
        0 0 0 1px #cbfe1c40,
        0 26px 60px -20px #000000e6,
        0 0 70px -10px #cbfe1c2e;
}

/* Un lugar apartado: mueble sin luz y pantalla sin señal. */
.cab.is-pending .cab-body { background: linear-gradient(180deg, #1b1e24, #0d0f14); }
.cab.is-pending .cab-stick { background: #3a3f49; box-shadow: none; }
.cab.is-pending .cab-btns i { background: #2c313a; }

/* --- Flechas --------------------------------------------------------
   Encima del abanico, no al lado: si tomaran ancho de layout, el gabinete
   del centro se encogería en el teléfono justo donde más falta hace grande. */
.rack-nav {
    position: absolute; top: 50%; z-index: 6;
    transform: translateY(-50%);
    width: clamp(42px, 5vw, 54px); height: clamp(42px, 5vw, 54px);
    border-radius: 50%;
    border: 1px solid var(--border);
    /* Fondo sólido y NO backdrop-filter. Desenfocar el fondo de un botón que
       flota sobre una escena en `preserve-3d` obliga al navegador a sacar todo
       el subárbol de atrás a su propia superficie y a volver a desenfocarla en
       cada cuadro: en un Android de gama media eso solo se come el presupuesto
       entero de la sección. Un 92% de opacidad se lee igual y cuesta cero. */
    background: #0b0e13ec;
    cursor: pointer;
    display: grid; place-items: center;
    transition: border-color .2s ease, background .2s ease, opacity .25s ease;
}
/* Pegadas al borde del escenario quedaban a media pantalla del abanico y
   nadie las relacionaba con él. Entran hasta justo fuera del primer vecino. */
.rack-nav.prev { left: clamp(var(--gutter), 7vw, 132px); }
.rack-nav.next { right: clamp(var(--gutter), 7vw, 132px); }
.rack-nav::before {
    content: ""; width: 11px; height: 11px;
    border-left: 2px solid var(--text); border-bottom: 2px solid var(--text);
}
.rack-nav.prev::before { transform: rotate(45deg) translate(2px, -2px); }
.rack-nav.next::before { transform: rotate(-135deg) translate(2px, -2px); }
.rack-nav:hover { border-color: var(--accent); background: #0b0e13ee; }
.rack-nav:hover::before { border-color: var(--accent); }
.rack-nav:active { transform: translateY(-50%) scale(.94); }
.rack-nav[disabled] { opacity: .22; pointer-events: none; }

/* --- Los puntos ------------------------------------------------------
   El mapa del rack: cuántos juegos hay y en cuál vas. El punto mide 8px pero
   el botón mide 22 — el área táctil es el padding, no el punto. */
.rack-dots {
    display: flex; justify-content: center; align-items: center;
    flex-wrap: wrap; gap: 0;
    list-style: none; margin: clamp(10px, 1.6vw, 16px) 0 0; padding: 0;
}
.rack-dot {
    display: block; padding: 7px 5px;
    border: 0; background: transparent; cursor: pointer;
    line-height: 0;
}
.rack-dot::before {
    content: ""; display: block;
    width: 8px; height: 8px; border-radius: 50%;
    background: #ffffff26;
    transition: background .2s ease, transform .2s ease;
}
.rack-dot:hover::before { background: #ffffff59; }
.rack-dot.is-on::before { background: var(--accent); transform: scale(1.35); box-shadow: 0 0 10px #cbfe1c8c; }
.rack-dot.is-pending::before { background: #ffffff14; box-shadow: inset 0 0 0 1px #ffffff2b; }
.rack-dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 4px; }

/* --- La ficha del juego en curso ------------------------------------- */
.rack-info {
    width: min(760px, calc(100% - 2 * var(--gutter)));
    margin: clamp(12px, 2vw, 20px) auto 0;
    min-height: 176px;
    display: flex; flex-direction: column; gap: 6px;
    text-align: center; align-items: center;
}
.rack-info .np-title {
    font-family: var(--display); color: var(--white);
    font-size: clamp(17px, 2.4vw, 24px); line-height: 1.25;
}
.rack-info .np-by { font-size: 13.5px; color: var(--text-dim); }
.rack-info .np-by b { color: var(--text); font-weight: 600; }
.rack-info .np-blurb { margin: 0; font-size: 14.5px; color: var(--text-dim); max-width: 58ch; }
.rack-info .np-badges { padding-top: 4px; display: flex; justify-content: center; flex-wrap: wrap; gap: 6px; }
.rack-info .np-play { margin-top: 10px; }
.rack-info .np-play[aria-disabled="true"] { opacity: .35; pointer-events: none; }


/* --- Jugando: la toma de pantalla completa ---------------------------
   Al meter la ficha, el juego se lleva la pantalla entera. Sólo se reserva
   sitio ARRIBA, para el rótulo con el nombre y las flechas de cambiar de
   juego; de ahí para abajo todo es el juego, a ancho completo y hasta el
   borde inferior de la ventana.

   Antes esta capa vivía dentro del escenario, y por tanto heredaba el ancho
   del contenido y el alto del mueble: un juego pensado para una pantalla
   entera se veía por una rendija, con franjas negras a los lados y su propia
   barra de scroll dentro. Eso no es un arcade, es una miniatura interactiva.

   `position: fixed` y no `absolute`: el candado de scroll pone el body en
   `position:fixed` desplazado por --y, así que cualquier `absolute` seguiría
   al documento y no a la ventana. Fijo es lo único que de verdad cubre la
   pantalla, esté donde esté el scroll.

   ⚠️ Y la capa NO puede colgar de ningún ancestro con `transform`, `filter`
   ni `perspective`: eso convierte al ancestro en bloque contenedor y un
   `fixed` deja de serlo. Por eso está fuera de `.rack-stage` —que además
   recorta con overflow— y fuera de `.rack-row`, que lleva perspective. */
.rack-play {
    position: fixed; inset: 0; z-index: 60;
    display: flex; flex-direction: column;
    background: #05070a;
    /* La cabecera del sitio es sticky con z-index 20. Pantalla completa
       quiere decir completa: también por encima de ella. */
}
.rack-play[hidden] { display: none; }

/* --- El rótulo de arriba --- */
.play-bar {
    flex: 0 0 auto;
    display: flex; align-items: center; gap: clamp(6px, 1.4vw, 16px);
    padding: clamp(6px, 1vw, 10px) clamp(8px, 1.6vw, 18px);
    background: linear-gradient(180deg, #eaffa0 0%, var(--accent) 46%, #a8d611 100%);
    box-shadow: 0 2px 24px #cbfe1c3d;
}
.play-title {
    flex: 1 1 auto; min-width: 0;
    text-align: center;
    font-family: var(--display);
    color: #10140a;
    font-size: clamp(13px, 2.6vw, 26px);
    line-height: 1.15;
    letter-spacing: -0.01em;
    text-transform: uppercase;
    /* Un título largo no puede empujar a las flechas ni saltar de renglón: la
       barra mide lo que mide y el juego se queda con el resto. */
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Las flechas, ahora sobre lima y por tanto en negro. Cambian de juego SIN
   salir: se recarga el marco y el rótulo, y el rack de detrás se mueve solo
   para que al salir aterrices en el gabinete que estabas jugando. */
.play-nav {
    flex: 0 0 auto;
    width: clamp(34px, 4.4vw, 44px); height: clamp(34px, 4.4vw, 44px);
    border-radius: 50%;
    border: 1px solid #10140a2e;
    background: #10140a14;
    cursor: pointer;
    display: grid; place-items: center;
    transition: background .18s ease, opacity .18s ease;
}
.play-nav::before {
    content: ""; width: 10px; height: 10px;
    border-left: 2px solid #10140a; border-bottom: 2px solid #10140a;
}
.play-nav.prev::before { transform: rotate(45deg) translate(2px, -2px); }
.play-nav.next::before { transform: rotate(-135deg) translate(2px, -2px); }
.play-nav:hover { background: #10140a2b; }
.play-nav:active { transform: scale(.94); }
.play-nav[disabled] { opacity: .28; pointer-events: none; }
.play-nav:focus-visible { outline: 2px solid #10140a; outline-offset: 2px; }

.rack-exit {
    flex: 0 0 auto;
    padding: 8px 15px; border-radius: 999px;
    border: 1px solid #10140a3d;
    background: #10140a;
    color: #eaffa0; font-family: var(--sans); font-size: 13px; font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
}
.rack-exit:hover { background: #000; color: var(--accent); }
.rack-exit:focus-visible { outline: 2px solid #10140a; outline-offset: 2px; }

/* --- El juego --- */
.play-screen {
    flex: 1 1 auto;
    min-height: 0;          /* sin esto el flex no deja encoger y el marco desborda */
    position: relative;
    background: #05070a;
}
.rack-frame {
    position: absolute; inset: 0;
    width: 100%; height: 100%; border: 0; display: block;
    background: #05070a;
}

/* NO hay CRT sobre el juego a pantalla completa, y es a propósito.
   Cuando el juego cabía en el hueco de un mueble, unas scanlines flojas
   vendían el arcade sin estorbar. Ocupando la pantalla entera son un filtro
   permanente sobre lo único que la persona está intentando leer, y un efecto
   que te cuesta una vida es un efecto malo. La fantasía la vende el rack; la
   partida se juega limpia. */

@media (max-width: 620px) {
    /* En el teléfono el rótulo se lleva lo mínimo: cuanto más alto sea, menos
       juego queda. Salir pierde la palabra y se queda con la cruz. */
    .play-bar { gap: 6px; padding: 5px 8px; }
    .rack-exit { padding: 7px 11px; font-size: 0; }
    .rack-exit::after { content: "✕"; font-size: 15px; }
}

/* El rack de detrás queda tapado por la capa, así que no hace falta apagarlo;
   lo único que se para es el brillo del marquee, que seguiría animando una
   pieza invisible y gastando cuadros que ahora son del juego. */
.rack.is-playing .rack-marquee-glow { animation: none; opacity: 0; }


/* Mientras corre un juego la PÁGINA no se mueve.
   ---------------------------------------------------------------------
   Las flechas y WASD son los controles del juego, pero el navegador también
   los trata como órdenes de scroll. En cuanto el foco está en este documento
   y no dentro del iframe —basta un clic despistado— cada paso del jugador
   subía además el rack por la pantalla.

   ⚠ `overflow:hidden` en la raíz por sí solo NO basta, y solo es peor que el
   error: quitar el overflow desplazable devuelve la página al tope, así que
   darle a jugar mandaba el juego fuera de vista y el jugador se quedaba
   mirando el hero. Eso parecía exactamente una pantalla negra y se publicó
   una vez.

   Lo que funciona es sacar el body del flujo y subirlo justo lo que estaba
   desplazado. El JS pone `--y`; al salir se restaura el scroll. */
html.jugando { overflow: hidden; }
html.jugando body {
    position: fixed;
    top: var(--y, 0px);
    left: 0;
    right: 0;
    width: 100%;
    overflow: hidden;
    overscroll-behavior: none;
    touch-action: none;
}

/* --- Controles de abajo --- */

.picker-foot {
    display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
    margin-top: clamp(16px, 2.4vw, 26px);
}
/* La salida al catálogo. El rack ya enseña todo, así que esto ya no es la
   única puerta a los demás juegos — pero /juegos/ sigue siendo la rejilla
   que se recorre de un vistazo, y hay quien prefiere eso. */
.picker-todos {
    margin-left: auto; color: var(--accent); text-decoration: none;
    font-weight: 600; font-size: 14px; white-space: nowrap;
    padding: 8px 4px; border-bottom: 1px solid transparent;
}
.picker-todos:hover, .picker-todos:focus-visible { border-bottom-color: var(--accent); }
.picker-sound {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 8px 14px; border-radius: 999px;
    border: 1px solid var(--border); background: transparent;
    color: var(--text-dim); font-family: var(--sans); font-size: 13px;
    font-weight: 600; cursor: pointer;
}
.picker-sound:hover { color: var(--white); border-color: #ffffff33; }
.picker-sound[aria-pressed="true"] { color: var(--accent); border-color: var(--accent-dim); }
.picker-sound .snd-on { font-size: 15px; line-height: 1; }

.picker-hint { margin-left: auto; font-size: 13px; color: var(--text-faint); }
/* Las flechas se esconden por debajo de 620px, así que la pista no puede
   nombrarlas. */
.hint-wide { display: inline; }
.hint-narrow { display: none; }
@media (max-width: 620px) {
    .hint-wide { display: none; }
    .hint-narrow { display: inline; }
}

/* --- Teléfono --------------------------------------------------------
   El abanico se recorta a dos por lado y el gabinete del centro se lleva casi
   todo el ancho: en 360px, nueve muebles son nueve manchas de 40px. */
@media (max-width: 720px) {
    .rack { --cab-w: clamp(178px, 52vw, 260px); --vis: 2; }
    .rack-nav { width: 44px; height: 44px; }
    .rack-info { min-height: 200px; }
}
@media (max-width: 420px) {
    .rack { --vis: 2; }
    /* Las flechas se van: a este ancho tapan el mueble del centro y el gesto
       natural ya es deslizar. La pista de abajo cambia de texto con ellas. */
    .rack-nav { display: none; }
}

/* --- Sin movimiento --------------------------------------------------
   El JS lee esta misma consulta y salta directo al destino en vez de
   interpolar; aquí se apaga todo lo demás que late. */
@media (prefers-reduced-motion: reduce) {
    .rack-marquee-glow { animation: none; opacity: 0; }
    .cab-coin { animation: none; }
    .cab-shot { transition: none; }
    .rack-stage { transition: none; }
}

/* --- Game page ---------------------------------------------------- */

.game-head { padding: clamp(26px, 4.5vw, 44px) 0 20px; }
.game-head h1 { font-size: clamp(28px, 5.4vw, 50px); letter-spacing: -0.02em; margin: 0 0 10px; }
.game-head .by { color: var(--text-dim); margin: 0 0 14px; }
.game-head .by b { color: var(--white); font-weight: 600; }
.crumb { font-size: 14px; color: var(--text-faint); text-decoration: none; display: inline-block; margin-bottom: 16px; }
.crumb:hover { color: var(--accent); }

.stage {
    background: #05070a; border: 1px solid var(--border);
    border-radius: var(--radius); overflow: hidden;
    /* Most of these prototypes are authored against a landscape canvas and
       letterbox themselves; the frame keeps a stable box so the page does
       not jump when the game boots. */
    aspect-ratio: 16 / 9;
}
.stage iframe { width: 100%; height: 100%; border: 0; display: block; }

.stage-offsite, .stage-download {
    display: grid; place-items: center; text-align: center; padding: 30px;
    gap: 16px;
}
.stage-offsite p, .stage-download p { margin: 0; color: var(--text-dim); max-width: 44ch; }

.game-meta {
    display: grid; gap: 12px 34px; margin: 26px 0 0;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr));
    border-top: 1px solid var(--border); padding-top: 22px;
}
.game-meta div { display: flex; flex-direction: column; gap: 3px; }
.game-meta dt, .game-meta .k {
    font-size: 11.5px; letter-spacing: 0.12em; text-transform: uppercase;
    color: var(--text-faint);
}
.game-meta dd, .game-meta .v { margin: 0; color: var(--white); font-size: 15px; }

/* --- CTA band ----------------------------------------------------- */

.cta-band {
    background: var(--accent); color: #07090c;
    margin-top: clamp(40px, 7vw, 80px);
    padding: clamp(34px, 6vw, 62px) 0;
}
.cta-band h2 { color: #07090c; font-size: clamp(24px, 4.6vw, 42px); margin: 0 0 12px; letter-spacing: -0.01em; }
.cta-band p { margin: 0 0 24px; max-width: 52ch; color: #1a2208; font-weight: 500; }
.cta-band .btn-primary { background: #07090c; color: var(--accent); }
.cta-band .btn-ghost { border-color: #07090c66; color: #07090c; }
.cta-band .btn-ghost:hover { border-color: #07090c; color: #07090c; background: #0000000d; }

/* --- Testimonial ---------------------------------------------------
   Last thing before the footer: a face, after a page of screens. The photo
   is the only photograph on the site, so it is allowed to be large. */

.testimonial { padding: clamp(48px, 8vw, 96px) 0; }

/* The wireframe scene is tuned to sit under a game-page header, and at this
   section's size its lattice reads as loud as the photo. A scrim between the
   field and the content pushes it back to texture. It has to be a child of
   the section rather than an opacity change on the canvas, because the canvas
   is shared with the hero and the CTA, which both want it at full strength. */
.has-field .testimonial::after {
    content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
    background: linear-gradient(180deg, #0B0E13f0 0%, #0B0E13c9 42%, #0B0E13f7 100%);
}
.testimonial-grid {
    display: grid; gap: clamp(24px, 4vw, 56px);
    grid-template-columns: 1fr;
    align-items: center;
}
@media (min-width: 860px) {
    .testimonial-grid { grid-template-columns: minmax(0, 0.92fr) minmax(0, 1fr); }
}

.testimonial-photo { margin: 0; position: relative; }
.testimonial-photo img {
    width: 100%; height: auto; display: block;
    border-radius: 14px;
    border: 1px solid var(--border);
    box-shadow: 0 40px 70px -34px #000;
}
/* A lime edge on two sides only — enough to tie the photo to the brand
   without putting a frame around someone's face. */
.testimonial-photo::after {
    content: ""; position: absolute; inset: auto auto -10px -10px;
    width: 46%; height: 46%;
    border-left: 3px solid var(--accent); border-bottom: 3px solid var(--accent);
    border-radius: 0 0 0 14px;
    pointer-events: none; opacity: .8;
}

.testimonial-body { min-width: 0; }
.testimonial-body .eyebrow {
    font-weight: 600; letter-spacing: 0.18em; text-transform: uppercase;
    font-size: 13px; color: var(--accent); margin: 0 0 16px;
}

.testimonial-quote { margin: 0; }
.testimonial-quote p {
    font-family: var(--display);
    font-size: clamp(21px, 3.1vw, 36px);
    line-height: 1.28; letter-spacing: -0.01em;
    color: var(--white); margin: 0;
    text-wrap: balance;
}
/* Quotation marks are drawn, not typed, so the quote text in games.json
   stays clean and never ends up double-quoted. */
.testimonial-quote p::before { content: "\201C"; color: var(--accent); }
.testimonial-quote p::after { content: "\201D"; color: var(--accent); }

.testimonial-by {
    display: flex; flex-direction: column; gap: 2px;
    margin-top: clamp(18px, 2.4vw, 28px);
    font-size: 15px; color: var(--text-dim);
}
.testimonial-by b { color: var(--white); font-weight: 600; font-size: 16px; }

/* The no-quote state. Same layout, no quotation marks, no attribution —
   it is a caption, and it must not read as something someone said. */
.testimonial-caption {
    margin: 0;
    font-family: var(--display);
    font-size: clamp(19px, 2.6vw, 30px);
    line-height: 1.32; color: var(--white);
    text-wrap: balance;
}

/* --- Footer ------------------------------------------------------- */

.site-foot { border-top: 1px solid var(--border); padding: 34px 0 46px; margin-top: 60px; }
.site-foot .wrap { display: flex; flex-wrap: wrap; gap: 16px 28px; align-items: center; }
.site-foot p { margin: 0; font-size: 14px; color: var(--text-faint); }
.site-foot nav { margin-left: auto; display: flex; gap: 20px; flex-wrap: wrap; }
.site-foot nav a:not(.btn) { font-size: 14px; color: var(--text-dim); text-decoration: none; }
.site-foot nav a:not(.btn):hover { color: var(--accent); }

/* --- Motion ------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .001ms !important;
        transition-duration: .001ms !important;
    }
    .card:hover { transform: none; }
}

/* --- Narrow ------------------------------------------------------- */

@media (max-width: 560px) {
    .site-head nav a.nav-hide { display: none; }
    .stage { aspect-ratio: 4 / 3; }
}

/* --- Paginated games list ------------------------------------------ */

/* The list needs a header, not a full hero — it is a utility page, and a
   90vh title band between the nav and the first game would be theatre. */
.hero-slim { padding: clamp(32px, 5vw, 60px) 0 clamp(20px, 3vw, 34px); }
.hero-slim h1 { font-size: clamp(30px, 5.4vw, 54px); margin: 0 0 14px; }
.hero-slim .lede { margin-bottom: 0; }

.pagination {
    display: flex; align-items: center; justify-content: center;
    flex-wrap: wrap; gap: 10px 16px;
    margin-top: clamp(30px, 5vw, 52px);
}

.pg-step {
    padding: 10px 18px; border-radius: 8px;
    border: 1px solid var(--border);
    color: var(--white); text-decoration: none;
    font-weight: 600; font-size: 14.5px;
    white-space: nowrap;
}
.pg-step:hover { border-color: var(--accent); color: var(--accent); }
/* Disabled ends stay in the layout rather than disappearing, so the row does
   not shift sideways as you move between pages. */
.pg-step.is-off { opacity: .3; }

.pg-nums { display: flex; gap: 6px; list-style: none; margin: 0; padding: 0; flex-wrap: wrap; }
.pg-num {
    display: grid; place-items: center;
    min-width: 40px; height: 40px; padding: 0 10px;
    border-radius: 8px; border: 1px solid transparent;
    color: var(--text-dim); text-decoration: none;
    font-weight: 600; font-size: 14.5px;
    font-variant-numeric: tabular-nums;
}
.pg-num:hover { color: var(--white); border-color: var(--border); }
.pg-num.is-on { background: var(--accent); color: #07090c; border-color: var(--accent); }

@media (max-width: 520px) {
    /* Prev/Next are the controls that matter on a phone; the numbers wrap
       under them rather than squeezing the row. */
    .pagination { gap: 12px; }
    .pg-nums { order: 3; width: 100%; justify-content: center; }
}

/* --- About ---------------------------------------------------------
   One photograph and a column of prose. No stat tiles, no timeline: a
   parent reading this is deciding whether to trust a person, and a person
   is made of sentences. */

.about { padding: clamp(24px, 4vw, 48px) 0 clamp(40px, 6vw, 72px); }

.about-grid {
    display: grid; gap: clamp(26px, 4vw, 56px);
    grid-template-columns: 1fr;
    align-items: start;
}
@media (min-width: 860px) {
    .about-grid { grid-template-columns: minmax(0, 0.72fr) minmax(0, 1fr); }
    /* Sticks while the prose scrolls past — the face stays with the words
       that are earning the trust. */
    .about-photo { position: sticky; top: 92px; }
}

.about-photo { margin: 0; position: relative; }
.about-photo img {
    width: 100%; height: auto; display: block;
    border-radius: 14px;
    border: 1px solid var(--border);
    box-shadow: 0 40px 70px -34px #000;
}
.about-photo::after {
    content: ""; position: absolute; inset: auto auto -10px -10px;
    width: 42%; height: 30%;
    border-left: 3px solid var(--accent); border-bottom: 3px solid var(--accent);
    border-radius: 0 0 0 14px;
    pointer-events: none; opacity: .8;
}

.about-body { min-width: 0; max-width: 60ch; }
.about-body p { margin: 0 0 20px; font-size: 16.5px; line-height: 1.75; }

/* The educational-psychology paragraph is the differentiator and the reason
   this page exists, so it gets the weight rather than being one of five
   equal blocks. */
.about-lead {
    font-size: clamp(18px, 2.2vw, 22px) !important;
    line-height: 1.55 !important;
    color: var(--white);
    border-left: 3px solid var(--accent);
    padding-left: clamp(16px, 2vw, 24px);
    margin-bottom: 30px !important;
}

.about-close {
    color: var(--white);
    font-family: var(--display);
    font-size: clamp(17px, 2vw, 21px) !important;
    line-height: 1.45 !important;
    margin-top: 30px;
}
