/* ======================================================================================
   The type, served from this origin.

   It came from fonts.googleapis.com until now, and that stylesheet was a render-blocking
   request to a third party whose only job was to hand back the rules below — 750ms of the
   1,050ms Lighthouse measured as render-blocking, and the reason the LCP breakdown put
   1,840ms into "element render delay" while the logo itself loaded in 320ms. Preloaded from
   here, the files start downloading with the page instead of after a stylesheet on another
   origin has been fetched and parsed.

   Nunito is one file for all four weights the site uses. It is a variable font, which is why
   Google served the identical woff2 for 400, 600, 700 and 800 — hence `font-weight: 400 800`
   on one face rather than four faces pointing at the same bytes.

   latin-ext is declared and costs nothing. `unicode-range` gates the download, so a page of
   plain Latin never fetches it; it is here so a future page with an accent outside U+00FF
   does not silently fall back to the system sans for that one glyph.

   Dropping the third party also took Google Fonts out of privacy.html, which had to list it
   as an origin that saw a visitor's IP.
   ====================================================================================== */

@font-face {
    font-family: 'Nunito';
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url('../assets/fonts/nunito-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'Nunito';
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url('../assets/fonts/nunito-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: 'Archivo';
    font-style: normal;
    font-weight: 800;
    font-display: swap;
    src: url('../assets/fonts/archivo-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'Archivo';
    font-style: normal;
    font-weight: 800;
    font-display: swap;
    src: url('../assets/fonts/archivo-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Reset and Base Styles */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Nunito', 'Quicksand', sans-serif;
    background: linear-gradient(135deg, #f0f4f8 0%, #e2e8f0 100%);
    color: #2d3748;
    overflow-x: hidden;
}

/*
 * The gradient mesh behind every page.
 *
 * It animates `transform`, not `background-position`. Moving a background repaints the
 * layer on every frame, and this layer is the size of the viewport with four gradients in
 * it — a full-screen repaint at 60fps, on every page, forever. A transform is handed to the
 * compositor and costs nothing per frame.
 *
 * `inset: -25%` is the room that makes it possible: the layer is wider than the screen, so
 * it can slide without an edge ever coming into view.
 */
body::before {
    content: "";
    position: fixed;
    inset: -25%;
    z-index: -2;
    background:
        radial-gradient(40vw 40vw at 15% 20%, rgba(255, 107, 107, 0.35), transparent 60%),
        radial-gradient(45vw 45vw at 85% 25%, rgba(78, 205, 196, 0.32), transparent 60%),
        radial-gradient(50vw 50vw at 50% 90%, rgba(254, 202, 87, 0.30), transparent 60%),
        linear-gradient(135deg, #f0f4f8 0%, #e2e8f0 100%);
    animation: meshDrift 34s ease-in-out infinite alternate;
}

@keyframes meshDrift {
    from { transform: translate3d(-2%, -1.5%, 0) scale(1.04); }
    to   { transform: translate3d(2.5%, 2%, 0) scale(1.09); }
}

/* Typography and Variables */
:root {
    --primary-color: #ff6b6b; /* Vibrant coral/pink */
    --secondary-color: #4ecdc4; /* Vibrant teal */
    --accent-color: #feca57; /* Vibrant yellow */
    --accent-deep: #7048e8;    /* deep violet for contrast/CTAs */
    --glass-bg: rgba(255, 255, 255, 0.7);
    --glass-border: rgba(255, 255, 255, 0.5);
    --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
}

/*
 * The scroll-reveal fade is gone, mechanism and all.
 *
 * It hid the home page's heading, its game strip and its 769-word prose section behind
 * `opacity: 0` until an IntersectionObserver said otherwise — so the page's content was
 * conditional on a callback. Guarding it on a class the script sets only moved the
 * failure: a tab opened in the background, a prerender, or any context where the observer
 * never reports still showed an empty page, which is what a 1500px-tall screenshot of it
 * actually looked like.
 *
 * Nothing replaced it. A fade-up on scroll is the most templated effect on the web and it
 * was buying a risk on the one page being judged for having no content. The cards' hover
 * shadow and the strip's arrows are the interaction that remains.
 */

h1, h2, h3 {
    font-weight: 800;
    letter-spacing: -0.5px;
}

/* Three.js Canvas Container */
/* The home page's speckle field. This was a WebGL particle grid — 3,600 points under a
   mouse-tracked camera, animated from JavaScript on every frame, behind a 600KB library
   fetched from a CDN. It cost a blocking request, a parse, and a repaint loop that never
   stopped, on the one page a first-time visitor lands on.

   Three tiled dot layers instead. They drift at different speeds and in different
   directions, which is the parallax the camera used to give, and each travels exactly one
   tile per cycle so the loop has no seam. Composited by the GPU, nothing to download, and
   the reduced-motion rule at the foot of this file stops it dead for anyone who asks. */
#canvas-container {
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    /* The layers inside are oversized so their travel never shows an edge; this clips it. */
    overflow: hidden;
}

/*
 * Two tiled dot layers, each translated rather than scrolled by background-position — the
 * same reason the mesh above moves by transform. They travel in different directions at
 * different speeds, which is the parallax the mouse-tracked WebGL camera used to give, and
 * each moves exactly one tile per cycle so the loop has no seam.
 *
 * Two layers because an element has two pseudo-elements. The third dot size rides along with
 * the second rather than earning a wrapper of its own.
 */
#canvas-container::before,
#canvas-container::after {
    content: "";
    position: absolute;
    /* More than one tile of slack on every side. */
    inset: -160px;
}

#canvas-container::before {
    background-image: radial-gradient(rgba(255, 107, 107, 0.30) 1.5px, transparent 1.6px);
    background-size: 96px 96px;
    animation: dotDriftA 42s linear infinite;
}

#canvas-container::after {
    background-image:
        radial-gradient(rgba(78, 205, 196, 0.26) 1.2px, transparent 1.3px),
        radial-gradient(rgba(254, 202, 87, 0.30) 1px, transparent 1.1px);
    background-size: 64px 64px, 132px 132px;
    background-position: 0 0, 34px 26px;
    animation: dotDriftB 66s linear infinite;
}

@keyframes dotDriftA {
    to { transform: translate3d(96px, 96px, 0); }
}

@keyframes dotDriftB {
    to { transform: translate3d(-64px, 64px, 0); }
}

/* Navigation Bar */
.navbar {
    position: fixed;
    top: 0;
    width: 100%;
    padding: 15px 50px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: var(--glass-bg);
    backdrop-filter: blur(15px);
    -webkit-backdrop-filter: blur(15px);
    border-bottom: 1px solid var(--glass-border);
    box-shadow: var(--glass-shadow);
    z-index: 100;
}

.logo img {
    height: 40px;
    width: auto;
    display: block;
}

.nav-links {
    list-style: none;
    display: flex;
    gap: 30px;
}

.nav-links a {
    color: #4a5568;
    text-decoration: none;
    font-size: 1.1rem;
    font-weight: 700;
    transition: color 0.3s;
}

.nav-links a:hover {
    color: var(--primary-color);
}

/* Dropdown Menu */
.dropdown {
    position: relative;
    display: inline-block;
}

.dropdown-content {
    display: none;
    position: absolute;
    background-color: var(--glass-bg);
    backdrop-filter: blur(20px);
    min-width: 170px;
    box-shadow: var(--glass-shadow);
    border: 1px solid var(--glass-border);
    border-radius: 12px;
    z-index: 1;
    top: 100%;
    left: -20px;
    padding: 16px 6px 6px 6px;
    overflow: hidden;
}

.dropdown-content a {
    color: #4a5568;
    padding: 11px 18px;
    text-decoration: none;
    display: block;
    font-weight: 600;
    border-radius: 8px;
    transition: background 0.3s, color 0.3s;
}

.dropdown-content a:hover {
    background-color: rgba(255, 107, 107, 0.1);
    color: var(--primary-color);
}

.dropdown:hover .dropdown-content {
    display: block;
}

/* Hero Section */
.hero {
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 0 20px;
}

.hero-content {
    max-width: 800px;
    background: var(--glass-bg);
    padding: 50px;
    border-radius: 30px;
    border: 1px solid var(--glass-border);
    box-shadow: var(--glass-shadow);
    backdrop-filter: blur(10px);
}

.hero-content h1 {
    /* Was clamp(2.8rem, 7vw, 5.5rem) — 88px on a wide screen, which read as shouting. */
    font-size: clamp(1.85rem, 6vw, 3.4rem);
    line-height: 1.1;
    /*
     * On a phone the line breaks whatever the size, and left alone it broke after "Big",
     * dropping "Joy" onto a line of its own. `balance` splits it evenly instead —
     * "Little Games," / "Big Joy" — so two lines read as a decision rather than a widow.
     * Browsers without it just wrap as before.
     */
    text-wrap: balance;
    color: #2d3748;
    margin-bottom: 20px;
    background: linear-gradient(90deg, var(--primary-color), var(--accent-deep), var(--secondary-color), var(--primary-color));
    background-size: 300% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    animation: gradientText 8s linear infinite;
}

/*
 * The one animation here that still moves a background, and it has to: the colour is a
 * gradient clipped to the glyphs, and there is no transform that slides a fill through
 * text. It is affordable for the same reason the others were not — the repaint is one
 * heading, not a layer the size of the screen.
 */
@keyframes gradientText {
    to { background-position: 300% 0; }
}

.hero p {
    font-size: clamp(1.05rem, 2.2vw, 1.35rem);
    margin-bottom: 35px;
    color: #4a5568;
    font-weight: 600;
}

/* Entrance on the container so it never fights the h1 gradient animation */
.hero-content {
    animation: heroRise 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

@keyframes heroRise {
    from { opacity: 0; transform: translateY(28px); }
    to   { opacity: 1; transform: none; }
}

.cta-button {
    position: relative;
    display: inline-block;
    padding: 15px 40px;
    font-size: 1.2rem;
    font-weight: 800;
    color: white;
    background: linear-gradient(45deg, var(--primary-color), var(--accent-deep));
    text-decoration: none;
    border-radius: 50px;
    overflow: hidden;
    box-shadow: 0 10px 20px rgba(112, 72, 232, 0.3);
    transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s;
}

/* Shine sweep on hover */
.cta-button::after {
    content: "";
    position: absolute;
    top: 0;
    left: -60%;
    width: 40%;
    height: 100%;
    background: linear-gradient(120deg, transparent, rgba(255, 255, 255, 0.55), transparent);
    transform: skewX(-20deg);
    transition: left 0.6s ease;
}
.cta-button:hover::after { left: 120%; }

.cta-button:hover {
    transform: translateY(-5px) scale(1.05);
    box-shadow: 0 15px 30px rgba(112, 72, 232, 0.45);
}

/* Official Google Play badge artwork, unaltered — their brand guidelines require the artwork
   itself untouched and forbid recreating the Play logo, so this is an <img> of
   assets/google-play-badge.png rather than an inline SVG dressed up as a site pill. Hover lifts
   it without scaling or filtering it. Defined once here so the game pages cannot drift apart.

   The file used to be 646x250: the artwork plus 41px of transparent clear space on every side,
   which Google bakes in. It is cropped to the artwork now, 564x168, and the clear space is
   supplied as margin below — see .play-badge. */
/*
 * The store badge, last in the block of actions and as wide as the ones above it.
 *
 * The asset was cropped to the badge itself. Google ships it with 41px of transparent margin
 * on every side — their required clear space, a quarter of the badge height, baked into the
 * file — and stretching *that* to the row width meant a badge drawn at 84% of the box it sat
 * in, so it read as small and as not lining up with the buttons above it. The clear space is
 * spacing, not part of the mark, and it is supplied below instead.
 *
 * `height: auto` keeps the proportions their guidelines require.
 */
.play-badge {
    display: block;
    /* A quarter of the rendered badge height, which is the clear space Google asks for. The
       badge is ~1/3.36 of its width, so a quarter of that is ~7.4%. */
    margin-top: 7.4%;
    /*
     * Sized against the buttons above it, not against the column. The badge is 1/3.36 of its
     * width, so 380px drew it 113px tall beside 48px buttons — two and a half times their
     * height, which is a banner rather than a badge, and it made the store the loudest thing
     * on a page about the game. 200px puts it at ~60px: still the largest single control, and
     * still comfortably over the 40px minimum Google's guidelines set for the web.
     *
     * A cap, never a height: the artwork's proportions have to stay as shipped.
     */
    max-width: min(100%, 200px);
    margin-inline: auto;
    transition: transform 0.3s ease;
}

.play-badge img {
    display: block;
    width: 100%;
    height: auto;
}

.play-badge:hover {
    transform: translateY(-4px);
}

/* Hero badge chips */
.hero-badges {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 12px;
    margin-top: 30px;
}
.hero-badges span {
    padding: 8px 18px;
    border-radius: 30px;
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    color: #4a5568;
    font-weight: 700;
    font-size: 0.9rem;
    box-shadow: var(--glass-shadow);
    animation: floatChip 4s ease-in-out infinite;
}
.hero-badges span:nth-child(2) { animation-delay: 0.6s; }
.hero-badges span:nth-child(3) { animation-delay: 1.2s; }

@keyframes floatChip {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-6px); }
}

/* Games Section */
.games-section {
    padding: 100px 50px;
}

.games-section h2 {
    text-align: center;
    /* Fluid rather than 3rem flat. A section heading is a signpost, not the content — at 48px
       it was taking a third of a phone screen before the first card. */
    font-size: clamp(1.8rem, 4vw, 2.4rem);
    margin-bottom: clamp(24px, 4vw, 44px);
    color: #2d3748;
}

/*
 * One row that scrolls sideways, at every width, shared by both strips on the page.
 * Wrapping made the page longer with every game added; this stays the same height whatever
 * the catalogue grows to, and it is the same interaction on a phone and a desktop rather
 * than two layouts to keep in step.
 *
 * The negative margin lets the strip bleed to the edges of its section while the padding
 * keeps the first and last card off them, so a half-visible card at the right edge says
 * "there is more" without needing a row of dots to say it.
 */
.card-strip {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    gap: 24px;
    overflow-x: auto;
    /*
     * Explicit, and not redundant. CSS computes a `visible` overflow to `auto` on one axis as
     * soon as the other axis is not visible — so `overflow-x: auto` alone made this strip
     * scrollable up and down as well. Keep it even though the entrance animation that gave it
     * something to scroll is gone: any card that ever sits taller than the row brings the
     * stray vertical travel straight back.
     */
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 24px;
    margin-inline: -24px;
    /* Top padding clears the 8px hover lift, which hidden overflow would otherwise crop. */
    padding: 12px 24px 20px 24px;
    /* Visible and thin where there is a mouse to drag it; hidden under a finger, which needs
       no scrollbar and is better off without one. */
    scrollbar-width: thin;
    scrollbar-color: rgba(112, 72, 232, 0.35) transparent;
}

.card-strip > * {
    scroll-snap-align: center;
}

/* ---------- the strip's arrows ---------- */

/*
 * The positioning context for the two arrows. It sits outside .card-strip on purpose: inside,
 * an absolutely positioned arrow would scroll away with the cards.
 */
.strip-wrap {
    position: relative;
}

.strip-arrow {
    position: absolute;
    top: 50%;
    translate: 0 -50%;
    z-index: 3;
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--glass-border);
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.86);
    backdrop-filter: blur(10px);
    box-shadow: 0 6px 18px rgba(31, 38, 135, 0.18);
    cursor: pointer;
    /* The strip is a rapid-tap surface too — without this a double tap zooms the page. */
    touch-action: manipulation;
    transition: background 0.2s ease;
}

.strip-arrow[hidden] {
    display: none;
}

.strip-arrow:hover {
    background: #fff;
}

.strip-arrow:focus-visible {
    outline: 3px solid var(--accent-deep);
    outline-offset: 2px;
}

/* Left and right sit over the strip's bleed, so a card is never fully covered by one. */
.strip-arrow-left {
    left: -6px;
}

.strip-arrow-right {
    right: -6px;
}

/* Two borders of a square, turned 45°. A glyph would sit differently in every fallback font. */
.strip-chevron {
    width: 11px;
    height: 11px;
    border-top: 2.5px solid #4a5568;
    border-right: 2.5px solid #4a5568;
}

.strip-arrow-left .strip-chevron {
    rotate: -135deg;
    /* The arrow's ink sits off-centre once rotated; nudge it back under the circle's middle. */
    margin-left: 3px;
}

.strip-arrow-right .strip-chevron {
    rotate: 45deg;
    margin-right: 3px;
}

/*
 * There is no entrance animation on the cards any more, and this is where the reason was
 * learned before the whole mechanism was removed: an observer that reveals an element when it
 * intersects the viewport suits a column you scroll past, not a row you swipe along. Cards
 * past the right-hand edge never intersected anything, so swiping to one meant landing on a
 * blank card and waiting for a fade that had already been skipped.
 */
@media (hover: none) {
    .card-strip {
        scrollbar-width: none;
    }

    .card-strip::-webkit-scrollbar {
        display: none;
    }
}

.card {
    flex: 0 0 380px;
    position: relative;
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: 25px;
    width: 380px;
    max-width: 100%;
    overflow: hidden;
    backdrop-filter: blur(15px);
    box-shadow: var(--glass-shadow);
    /*
     * No hover effect. A cursor sheen and a lift came before the tilt did, and each read worse
     * than the last on a card whose whole surface is already the link. What tells you it is
     * tappable is the pointer over it, not the card moving away from it.
     */
    transition: box-shadow 0.3s ease;
}

/* Nothing moves. A slightly deeper shadow is the whole hover state. */
.card:hover {
    box-shadow: 0 18px 38px rgba(31, 38, 135, 0.16);
}

.card-image {
    height: 220px;
    background: linear-gradient(45deg, #feca57, #ff6b6b);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 2rem;
    font-weight: 800;
    color: white;
    text-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

/*
 * Sized against the banner rather than in fixed pixels. At 170px flat it burst out of the
 * shorter banner the mobile layout uses, and a logo hanging over the edge of its own card is
 * the first thing anyone notices.
 */
/* App art in a square file. 22% is roughly the platform's own icon curve, and a proportion
   rather than a length so it holds at 234px in a card and at 120px on a game page. */
.game-icon {
    border-radius: 22%;
    height: 78%;
    width: auto;
    aspect-ratio: 1;
    max-width: 78%;
    object-fit: contain;
    filter: drop-shadow(0 6px 16px rgba(0,0,0,0.25));
}

.card-content {
    padding: 30px;
    text-align: center;
}

.card-content h3 {
    font-size: 1.8rem;
    margin-bottom: 15px;
    color: #2d3748;
}

.card-content p {
    color: #718096;
    line-height: 1.6;
    margin-bottom: 25px;
    font-size: 1.1rem;
    font-weight: 600;
}

.card-links {
    display: flex;
    justify-content: center;
    align-items: stretch;
}

.card-links a {
    /*
     * Flex and centred *here*, on the selector that wins.
     *
     * `.card-play` set `display: flex; align-items: center` and it never applied: this
     * rule is one class plus a type (0,1,1) and beats a lone class (0,1,0), so the pills
     * stayed inline-block and `align-items` meant nothing. With `min-height` on an
     * inline-block the line box sits at the top, which is why every label had 1px above it
     * and 24px below.
     */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 10px 25px;
    background: var(--secondary-color);
    color: white;
    text-decoration: none;
    /* Three labels in one row on a 380px card leaves ~104px each, and 1rem wrapped every
       one of them onto two lines. */
    font-size: 0.9rem;
    /* And nowrap, which is the actual fix: with `flex-basis: 0` the pills were sized by the
       even split rather than by their content, so "▶ Play" and "About" broke across two
       lines inside boxes wide enough for them. Nowrap makes each pill's min-content its
       whole label, which is what a button should be. */
    white-space: nowrap;
    font-weight: 700;
    border-radius: 20px;
    transition: background 0.3s, transform 0.2s;
    box-shadow: 0 5px 15px rgba(78, 205, 196, 0.3);
}

.card-links a:hover {
    background: #3db8b0;
    transform: translateY(-2px);
}

/* Back Button */
.back-button {
    display: inline-block;
    padding: 10px 20px;
    margin-bottom: 20px;
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: 20px;
    color: #4a5568;
    text-decoration: none;
    font-weight: 700;
    font-size: 0.9rem;
    transition: all 0.3s ease;
    box-shadow: 0 4px 6px rgba(0,0,0,0.05);
}
.back-button:hover {
    background: rgba(255, 255, 255, 0.9);
    color: var(--primary-color);
    transform: translateY(-2px);
    box-shadow: 0 6px 12px rgba(0,0,0,0.08);
}


/*
 * The advert on a game's about page. Its width matches the card the actions sit in, so it reads
 * as part of the page rather than as something bolted underneath it. Nothing on this page is
 * sized from a constant, so the advert may be whatever height Google fills it with without
 * pushing anything off a screen.
 *
 * `min-height` is there for layout shift rather than for layout. An `auto` slot starts at
 * zero and grows to whatever arrives, and everything below it moves the moment it does —
 * Lighthouse measured 0.335 of CLS on the live site against 0.000 locally, where no advert
 * fills, which is what points at this. 100px is the height the play pages already budget for
 * their banner, so it is the number this codebase already trusts: it absorbs a leaderboard
 * without leaving a large hole on a page whose adverts are not serving yet. A taller fill
 * still shifts, and the alternative — a fixed height with the overflow clipped — is against
 * AdSense's own policy.
 */
.page-ad {
    max-width: 800px;
    min-height: 100px;
    margin: 30px auto 0 auto;
    padding: 0 20px;
}

/* ---------- the side rails ---------- */

/*
 * On a wide screen everything here is a centred column and the space either side of it is
 * empty, so it carries a 160x600 advert on each side. They are absent below the widths named
 * here: a rail that does not fit either overlaps the content or hangs off the page, and there
 * is no version of a narrow screen where three adverts is the right answer.
 *
 * Absolutely positioned, not flex items. On a play page the board's width is
 * `min(..., 100%, ...)`, and that 100% resolves against the stage — making the stage a
 * content-sized flex item would move the number the whole board-sizing model rests on. Out of
 * flow, a rail cannot touch it.
 *
 * The offsets are measured from the centre: half the content's own width, then the gap, then
 * the rail. Each context has its own half-width, which is why there are two blocks.
 */
.ad-rail {
    display: none;
}

.play-with-rails,
.page-with-rails {
    position: relative;
}

/* Play pages: the board is at most 520px, so half of it is 260. 260 + 32 gap + 160 rail + 24
   clear of the window edge means the rails need 476px either side of the centre. */
@media (min-width: 1024px) {
    .play-with-rails .ad-rail {
        display: block;
        position: absolute;
        top: 0;
        width: 160px;
    }

    .play-with-rails .ad-rail-left {
        left: calc(50% - 452px);
    }

    .play-with-rails .ad-rail-right {
        left: calc(50% + 292px);
    }
}

/* About pages: the card is at most 800px, so the same arithmetic from a half-width of 400
   needs 616px either side, and the rails cannot appear until the window is 1280. */
@media (min-width: 1280px) {
    .page-with-rails .ad-rail {
        display: block;
        position: absolute;
        top: 40px;
        width: 160px;
    }

    .page-with-rails .ad-rail-left {
        left: calc(50% - 592px);
    }

    .page-with-rails .ad-rail-right {
        left: calc(50% + 432px);
    }
}

/* Footer */
footer {
    text-align: center;
    padding: 40px;
    color: #718096;
    font-weight: 600;
}

/* Responsive */
@media (max-width: 768px) {
    .navbar {
        padding: 15px 20px;
    }
    .nav-links {
        display: none;
    }
    .hero-content {
        padding: 36px 26px;
    }
    .games-section {
        padding-left: 20px;
        padding-right: 20px;
    }
}

/* Respect reduced-motion: kill entrance/loop animations */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        /* The delay has to go too. A staggered `animation-fill-mode: both` holds its
           from-state for the length of the delay, so cutting only the duration still left
           the board's cells invisible for a third of a second. */
        animation-delay: 0s !important;
        transition-duration: 0.001ms !important;
    }
    .hero-content { opacity: 1; }
}

/*
 * "Play in your browser" — the primary action on a game page, above the store links.
 * Shared rather than per-game: every games/<slug>/index.html gets the same button.
 */
/*
 * Two class names, not one. .play-now is a <p> inside .game-info-container, and each game page
 * carries an inline `.game-info-container p { margin-bottom: 30px }` — higher specificity, so a
 * plain `.play-now` loses however late it is declared. That 30px sat on top of the grid's own
 * margin and put twice as much space above the legal links as below them.
 */
.game-info-container .play-now {
    margin-top: 30px;
    margin-bottom: 0;
}

.play-now a {
    /* The whole row: playing here is the primary action on the page, and the two legal links
       and the store badge below it are all full-width too. */
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px;
    padding: 14px 30px;
    border-radius: 14px;
    background: var(--accent-deep);
    color: #fff;
    text-decoration: none;
    font-weight: 800;
    font-size: 1.05rem;
    box-shadow: var(--glass-shadow);
    transition: filter 0.2s ease, transform 0.2s ease;
}

.play-now a:hover {
    filter: brightness(1.1);
    transform: translateY(-2px);
}

/* ---------- game cards: two actions, and a whole-card tap target ---------- */

.card-links {
    gap: 10px;
    flex-wrap: wrap;
}

/* Play and About split the row evenly. Sized to their labels they came out lopsided — "About"
   is half the width of "▶ Play" — which read as one real button beside an afterthought. */
.card-play,
.card-info,
.card-store {
    /*
     * Sized by their labels, not by an even split of the row.
     *
     * `flex: 1 1 0` divided the row between however many pills a card happened to have, so
     * PawDoku — the one game with a live listing, and so the one card with three — drew Play
     * at 87px while the other five drew it at 154px. Reading down the strip, the same button
     * changed size on every card. Content-sized, they match everywhere, and the row centres
     * whatever it holds.
     *
     * Each pill also centres its own label and declares its height: left to the flex line
     * they came out 54, 54 and 42 with identical box properties. 44px is the floor for a
     * comfortable touch target.
     */
    flex: 0 1 auto;
    min-height: 44px;
    padding-block: 0 !important;
    padding-inline: 16px !important;
    text-align: center;
    box-sizing: border-box;
    /*
     * About carries a 1px border and Play does not, and that alone made the two halves 2px
     * apart: a border-box flex basis cannot resolve below the border it contains, so About
     * started 2px ahead and kept the lead through the even split of the free space. A
     * transparent border on Play costs nothing and makes the two identical boxes.
     */
    border: 1px solid transparent;
}

/* Play is the primary action on the front page, because four of these are playable in the
   browser and the shortest path from the front door to a game should be one tap. */
.card-play {
    background: var(--accent-deep) !important;
    /* Softer than the 0 5px 15px it had: beside two flat siblings that much lift made the
       primary action look like it was hovering off the card. */
    box-shadow: 0 2px 8px rgba(112, 72, 232, 0.25) !important;
}

/*
 * There is no play triangle on this button any more.
 *
 * It began as the glyph "▶", which Nunito does not have — it fell through to a system
 * symbol font whose line box is taller than the type around it. Drawing it from three
 * borders fixed the metrics, and then the shape had to justify 15px of a row that could not
 * spare them on a phone. It could not: "Play" is unambiguous on its own, and the primary
 * action is already the only one with a filled background.
 */

.card-play:hover {
    background: #5c34d4 !important;
}

/*
 * The secondary pills need a body you can see. Their border was var(--glass-border) —
 * rgba(255,255,255,.5), a highlight for the edge of a panel floating over the page — which
 * on the card's own white ground is invisible. Both read as bare text beside a solid Play
 * button, so a row of three sibling controls looked like one button and two links.
 */
.card-info,
.card-store {
    background: #eef0f5 !important;
    color: #414b5f !important;
    border: 1px solid rgba(0, 0, 0, 0.1) !important;
    box-shadow: none !important;
}

.card-info:hover,
.card-store:hover {
    background: #e3e7ef !important;
    color: #22293a !important;
}

.card-info:hover {
    background: rgba(255, 255, 255, 0.7) !important;
}

/*
 * The whole card opens the game. A card that looks tappable and is not — where only a
 * 90px pill inside it responds — is the most common way a phone layout feels broken, and the
 * cure is one stretched pseudo-element rather than restructuring the markup into a link.
 * The real buttons sit above it, so they still get their own taps.
 */
.card-play::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: inherit;
}

.card-links a {
    position: relative;
    z-index: 2;
}

/* ---------- mobile: the games scroll sideways ---------- */

@media (max-width: 768px) {
    /*
     * Five stacked cards is a very long page, and it gets longer with every game. Sideways,
     * one card at a time, it stays the same length however many there are.
     *
     * The negative margin lets the strip bleed to the screen edges while the padding keeps
     * the first and last card off them, so a half-visible card at the right edge tells the
     * player there is more without needing a row of dots to say it.
     */
    .card-strip {
        gap: 16px;
        scroll-padding-inline: 20px;
        margin-inline: -20px;
        /* Same clearance for the hover lift as the desktop rule, since overflow-y is hidden. */
        padding: 12px 20px 12px 20px;
    }

    /* A share of the screen rather than a fixed width, so one card fills a phone and the
       next one peeks. 90% rather than 84%: the strip already holds 24px of padding either
       side, so the share is taken twice and 84% left a 281px card on a 375px phone — two
       pixels short of fitting its own three buttons on one row. A tenth of a card is still
       plainly a card, so the peek survives. */
    .card {
        flex: 0 0 90%;
        max-width: 340px;
    }

    /* Half the height of the desktop banner: on a phone the icon is the identifier, and the
       description below it is what the player actually reads. */
    .card-image {
        height: 150px;
    }

    .card-content p {
        display: -webkit-box;
        -webkit-line-clamp: 4;
        -webkit-box-orient: vertical;
        overflow: hidden;
        font-size: 0.95rem;
    }
}

/*
 * The store link is a pill, not the badge.
 *
 * The badge is an image with a fixed 3.36:1 shape and a 40px floor under its height on the
 * web, which on a 380px card is 168x50 — so it could not share a row with two buttons and
 * took one of its own, 95px tall before it was capped. Three pills fit one row, and the
 * card loses an image nobody needed to read.
 *
 * The official artwork still appears on each game's own page, which is where a promotional
 * badge belongs. The label stays "Google Play" in full: their guidelines do not allow it
 * abbreviated.
 */
.card-store {
    text-align: center;
    box-sizing: border-box;
}

/*
 * A phone card gives the row less to work with than the pills need at desktop measure:
 * .card-content's 30px of padding either side leaves 255px of a 315px card, and the three
 * labels want 289px. So the padding comes in and the pills tighten, rather than the store
 * link being pushed onto a line of its own — which is what a `flex-basis: 100%` here used
 * to do, from back when the pills were an even split and "Google Play" could not fit at any
 * padding.
 *
 * Below about 340px the row still wraps, and it is left to wrap on its own terms.
 */
@media (max-width: 768px) {
    .card-content {
        /* 30px either side left 243px of a 281px card, and the three labels want 261px.
           The padding is what was oversized, not the buttons. */
        padding: 24px 16px;
    }

    .card-links {
        gap: 6px;
    }

    .card-play,
    .card-info,
    .card-store {
        /* The triangle's 15px bought this back from 10px. */
        padding-inline: 12px !important;
        font-size: 0.85rem;
    }
}

/* The clear space the cropped asset no longer carries — a quarter of the badge height, and the
   badge is 1/3.36 of its width. The row's own 10px gap is not enough on its own. */
.card-store:hover {
    transform: translateY(-2px);
}

/* ---------- phone menu ---------- */

.nav-menu {
    display: none;
    position: relative;
}

.nav-menu > summary {
    list-style: none;
    cursor: pointer;
    border-radius: 12px;
    /* Fixed, not padding-derived. The three bars are absolutely positioned, so they add no
       height of their own and the button came out 23px tall — half a comfortable target. */
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.nav-menu > summary::-webkit-details-marker {
    display: none;
}

.nav-menu[open] > summary {
    background: rgba(255, 255, 255, 0.7);
}

/* Three bars drawn from one element, so the icon needs no image and no font. */
.nav-burger,
.nav-burger::before,
.nav-burger::after {
    display: block;
    width: 22px;
    height: 2.5px;
    border-radius: 2px;
    background: #2d3748;
    transition: transform 0.2s ease, opacity 0.2s ease;
}

.nav-burger::before,
.nav-burger::after {
    content: "";
    position: absolute;
}

.nav-burger::before {
    transform: translateY(-7px);
}

.nav-burger::after {
    transform: translateY(7px);
}

/* Open, the bars become a cross — the same element saying it will close again. */
.nav-menu[open] .nav-burger {
    background: transparent;
}

.nav-menu[open] .nav-burger::before {
    transform: rotate(45deg);
}

.nav-menu[open] .nav-burger::after {
    transform: rotate(-45deg);
}

.nav-menu > ul {
    position: absolute;
    right: 0;
    top: calc(100% + 10px);
    min-width: 190px;
    list-style: none;
    margin: 0;
    padding: 8px;
    /* Opaque. A menu is a thing you read, and the hero headline behind it was showing
       straight through the translucent panel this started as. */
    background: #fff;
    border: 1px solid var(--glass-border);
    border-radius: 16px;
    box-shadow: 0 18px 40px rgba(31, 38, 135, 0.18);
    z-index: 100;
}

/* The site sets a bottom margin on list items for the prose pages; a menu is not prose. */
.nav-menu > ul li {
    margin: 0;
}

.nav-menu > ul a {
    display: block;
    padding: 11px 14px;
    border-radius: 10px;
    color: #2d3748;
    text-decoration: none;
    font-weight: 700;
}

.nav-menu > ul a:hover {
    background: rgba(112, 72, 232, 0.1);
    color: var(--accent-deep);
}

/*
 * Last in the file on purpose. The base rule above hides the phone menu, and this has to come
 * after it to win — same specificity, so order is the only thing deciding which applies.
 */
@media (max-width: 768px) {
    .nav-menu {
        display: block;
    }
}


/*
 * The navbar is sticky, so an anchor jump lands with the heading underneath it. Every jump on
 * this site goes to a section, so the offset belongs on the sections rather than on each link.
 */
section[id] {
    scroll-margin-top: 88px;
}

/* ---------------------------------------------------------------------------
   Long-form prose: the how-to-play guides on the game pages, and the About and
   Contact pages. One block here rather than a copy in each page's <style>,
   because eight pages want the identical treatment.

   .game-info-container is centred, which is right for an icon, a title and two
   buttons and wrong for four hundred words. .guide resets to a left-aligned
   measure inside it.
   --------------------------------------------------------------------------- */
.guide {
    text-align: left;
    margin-top: 45px;
    padding-top: 35px;
    /* Not var(--glass-border): that is a near-white highlight meant for the edge of a
       card floating over the page, and on the card's own pale ground it is invisible —
       the divider was there and could not be seen. */
    border-top: 1px solid rgba(0, 0, 0, 0.08);
}

.guide h2 {
    font-size: 1.6rem;
    color: var(--primary-color);
    margin: 38px 0 14px;
}

.guide h2:first-child {
    margin-top: 0;
}

.guide h3 {
    font-size: 1.15rem;
    color: var(--accent-deep);
    margin: 26px 0 10px;
}

/* The container sets 1.2rem/600 on every <p>, which is a blurb weight — a guide
   set in it reads as shouting. */
.guide p,
.guide li {
    font-size: 1.02rem;
    font-weight: 400;
    line-height: 1.75;
    color: #3d4657;
    margin-bottom: 16px;
}

.guide ul,
.guide ol {
    /* Indent with padding, not margin. A margin moves the list box and leaves the
       browser's own 40px padding-inline-start in place, so the markers ended up 62px
       in with the text starting after them. */
    margin: 0 0 18px;
    padding-left: 24px;
}

.guide li {
    margin-bottom: 9px;
}

.guide strong {
    color: #2c3444;
    font-weight: 700;
}

.guide a {
    color: var(--accent-deep);
    font-weight: 600;
}

/* A table of the level ramps and board sizes. Scrolls in its own box rather than
   widening the page: three of these have more columns than a phone has room for. */
.guide-table-wrap {
    overflow-x: auto;
    margin-bottom: 20px;
    -webkit-overflow-scrolling: touch;
}

.guide table {
    border-collapse: collapse;
    width: 100%;
    min-width: 380px;
    font-size: 0.95rem;
}

.guide th,
.guide td {
    text-align: left;
    padding: 9px 14px;
    border-bottom: 1px solid rgba(0, 0, 0, 0.08);
    color: #3d4657;
}

.guide th {
    font-weight: 700;
    color: #2c3444;
    background: rgba(255, 255, 255, 0.5);
}

/* A pulled-out aside: the one thing about a game worth knowing before playing it. */
.guide-note {
    background: rgba(255, 255, 255, 0.55);
    border-left: 4px solid var(--secondary-color);
    border-radius: 0 12px 12px 0;
    padding: 16px 20px;
    margin: 0 0 22px;
}

.guide-note p:last-child {
    margin-bottom: 0;
}

/* The site footer grew a row of links, so the copyright line needs to stop being
   the only thing in it. */
.footer-links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px 22px;
    margin-bottom: 14px;
}

.footer-links a {
    /* The footer sits on the page's own light ground, not a dark bar. */
    color: #5a6478;
    text-decoration: none;
    font-weight: 600;
    transition: color 0.2s ease;
}

.footer-links a:hover {
    color: var(--primary-color);
    text-decoration: underline;
}

@media (max-width: 600px) {
    .guide {
        margin-top: 34px;
        padding-top: 26px;
    }

    .guide h2 {
        font-size: 1.35rem;
    }

    .guide p,
    .guide li {
        font-size: 0.98rem;
    }
}

/* The home page's prose block, under the game strip. A card like the game pages'
   so it reads as part of the same site rather than as a wall of text bolted on. */
.home-prose {
    max-width: 820px;
    margin: 10px auto 40px;
    padding: 38px 44px;
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: 20px;
    backdrop-filter: blur(10px);
}

/* .guide's rule separates it from whatever sits above it on a game page — an icon, a
   title and two buttons. This card starts with the guide, so there is nothing for the
   rule to divide and it reads as a line drawn for no reason. */
.home-prose .guide {
    border-top: none;
    margin-top: 0;
    padding-top: 0;
}

@media (max-width: 700px) {
    .home-prose {
        margin: 10px 15px 30px;
        padding: 28px 22px;
    }
}

/* ---------------------------------------------------------------------------
   The common-questions block at the foot of each guide.

   <details> rather than a button and a script: the browser already has a
   disclosure widget, it opens on click, Enter and Space, and reports its state
   to a screen reader. Collapsed content is still indexed, so nothing is hidden
   from a crawler by folding it away.
   --------------------------------------------------------------------------- */
.guide .faq {
    margin-bottom: 10px;
}

.guide .faq details {
    border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}

.guide .faq details:first-of-type {
    border-top: 1px solid rgba(0, 0, 0, 0.08);
}

.guide .faq summary {
    cursor: pointer;
    list-style: none;
    padding: 15px 34px 15px 0;
    position: relative;
    font-weight: 700;
    font-size: 1.02rem;
    color: #2c3444;
    line-height: 1.5;
}

.guide .faq summary::-webkit-details-marker {
    display: none;
}

/* The chevron is drawn rather than typed: a rotating glyph drifts off centre
   because a character's box is not its ink. */
.guide .faq summary::after {
    content: "";
    position: absolute;
    right: 6px;
    top: 22px;
    width: 8px;
    height: 8px;
    border-right: 2px solid var(--accent-deep);
    border-bottom: 2px solid var(--accent-deep);
    transform: rotate(45deg);
    transition: transform 0.2s ease;
}

.guide .faq details[open] summary::after {
    transform: rotate(-135deg);
    top: 25px;
}

.guide .faq summary:hover {
    color: var(--accent-deep);
}

.guide .faq details > p {
    margin: 0 0 16px;
    padding-right: 34px;
}

@media (max-width: 600px) {
    .guide .faq summary {
        font-size: 0.98rem;
        padding-right: 28px;
    }

    .guide .faq details > p {
        padding-right: 0;
    }
}

/* ======================================================================================
   The home board.

   Four of the six games are literally grids of coloured cells, and Rekta's whole mechanic
   is cutting a grid into rectangles that tile it exactly. So the hero is a board: one
   6x4 partition with no gaps and no overlaps — a pitch block of 3x4 and six rectangles of
   four different sizes, which is a legal Shikaku solution rather than a card grid.

   It replaces a 100vh section holding one glass card, which was mostly empty gradient and
   asked the visitor to scroll before seeing a single game. Now the six games *are* the
   hero, and choosing one is the first thing available.

   Ink ground, because that is what these games actually look like: four of the six icons
   sit on near-black. #191527 is a real aubergine rather than a tinted black.

   The six fills are chosen, not sampled — sampling the icons collapsed PawDoku onto 2048
   and StackO! onto Rekta, since the dominant colour of a dark-grounded icon is the ground.
   Each wears the hue its own art wears, all at one darkness so they read as one board, and
   placed so no two neighbours share a hue. White on the lightest of them is 5.5:1.
   ====================================================================================== */
.board {
    --ink: #191527;
    background: var(--ink);
    /* The navbar is fixed, so the board owns the room under it. */
    padding: 100px 22px 60px;
}

.board-grid {
    max-width: 1180px;
    margin: 0 auto;
    display: grid;
    /* A hairline of ink between cells: the rule a puzzle board draws, not a card gutter. */
    gap: 5px;
    grid-template-columns: repeat(6, 1fr);
    grid-auto-rows: minmax(104px, auto);
    grid-template-areas:
        "pitch pitch pitch paw   paw   kata"
        "pitch pitch pitch paw   paw   kata"
        "pitch pitch pitch stac  t2048 t2048"
        "pitch pitch pitch stac  rek   pour";
}

.board-pitch {
    grid-area: pitch;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 20px;
    padding: 42px 46px 42px 8px;
    color: #fff;
}

.board-pitch h1 {
    font-family: 'Archivo', 'Nunito', sans-serif;
    font-weight: 800;
    font-size: clamp(2.05rem, 4.4vw, 3.5rem);
    line-height: 0.97;
    letter-spacing: -0.035em;
    color: #fff;
    /* Two short sentences: let the browser pick the break so neither line is a runt.
       Left to itself it broke after "Play", mid-sentence. */
    text-wrap: balance;
}

.board-pitch p {
    max-width: 46ch;
    font-size: 1.05rem;
    line-height: 1.65;
    color: rgba(255, 255, 255, 0.78);
    font-weight: 400;
}

.board-pitch a {
    align-self: flex-start;
    color: #fff;
    font-weight: 700;
    font-size: 0.95rem;
    text-decoration: none;
    border-bottom: 2px solid var(--accent-color);
    padding-bottom: 2px;
}

.board-pitch a:hover {
    border-bottom-color: #fff;
}

.board-tile {
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding: 16px;
    /* Tiles, not cards: enough to soften a corner, nowhere near the 20px the rest of the
       site uses on panels. */
    border-radius: 4px;
    background: var(--tile);
    color: #fff;
    text-decoration: none;
    transition: background-color 0.25s ease;
}

.board-tile:hover,
.board-tile:focus-visible {
    background-color: color-mix(in srgb, var(--tile) 82%, white);
}

/*
 * The art sits fully inside its cell, sized by `--art` and not by a share of the width.
 *
 * It bled off the top-right corner first, which was meant to read as a piece resting on the
 * board and read as breakage instead: a percentage of the width made the icon enormous on a
 * wide cell, and the overhang cropped PawDoku's paw and cut 2048's tiles in half. A square
 * mark in a rectangle of unknown proportion has to be given a size, not a fraction.
 */
.board-tile img {
    position: absolute;
    top: 14px;
    right: 14px;
    width: var(--art, 58px);
    height: auto;
    border-radius: 22%;
    transition: transform 0.25s ease;
}

.board-tile:hover img,
.board-tile:focus-visible img {
    transform: translateY(-4px);
}

/*
 * The name and the hook live in a wrapper so the phone layout has one box to set beside the
 * art — and it has to stack at every width. As a plain block div it left both children
 * inline, and every tile read "PawDoku One cat per row. No numbers." on one line.
 */
.board-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    position: relative;
}

.board-tile b {
    position: relative;
    font-family: 'Archivo', 'Nunito', sans-serif;
    font-size: 1.12rem;
    font-weight: 800;
    letter-spacing: -0.015em;
    line-height: 1.1;
}

.board-tile span {
    position: relative;
    margin-top: 5px;
    font-size: 0.86rem;
    line-height: 1.4;
    /*
     * 0.90, not 0.80. An alpha blends toward the tile, so the effective colour is not white
     * and checking white against the fill is the wrong sum: at 0.80 over PawDoku's amber it
     * lands on 4.12:1, and 13.8px at regular weight needs 4.5. The amber is the lightest of
     * the six and the only one that failed; 0.90 puts the worst of them at 4.76:1 and the
     * hook is still plainly secondary to a name in full white.
     */
    color: rgba(255, 255, 255, 0.9);
}

/* The two smallest cells carry a name and the art, and let the game's own page do the
   describing — a hook squeezed into four lines is worse than no hook. */
.board-tile.is-small span {
    display: none;
}

/*
 * One orchestrated moment: the cells settle once, in reading order, like a board resolving.
 * It animates *from* an offset toward each tile's natural state, so a browser that skips
 * the animation — or a reader who asked it to — still gets a finished board rather than an
 * empty one. That was the bug in the fade-on-scroll this replaces.
 */
@keyframes settle {
    from {
        opacity: 0;
        transform: translateY(12px) scale(0.985);
    }
}

.board-pitch,
.board-tile {
    animation: settle 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

.board-tile:nth-of-type(1) { animation-delay: 0.06s; }
.board-tile:nth-of-type(2) { animation-delay: 0.12s; }
.board-tile:nth-of-type(3) { animation-delay: 0.18s; }
.board-tile:nth-of-type(4) { animation-delay: 0.24s; }
.board-tile:nth-of-type(5) { animation-delay: 0.30s; }
.board-tile:nth-of-type(6) { animation-delay: 0.36s; }

@media (max-width: 900px) {
    .board-grid {
        grid-template-columns: repeat(4, 1fr);
        grid-template-areas:
            "pitch pitch paw   paw"
            "pitch pitch kata  stac"
            "t2048 t2048 rek   pour";
    }

    .board-pitch {
        padding: 34px 26px 34px 6px;
    }
}

/*
 * On a phone the board becomes a list, and each tile a row: art on the left, in the flow,
 * with the text beside it.
 *
 * Two columns at 375px gave each tile about 190px, which is not enough for a name and a
 * sentence — and the art, absolutely positioned top-right, landed exactly where the name
 * was on a short cell, so "Kata·Word" ran underneath its own icon. A row cannot overlap.
 * The partition is a desktop idea anyway; on one column there is nothing to partition.
 */
@media (max-width: 620px) {
    .board {
        padding: 84px 14px 44px;
    }

    .board-grid {
        grid-template-columns: 1fr;
        grid-auto-rows: auto;
        grid-template-areas: none;
        gap: 6px;
    }

    .board-pitch,
    .board-tile {
        grid-area: auto !important;
    }

    .board-pitch {
        padding: 8px 6px 24px;
        gap: 16px;
    }

    .board-tile {
        flex-direction: row;
        /* flex-end is what bottom-aligns the text on a board cell; turned sideways it
           packs the row against the right edge, so every row started at a different x. */
        justify-content: flex-start;
        align-items: center;
        gap: 14px;
        padding: 15px 16px;
        min-height: 0;
    }

    .board-tile img {
        position: static;
        flex: 0 0 auto;
        width: 52px;
        border-radius: 12px;
    }

    /* There is room for a hook on every row now, including the two that go without it
       when they are 1x1 cells on a board. */
    .board-tile.is-small span {
        display: block;
    }
}
