/* Playgama MCP · /mcp — old terminal screen.

   Four colours, and nothing else: onyx black, Playgama purple, white, terminal
   green. Onyx is the screen, green is what the terminal says, purple marks the
   structure, white carries the reading.

   The page runs the full width of the window inside a gutter, with the reading
   held to an 800px column on the left. A dashed rule runs the full width
   between its parts, and that is the only line the page draws: no header, no
   navigation, no boxes. The one script, main.js, starts a game's gameplay
   video when you point at its tile, copies the endpoint, and now and then
   takes the picture away for half a second. */

:root {
    --onyx: #0e0e10;
    --purple: #9747ff;
    --purple-lt: #b07dff;
    /* The lit purple: what the guide link is set in, and what a game name
       turns when you point at it. */
    --purple-hi: #aa76ff;
    --white: #ffffff;
    --green: #10e963;

    /* Transparencies of the four, for text that should sit back and for the
       hairlines between things. Nothing here is a fifth colour. */
    --white-70: rgba(255, 255, 255, 0.72);
    --white-55: rgba(255, 255, 255, 0.55);
    --purple-40: rgba(151, 71, 255, 0.4);
    --purple-12: rgba(151, 71, 255, 0.12);
    /* The endpoint frame sits in front of the scanlines now, so it no longer
       gets the 24% black they mixed in. This takes it back past where it was:
       less purple in the fill is a darker box, not a greyer one. */
    --purple-07: rgba(151, 71, 255, 0.07);

    --mono: "DM Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;

    --gutter: 20px;
    --content: 800px;
}

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

body {
    margin: 0;
    background: var(--onyx);
    color: var(--white-70);
    font-family: var(--mono);
    font-size: 15px;
    line-height: 1.4;
    -webkit-text-size-adjust: 100%;
}

/* The old screen: a scanline grid fixed to the viewport, so the page reads as
   something displayed rather than something printed. Faint on purpose — text
   contrast comes first — and dropped for readers who have asked the platform
   for more contrast. */
body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 40;
    pointer-events: none;
    background: repeating-linear-gradient(
        to bottom,
        rgba(0, 0, 0, 0.24) 0 1px,
        transparent 1px 3px
    );
}
@media (prefers-contrast: more) {
    body::after {
        display: none;
    }
}

/* The reading is not read through the screen. The body copy and the button
   step out in front of the overlay, the way a game cover does on hover — same
   z-index, same root stacking context, and z-index needs a position, so
   relative is enough. What stays behind the lines is the page's own furniture:
   the headings, the endpoint frame and the grid. */
body > section > p {
    position: relative;
    z-index: 50;
}

/* Every part of the page is one of these, so they can be styled by element and
   the markup stays free of layout classes. They run the full width of the
   window inside the gutter; it is the reading that is held to a column, not the
   page. The game grid is deliberately not, and neither is the legal line that
   closes the footer: both fill the width.

   The rule between the parts is drawn, but the distances are still what does
   the separating, on a scale of
   8 / 16 / 32: 8 binds a line to the one above it, 16 separates paragraphs and
   headings, 32 is the vertical padding on a content block, so two of them sit
   64 apart. The one number off the scale is the 6 between a game's name and its
   genre, which was asked for directly. */
body > header,
body > section,
body > footer {
    padding: 32px var(--gutter);
}
body > header > *,
body > section > :not(.tiles),
body > footer > :not(.footer__legal) {
    max-width: var(--content);
}
/* The top of the page is not a gap between blocks; it gets the gutter. */
body > header {
    padding-top: var(--gutter);
}

/* The rules start and end with the text, on the same gutter as everything
   else. Drawn as a gradient rather than `border-style: dashed`, so the dashes
   are the length of a typed hyphen instead of whatever the browser picks.
   They stay behind the scanlines with the rest of the page's furniture. */
hr {
    height: 1px;
    /* The blocks either side already give 32; this adds 12 to each, so a rule
       sits in 44 of clear space rather than 32. */
    margin: 12px var(--gutter);
    border: 0;
    background: repeating-linear-gradient(to right, var(--purple-40) 0 7px, transparent 7px 12px);
}

a {
    color: var(--green);
    text-decoration: none;
    border-bottom: 1px solid currentColor;
}
a:hover {
    color: var(--white);
}

:focus-visible {
    outline: 2px solid var(--green);
    outline-offset: 3px;
}

/* 23px is the ceiling and was asked for directly: the headings sit just above
   the body text rather than over it, the way a terminal prints a line. No
   clamp — the size is the same on a phone as on a desktop. */
h1,
h2 {
    margin: 0 0 16px;
    color: var(--white);
    font-weight: 400;
    font-size: 23px;
    line-height: 1.3;
}

p {
    margin: 0 0 16px;
}
p:last-child {
    margin-bottom: 0;
}

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ── wordmark and command line ──────────────────────────────────────────
   The wordmark is one asset — the brand lock-up with MCP already set inside
   it. Its height is the only knob, and it comes off a variable so a phone
   narrower than ~470px scales it down rather than overflow the gutter. Flex
   rather than a bare inline image, so no descender gap sits under it. */
.wordmark {
    --logo-h: clamp(44px, 12vw, 56px);
    display: flex;
    align-items: center;
    margin: 0 0 8px;
}
.wordmark__logo {
    height: var(--logo-h);
    width: auto;
    /* text-shadow does not reach an image; this is the same CRT glow. */
    filter: drop-shadow(0 0 26px rgba(151, 71, 255, 0.5));
}

.prompt {
    margin: 0;
    font-size: clamp(13px, 2.6vw, 16px);
    color: var(--green);
    text-shadow: 0 0 9px rgba(16, 233, 99, 0.4);
    word-break: break-word;
}
.caret {
    display: inline-block;
    width: 0.55em;
    height: 1.05em;
    margin-left: 3px;
    background: var(--green);
    vertical-align: text-bottom;
    animation: blink 1.06s step-end infinite;
}
@keyframes blink {
    50% {
        opacity: 0;
    }
}
@media (prefers-reduced-motion: reduce) {
    .caret {
        animation: none;
    }
}

/* ── the MCP endpoint ─────────────────────────────────────────────────────
   The one thing on the page a reader has to take away with them, so it is the
   only framed element left, and it carries `user-select: all` — one click takes
   the whole URL, which is as close to a copy button as a page with no
   JavaScript gets. */
.endpoint {
    margin: 32px 0 0;
    padding: 16px;
    border: 1px solid var(--purple);
    background: var(--purple-07);
    /* Out in front of the scanlines, like the copy around it — the URL is
       there to be read and copied, not to be read through the screen. */
    position: relative;
    z-index: 50;
}
/* Built by main.js, so a page without JavaScript never carries a button that
   cannot work — there, one click on the URL still selects the whole thing.
   It sits in the frame's bottom corner, and the URL is padded away from it so
   a long one never runs underneath. */
.endpoint__copy {
    position: absolute;
    right: 10px;
    bottom: 10px;
    display: flex;
    padding: 6px;
    border: 0;
    background: none;
    color: var(--purple-lt);
    line-height: 0;
    cursor: pointer;
}
.endpoint__copy:hover,
.endpoint__copy:focus-visible {
    color: var(--green);
}
.endpoint--copyable code {
    padding-right: 34px;
}

.endpoint__label {
    display: block;
    margin-bottom: 8px;
    color: var(--purple-lt);
    font-size: 12px;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}
.endpoint code {
    display: block;
    font-family: inherit;
    /* The endpoint is read as part of the text around it, so it takes the
       reading's own size rather than a fluid one of its own — the same 15px,
       at every width. `inherit` rather than the number, so it cannot drift
       from the body the way a second copy of the number would. */
    font-size: inherit;
    color: var(--green);
    text-shadow: 0 0 9px rgba(16, 233, 99, 0.35);
    overflow-wrap: anywhere;
    -webkit-user-select: all;
    user-select: all;
}

/* The guide sits under the endpoint frame, 16 below it — the frame gives up
   its bottom margin so this number is the whole gap. It runs the two page
   colours the other way round from every other link: lit purple at rest, green
   when you point at it. */
.cta {
    margin: 16px 0 0;
}
.cta a {
    color: var(--purple-hi);
}
.cta a:hover,
.cta a:focus-visible {
    color: var(--green);
}

/* The portal's superellipse, copied as-is from playgama.com's `.sqrcl`.

   `clip-path: shape()` is what does it — not `corner-shape: squircle`, which is
   the property everyone reaches for first and which no shipping browser renders
   yet. shape() has been in Chromium since 135 and is what the portal ships.

   `--_sqrcl` is the only knob: the corner size, halved into `--_r` and capped
   at 25% of the box. Everything else is the curve construction and should be
   left alone — keeping it identical is the point, so a card here and a card on
   the portal are the same shape. */
.sqrcl {
    --_r: clamp(0%, var(--_sqrcl) / 2, 25%);
    --_v: calc(var(--_r) * (1 - sqrt(2) / 4));
    --_p: calc(var(--_v) - var(--_r) / 2);
    clip-path: shape(
        from var(--_v) var(--_p),
        curve to 50% 0 with var(--_r) 0,
        curve to calc(100% - var(--_v)) var(--_p) with calc(100% - var(--_r)) 0,
        curve to calc(100% - var(--_p)) var(--_v) with calc(100% - 2 * var(--_p))
            calc(2 * var(--_p)),
        curve to 100% 50% with 100% var(--_r),
        curve to calc(100% - var(--_p)) calc(100% - var(--_v)) with 100% calc(100% - var(--_r)),
        curve to calc(100% - var(--_v)) calc(100% - var(--_p)) with calc(100% - 2 * var(--_p))
            calc(100% - 2 * var(--_p)),
        curve to 50% 100% with calc(100% - var(--_r)) 100%,
        curve to var(--_v) calc(100% - var(--_p)) with var(--_r) 100%,
        curve to var(--_p) calc(100% - var(--_v)) with calc(2 * var(--_p))
            calc(100% - 2 * var(--_p)),
        curve to 0 50% with 0 calc(100% - var(--_r)),
        curve to var(--_p) var(--_v) with 0 var(--_r),
        curve to var(--_v) var(--_p) with calc(2 * var(--_p)) calc(2 * var(--_p))
    );
}

/* The line under the grid's heading is its caption, not a paragraph of its
   own, so 8 binds it to the heading where 16 would set it apart. */
body > section:has(.tiles) > h2 {
    margin-bottom: 8px;
}

/* ── game tiles ──────────────────────────────────────────────────────────
   A game embedded from the portal always looks the same: a 16:9 cover with
   superellipse corners, and its gameplay video on hover. The grid fills the
   window rather than the reading column, at the portal's own card size — cards
   stay around 200px wide and the count per row follows the window, up to five.

   The min track is the larger of that 200px and the width five cards would take
   right here: the row less its four 20px gaps, split five ways. Under a ~1120px
   window the 200px wins and auto-fill drops to four cards and fewer, exactly as
   before; above it the computed width wins and five is all that fits. */
.tiles {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(max(200px, (100% - 4 * 20px) / 5), 1fr));
    gap: 32px 20px;
    margin: 32px 0 0;
    padding: 0;
    list-style: none;
}
.tile {
    display: block;
    border: 0;
    color: var(--white);
}
/* Cover and video share one box, so the video can sit exactly on top of the
   still without either of them setting the tile's height. */
.tile__media {
    position: relative;
    display: block;
    aspect-ratio: 16 / 9;
    margin-bottom: 8px;
    overflow: hidden;
    background: var(--purple-12);
    /* Superellipse corners, the portal's own .sqrcl — see the block below.
       This is the only knob, and it was turned up from 28 on request. 36px is
       where it stops: the curve starts 18px in from each corner, which stays
       under the 25% cap in --_r at both the desktop and the phone card size —
       so the shape is the same at every width — and past it the straight run
       along the top and bottom edges visibly bows. */
    --_sqrcl: 36px;
    transition: filter 0.12s linear;
}
.tile__cover,
.tile__video {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
/* The video waits under nothing and over the cover, revealed on hover by
   main.js. A tile with no video never gets the class and never changes. */
.tile__video {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity 0.18s linear;
}
.tile__media.is-playing .tile__video {
    opacity: 1;
}
.tile__name {
    display: block;
    margin-bottom: 6px;
    font-size: 14px;
    line-height: 1.35;
    /* In front of the scanlines, like the rest of the reading. */
    position: relative;
    z-index: 50;
}
.tile__meta {
    display: block;
    color: var(--white-55);
    font-size: 12px;
    /* In front of the scanlines, with the name it belongs to. */
    position: relative;
    z-index: 50;
}
/* Pointing at a cover lifts it over the scanline overlay (z-index 40 on
   body::after) so the game is seen through nothing: the grid is the screen the
   page is displayed on, and the thing being looked at steps out of it. Both are
   in the root stacking context — no ancestor of a tile creates one — so a
   z-index here is enough. */
.tile:hover .tile__media,
.tile:focus-visible .tile__media {
    z-index: 50;
    filter: brightness(1.12);
}
.tile:hover .tile__name,
.tile:focus-visible .tile__name {
    color: var(--purple-hi);
}

/* ── the signal ──────────────────────────────────────────────────────────
   The page is a screen, so now and then the screen loses the picture: the
   frame tears along its own scanlines, slips sideways and settles. main.js
   runs it — on arrival and then every 20 seconds, three times a session at
   most — through a view transition whose callback is empty. Nothing in the
   document changes: the browser snapshots the page and it is the snapshot that
   comes apart, so this needs no wrapper element and moves no layout.

   `steps(1, end)` is what makes it read as a signal rather than a wobble —
   the frame snaps between offsets instead of sliding between them. Every
   offset is multiplied by --signal-jitter, so the whole thing has one knob.

   The torn snapshot sits over a second, identical one that never moves, and
   that steady copy is what shows through the holes the mask punches. No new
   colour enters: the tear is the page against itself. */
::view-transition-old(root) {
    --signal-jitter: 0.4;
    --signal-band: repeating-linear-gradient(to bottom, #000 0 7px, transparent 7px 13px);
    --signal-band-fine: repeating-linear-gradient(to bottom, #000 0 2px, transparent 2px 5px);
    z-index: 2;
    mask-mode: alpha;
    animation: signal-tear 0.5s steps(1, end) both;
}
/* Whatever the browser would have done to the incoming frame, it should do
   nothing: it is the same frame, and it is the thing being torn away from. */
::view-transition-new(root) {
    z-index: 1;
    animation: none;
}
@keyframes signal-tear {
    0%,
    6% {
        transform: none;
        mask-image: none;
    }
    12% {
        transform: translateX(calc(-11px * var(--signal-jitter)));
        mask-image: var(--signal-band);
    }
    20% {
        transform: translate(calc(14px * var(--signal-jitter)), calc(2px * var(--signal-jitter)));
        mask-image: var(--signal-band-fine);
    }
    28% {
        transform: translateX(calc(-4px * var(--signal-jitter)))
            skewX(calc(0.2deg * var(--signal-jitter)));
        mask-image: none;
    }
    36% {
        transform: translate(calc(-19px * var(--signal-jitter)), calc(-2px * var(--signal-jitter)));
        mask-image: var(--signal-band);
    }
    46% {
        transform: translateX(calc(8px * var(--signal-jitter)));
        mask-image: var(--signal-band-fine);
    }
    56% {
        transform: translateX(calc(-6px * var(--signal-jitter)));
        mask-image: var(--signal-band);
    }
    66% {
        transform: translateX(calc(12px * var(--signal-jitter)));
        mask-image: none;
    }
    76% {
        transform: translateX(calc(-3px * var(--signal-jitter)));
        mask-image: var(--signal-band-fine);
    }
    86%,
    100% {
        transform: none;
        mask-image: none;
    }
}
/* Same opt-out the scanlines and the hover videos already take. */
@media (prefers-reduced-motion: reduce) {
    ::view-transition-old(root),
    ::view-transition-new(root) {
        animation: none !important;
    }
}

/* ── footer ─────────────────────────────────────────────────────────────── */
body > footer {
    padding-bottom: 64px;
    font-size: 13px;
    color: var(--white-55);
}
/* The footer is read, not looked at, so it steps in front of the scanlines
   with the rest of the reading. Named rather than `footer > *`, because
   .sr-only is positioned and must stay that way to keep itself hidden. */
.footer__email,
.footer__legal {
    position: relative;
    z-index: 50;
}
.footer__email {
    font-size: 16px;
    /* The guide link's colours: lit purple at rest, green when pointed at. */
    color: var(--purple-hi);
}
.footer__email:hover,
.footer__email:focus-visible {
    color: var(--green);
}
.footer__legal {
    margin: 16px 0 0;
    line-height: 1.9;
}
.footer__legal a {
    color: var(--white-55);
    border-bottom-color: var(--white-55);
}
.footer__legal a:hover {
    color: var(--green);
}

/* Past 1440 the page stops laying out wider and starts growing instead: the
   body zooms by exactly as much as the window exceeds 1440, so it always lays
   out at 1440 and everything in it — type, gutters, game covers — is drawn
   larger in proportion. Five cards a row hold at any width, and the cover is
   what gets bigger, which is the point.

   The ratio is written as tan(atan2(...)) rather than calc(100vw / 1440px):
   zoom takes a number, dividing a length by a number gives a length, and typed
   length-by-length division is not something to bet a layout on yet. This form
   has worked everywhere since 2023. zoom sits on body, not html, so the media
   query still reads the real window and there is no feedback loop. */
@media (min-width: 1441px) {
    body {
        zoom: tan(atan2(100vw, 1440px));
    }
}

/* Phones get a 12px gutter; tablets keep the desktop 40px. */
@media (max-width: 600px) {
    :root {
        --gutter: 12px;
    }
    .tiles {
        grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
        gap: 24px 12px;
    }
}
