
/* PLAY DIVERGES FROM onejs.com HERE, ON PURPOSE.

   These tokens used to be onejs.com's own, copied value for value so this read
   as a page of that site rather than a separate product. That was the right
   call while Play was a catalog hanging off the product. It stopped being the
   right call when the front page became a sketchbook: onejs.com sells a
   package, and this is where you try things that may not work. Sai's decision,
   2026-09-14: diverge deliberately, and possibly bring onejs.com to this look
   later if it turns out well.

   So the NAMES are unchanged and stay that way. Every one of these exists in
   Website/src/styles/base.css under the same name, which is what makes moving
   this palette over there a copy of values rather than a rewrite of rules.
   Anyone adding a token here should add it there too, or not add it.

   Paper: a warm off-white ground, warm near-black ink, and the burnt orange
   both sites already used as the single accent. Still no webfonts, on either
   side. */
:root {
    --bg: #faf7f2;
    --bg-code: #f2ede4;
    --text: #23201c;
    --text-muted: #6b635a;
    /* Checked rather than eyeballed. --text-label sets 0.78rem uppercase
       labels, which is small text, so it needs the full 4.5:1 and not the
       3:1 that large text gets away with. The first warm value I picked read
       fine and measured 3.88, which would have been a quiet accessibility
       regression from the white palette it replaced. */
    --text-label: #736a5e;
    --border: #e3dbcf;
    --link: #c2410c;
    /* DARKER on hover here, where the old palette went brighter. On white,
       brighter was the only direction that kept a link looking like a link.
       On a warm ground the same move reads as neon, which is the one thing the
       redesign rules out, and going darker still lands nowhere near --text. */
    --link-hover: #9a3412;
    /* Only for a control whose next click destroys something. */
    --danger: #b3261e;
    --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --font-serif: Georgia, "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
    --board-empty: #e7dfd2;
    --board-line: #dbd0c0;

    /* Measure, and the width of things that are not prose. A line of text
       stops being comfortable somewhere around 70 characters, which is the
       whole reason a book is the shape it is. */
    --measure: 34rem;
    --wide: 58rem;
    /* One radius, small. The old file had 2, 3, 6, 8 and 10px depending on
       which rule you landed in, which is not a decision, it is an accident
       repeated. Pills and circles keep their own shapes. */
    --radius: 3px;
    /* The spacing scale: multiples of this, not arbitrary rem values. */
    --step: 0.5rem;
    color-scheme: light;
}

/* Paper after dark: warm through the whole range rather than neutral grey, or
   the ground reads as a different site with the lights off. Here the accent
   does go brighter on hover, because on a dark ground that is the direction
   that reads as alive. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #181613;
        --bg-code: #211e1a;
        --text: #e8e2d8;
        --text-muted: #a79c8e;
        --text-label: #998d7c;
        --border: #332e28;
        --link: #e8834a;
        --link-hover: #f2a273;
        --danger: #e8776a;
        --board-empty: #2b2721;
        --board-line: #3a342c;
        color-scheme: dark;
    }
}
[data-theme="dark"] {
    --bg: #181613;
    --bg-code: #211e1a;
    --text: #e8e2d8;
    --text-muted: #a79c8e;
    --text-label: #998d7c;
    --border: #332e28;
    --link: #e8834a;
    --link-hover: #f2a273;
    --danger: #e8776a;
    --board-empty: #2b2721;
    --board-line: #3a342c;
    color-scheme: dark;
}

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

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

/* In-content links: the accent, no underline. Every piece of chrome (the
   logo, both navs, cards, buttons, the manage rows) sets its own colour, so
   this reaches prose and nothing else. */
a { color: var(--link); text-decoration: none; }
/* Colour rather than an underline. The chrome shifts to --text on hover, but a
   prose link cannot: leaving the accent for the body colour reads as the link
   being switched off rather than pointed at. One step along the same hue says
   the same thing without spending the colour that marks it as a link. */
a:hover { color: var(--link-hover); }

/* Full bleed. A catalog meant to hold hundreds of games should use the whole
   screen; capping it at 1200px would waste half a wide monitor on margins. */
/* The column owns the width and the gutter; this is just a block. */
.wrap { width: 100%; }
:root { --edge: clamp(1rem, 2.6vw, 3rem); }

/* ---------- header, matching the marketing page's rhythm ---------- */

header {
    width: 100%; padding: 0 var(--edge);
    border-bottom: 1px solid var(--border);
}
.header-inner {
    width: 100%;
    display: flex; justify-content: space-between; align-items: center;
    padding: 1.1rem 0; gap: 1.5rem; flex-wrap: wrap;
}
/* OneJS stays in the site's serif so the parent brand carries over; only the
   Play half is game-like. Drawn as SVG rather than loaded as a pixel font: the
   site ships no webfonts, and one word is not worth a network request. */
.logo {
    text-decoration: none; display: flex; align-items: center; gap: 0.55rem;
    color: var(--text);
}
/* The citrus mark. Orange comes from color because the icon strokes
   currentColor, and it is set here rather than on the path so a theme can
   reach it. Sized against the serif OJ beside it rather than in pixels, so the
   two stay in proportion if the wordmark ever changes size. */
.logo .logo-mark {
    width: 1.25rem; height: 1.25rem; display: block;
    color: #ea580c; flex: none;
}
.logo .name { font-family: var(--font-serif); font-size: 1.4rem; line-height: 1; }
.logo .pixel-play { height: 0.85rem; width: auto; display: block; }
.logo .pip {
    width: 0; height: 0; margin-right: 0.1rem;
    border-left: 0.5rem solid currentColor;
    border-top: 0.32rem solid transparent; border-bottom: 0.32rem solid transparent;
}
/* The pip nudges forward on hover, the way a play button wants to. */
.logo .pip { transition: transform 0.16s cubic-bezier(.3,1.5,.5,1); }
.logo:hover .pip { transform: translateX(2px); }

/* Scoped to the header. As a bare element selector this reached every nav on
   the site, and the docs sidebars are navs: align-items on a column flex
   container centred every link, and "nav a" beat ".docs-nav-link" on
   specificity, so it set their size and colour too. */
header nav { display: flex; align-items: center; gap: 1.25rem; }
header nav a { font-size: 0.9rem; text-decoration: none; color: var(--text-muted); }
header nav a:hover { color: var(--text); }

.search {
    flex: 1; max-width: 340px; display: flex; align-items: center;
    border: 1px solid var(--border); border-radius: var(--radius); padding: 0.4rem 0.7rem;
}
.search input {
    all: unset; width: 100%; font-family: var(--font-sans);
    font-size: 0.9rem; color: var(--text);
}
.search input::placeholder { color: var(--text-muted); }
/* No focus ring here: the global input rule would draw a rectangle inside the
   bordered box, and a ring on the box makes two. The border darkening is the
   focus state. */
.search input:focus-visible { outline: none; }
.search:focus-within { border-color: var(--text); }

a:focus-visible, button:focus-visible, [tabindex]:focus-visible, input:focus-visible {
    outline: 2px solid var(--text); outline-offset: 2px; border-radius: var(--radius);
}

/* The site's own button idiom: bordered, and hover inverts. */
/* inline-flex with an explicit line-height, not inline-block: a <button> and an
   <a> resolve their default line box differently, so the same padding produced
   visibly different heights when both were used as buttons side by side. */
.cta-button {
    display: inline-flex; align-items: center; justify-content: center;
    box-sizing: border-box; padding: 0.6rem 1.15rem; line-height: 1.25;
    border: 1px solid var(--text); border-radius: var(--radius);
    font-size: 0.92rem; text-decoration: none; color: var(--text);
    background: none; cursor: pointer; font-family: var(--font-sans);
    white-space: nowrap;
}
/* Forms are only wrappers here; without this they add a line box of their own
   and the buttons inside sit lower than the links beside them. */
.game-actions form { display: contents; }
.cta-button:hover { background: var(--text); color: var(--bg); }
.cta-button.quiet { border-color: var(--border); color: var(--text-muted); }
.cta-button.quiet:hover { background: none; border-color: var(--text); color: var(--text); }
.cta-button.small { padding: 0.4rem 0.8rem; font-size: 0.85rem; }

/* ---------- the stage ---------- */

.stage {
    position: relative; width: 100%; aspect-ratio: 16 / 9;
    background: var(--bg-code);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    display: grid; place-items: center; overflow: hidden;
}

/* The fallback card: a square grid that scales with whatever box it is in, so
   one drawing serves a gallery card and a hero stage. */
.gen-cover {
    display: grid; grid-template-columns: repeat(5, 1fr); gap: 6%;
    width: 62%; aspect-ratio: 1;
}
.gen-cover i { border-radius: 14%; display: block; }

/* An author's cover fills the card and stays out of the way: no pointer events,
   so the whole card is still one link, and no border of its own. */
.cover-frame {
    position: absolute; inset: 0; width: 100%; height: 100%;
    border: 0; display: block; pointer-events: none;
    background: var(--bg-code);
    /* Covers are authored at their own aspect and shown at the card's, so the
       one that does not match is cropped rather than letterboxed or squashed. */
    object-fit: cover;
}

/* The best runs. A rank, a name and a number, in the same shape as every other
   key/value row in the sidebar so it reads as part of the panel rather than as
   a widget somebody bolted on. */
.score-row .rank {
    display: inline-block; width: 1.4rem;
    font-style: normal; color: var(--text-label);
    font-variant-numeric: tabular-nums;
}
.score-row b { font-variant-numeric: tabular-nums; }
.score-more {
    margin-top: 0.5rem; font-size: 0.72rem; color: var(--text-label);
}

/* The Play docs.
   Three columns where there is room: what there is to read, what you are
   reading, and where you are in it. The middle column is capped near 68
   characters because that is what running prose wants, and the outer two
   collapse before it does.

   No width and no side padding of its own: main.page.docs-column is the
   column, and this is the grid inside it. Carrying both here as well was one
   cap that could not bind and one gutter counted twice. */
.docs {
    display: grid; grid-template-columns: 13rem minmax(0, 1fr) 11rem;
    gap: 3rem; align-items: start;
    padding: 2.5rem 0 5rem;
}
.docs-nav, .docs-toc {
    position: sticky; top: 2rem;
    display: flex; flex-direction: column;
    /* Both stated rather than left to the default: a column flex container
       centres its children when something upstream sets align-items, and a
       sidebar of centred links is the first thing anyone notices. */
    align-items: stretch; text-align: left;
    gap: 0.05rem;
}
.docs-nav-title {
    font-size: 0.66rem; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.1em; color: var(--text-label); margin-bottom: 0.7rem;
}
.docs-nav-link, .docs-toc-link {
    display: block; text-align: left; text-decoration: none;
    color: var(--text-muted); line-height: 1.4;
}
.docs-nav-link {
    /* Indented by a hair so the current page's marker has somewhere to sit
       without the text moving when it appears. */
    padding: 0.32rem 0 0.32rem 0.7rem; font-size: 0.855rem;
    border-left: 2px solid transparent; margin-left: -0.7rem;
}
.docs-nav-link:hover, .docs-toc-link:hover { color: var(--text); }
.docs-nav-link.current {
    color: var(--text); font-weight: 600; border-left-color: var(--text);
}
.docs-toc-link { padding: 0.28rem 0; font-size: 0.79rem; }

/* The column is wider than the reading measure, and prose is narrowed back to
   it below. Running text wants about 70 characters; a code sample and a table
   want every pixel they can get, and holding all three to one width either
   clips the samples or stretches the prose. */
.docs-body { min-width: 0; max-width: var(--measure); font-size: 0.945rem; }
.docs-body > p, .docs-body > ul, .docs-body > ol, .docs-body > blockquote, .docs-lede {
    max-width: 34rem;
}
.docs-body h1 {
    font-family: var(--font-serif); font-size: 2.05rem; line-height: 1.15;
    letter-spacing: -0.012em; margin: 0 0 0.55rem; text-wrap: balance;
}
.docs-lede {
    color: var(--text-muted); font-size: 1.04rem; line-height: 1.5;
    margin: 0 0 2.25rem; text-wrap: pretty;
}
/* Headings in the serif too, so a page reads as one voice rather than as a
   serif title bolted onto a sans document. */
.docs-body h2 {
    font-family: var(--font-serif); font-size: 1.42rem; letter-spacing: -0.008em;
    margin: 2.75rem 0 0.85rem; padding-top: 1.5rem;
    border-top: 1px solid var(--border); text-wrap: balance;
}
.docs-body h3 {
    font-family: var(--font-serif); font-size: 1.1rem;
    margin: 1.9rem 0 0.5rem; text-wrap: balance;
}
.docs-body p, .docs-body li { line-height: 1.7; text-wrap: pretty; }
.docs-body p { margin: 0 0 1.05rem; }
.docs-body a { color: var(--link); }
.docs-body ul, .docs-body ol { padding-left: 1.2rem; }
.docs-body li { margin: 0.35rem 0; }
.docs-body code {
    font-family: var(--font-mono); font-size: 0.86em;
    background: var(--bg-code); padding: 0.12em 0.34em; border-radius: var(--radius);
}
/* Shiki paints its own colours, so the wrapper only owns the box. Scrolls
   inside itself: a long line in a sample must never widen the page. */
.docs-body pre {
    overflow-x: auto; padding: 0.9rem 1rem; border-radius: var(--radius);
    margin: 1.1rem 0; font-size: 0.82rem; line-height: 1.55;
}
.docs-body pre code { background: none; padding: 0; font-size: inherit; }
.docs-body table { width: 100%; border-collapse: collapse; margin: 1.2rem 0; font-size: 0.9rem; }
.docs-body th, .docs-body td { text-align: left; padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--border); }
.docs-body th { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-label); }
.docs-body blockquote {
    margin: 1.2rem 0; padding: 0.1rem 0 0.1rem 1rem;
    border-left: 2px solid var(--border); color: var(--text-muted);
}

@media (max-width: 60rem) {
    /* One column, and the page comes BEFORE the index. Left in source order a
       reader on a phone scrolls past ten section links and a heading before
       reaching a word of what they opened. The on-this-page column goes: it is
       a jump list for a page you can already see, which a phone cannot do. */
    .docs { grid-template-columns: minmax(0, 1fr); gap: 2.5rem; padding-top: 1.75rem; }
    .docs-nav, .docs-toc { position: static; }
    .docs-toc { display: none; }
    .docs-body { order: 1; max-width: none; }
    .docs-nav { order: 2; border-top: 1px solid var(--border); padding-top: 1.75rem; }
    .docs-body > p, .docs-body > ul, .docs-body > ol, .docs-body > blockquote, .docs-lede {
        max-width: none;
    }
}

/* The second eject sits under the first as the quieter of the two. */
.eject.quiet { margin-top: 0.75rem; }
.eject.quiet b { font-weight: 600; }

/* The tour. Terse by design, so the styling gets out of the way: one column,
   generous spacing between sections, nothing decorative. */
/* One reading width, shared rather than repeated, so the pages that are a
   column of text or rows cannot drift apart from each other. A grid of cards
   wants the full window; a list you read does not. */
.tour { max-width: var(--wide); }
.tour { padding: 2.5rem var(--edge) 5rem; }
.tour h1 { font-family: var(--font-serif); font-size: 2.1rem; margin: 0 0 0.4rem; }
.tour-lede { color: var(--text-muted); font-size: 1.05rem; margin: 0 0 2.5rem; }
.tour section { margin-bottom: 2.6rem; }
.tour h2 {
    font-family: var(--font-serif); font-size: 1.3rem; margin: 0 0 0.9rem;
    padding-top: 1.4rem; border-top: 1px solid var(--border);
}
.tour ul { margin: 0; padding-left: 1.15rem; }
.tour li { margin: 0.4rem 0; line-height: 1.6; }
.tour code {
    font-family: var(--font-mono); font-size: 0.86em;
    background: var(--bg-code); padding: 0.12em 0.34em; border-radius: var(--radius);
}
/* Shiki paints its own colours, so the wrapper owns only the box. */
.tour pre {
    overflow-x: auto; margin: 1.1rem 0; padding: 0.9rem 1rem; border-radius: var(--radius);
    font-size: 0.9rem; line-height: 1.5;
}
/* inherit, or the .tour code rule above multiplies its 0.86em into the block
   as well and the one snippet on the page renders a size smaller than
   anything around it. */
.tour pre code { background: none; padding: 0; font-size: inherit; }

/* Name on the left, one line on the right. */
.tour-grid { display: grid; grid-template-columns: max-content 1fr; gap: 0.55rem 1.2rem; margin: 0; }
.tour-grid dt { font-weight: 600; }
.tour-grid dd { margin: 0; color: var(--text-muted); line-height: 1.5; }

.tour-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.tour-table th {
    text-align: left; font-size: 0.7rem; text-transform: uppercase;
    letter-spacing: 0.07em; color: var(--text-label); font-weight: 600;
    padding: 0 0.7rem 0.5rem 0;
}
.tour-table td { padding: 0.45rem 0.7rem 0.45rem 0; border-top: 1px solid var(--border); }
.tour-table td:first-child { font-weight: 600; }
.tour-note { color: var(--text-muted); font-size: 0.88rem; margin: 0.9rem 0 0; line-height: 1.55; }

@media (max-width: 34rem) {
    .tour-grid { grid-template-columns: 1fr; gap: 0.15rem; }
    .tour-grid dd { margin-bottom: 0.7rem; }
}

/* An author's own games: one row each, name on the left, controls on the
   right. A list rather than cards, because this page is for doing something to
   a game rather than for choosing one to play. */
/* The assets panel's three ways in. The drop state is deliberately obvious:
   a drop target that does not light up is one you aim at and hope. */
.ed-dropping { outline: 2px dashed var(--link); outline-offset: -4px; }
/* The name is a button now, because it copies. It should still read as the
   file's name and not as a control, so it borrows nothing from a button. */
.ed-asset {
    background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
    font: inherit; color: inherit; text-align: left;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ed-asset:hover { color: var(--link); }
.ed-asset-copied { color: var(--link); }
.ed-asset-from {
    font-size: 0.65rem; color: var(--text-label); white-space: nowrap;
    opacity: 0.85;
}

/* What the account is holding. A bar rather than a number alone, because
   "412 MB of 1 GB" is arithmetic and a bar is a glance. */
.quota { display: flex; flex-direction: column; gap: 0.3rem; max-width: 22rem; }
.quota-bar {
    height: 5px; border-radius: var(--radius); background: var(--border); overflow: hidden;
}
.quota-fill { height: 100%; background: var(--link); }
.quota-said { font-size: 0.78rem; color: var(--text-muted); }

/* The visibility control.
   The Set button is visible by default and a script hides it after wiring the
   select to submit on change. That way round on purpose: hiding it in CSS and
   revealing it for non-JavaScript readers needs a class on the document that
   nothing sets, so the control would have been unusable for exactly the people
   the button exists for. Enhancement removes it; it is never missing first. */
.manage-see { display: flex; align-items: center; gap: 0.3rem; }
.manage-see-pick {
    font: inherit; font-size: 0.8rem; color: var(--text);
    background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 0.2rem 0.4rem;
}

/* Off screen rather than hidden: a label a screen reader still reads, which is
   the point of having one on a select whose visible text is its value. */
.visually-hidden {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.manage-list { display: flex; flex-direction: column; margin-bottom: 4rem; }
.manage-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1.5rem; flex-wrap: wrap;
    padding: 1rem 0; border-bottom: 1px solid var(--border);
}
.manage-what { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.manage-title { display: flex; align-items: center; gap: 0.5rem; min-width: 0; flex-wrap: wrap; }
.manage-name { font-size: 1.05rem; font-weight: 600; color: var(--text); text-decoration: none; }
.manage-name:hover { color: var(--link); }
.manage-meta { font-size: 0.8rem; color: var(--text-label); }
/* Outlined rather than filled, and beside the name rather than over the row.
   This list is mostly published sketches and the mark is the exception, so it
   has to be findable while scanning without turning every private row into a
   coloured block. The word carries it; the box is just what keeps the word
   from reading as part of the title. */
.manage-tag {
    flex: none; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.04em;
    text-transform: uppercase; color: var(--text-label);
    border: 1px solid var(--border); border-radius: var(--radius);
    padding: 0.1rem 0.36rem; line-height: 1.5;
}
.manage-does { display: flex; align-items: center; gap: 0.5rem; }

/* Closed it is one more quiet button. Open, the confirmation takes the row
   under the controls rather than pushing them sideways, which is what happened
   when this lived inline: the question and its button landed beside the button
   that opened them and the whole row went crooked. */
.danger { position: relative; }
.danger summary { list-style: none; cursor: pointer; }
.danger summary::-webkit-details-marker { display: none; }
.danger[open] > summary { color: var(--text-label); }
.danger[open] form {
    position: absolute; right: 0; top: calc(100% + 0.5rem); z-index: 5;
    display: flex; align-items: center; gap: 0.75rem; white-space: nowrap;
    padding: 0.7rem 0.85rem; border-radius: var(--radius);
    background: var(--bg); border: 1px solid var(--border);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
    font-size: 0.85rem; color: var(--text-muted);
}
.danger form { display: none; }
.cta-button.danger-go { background: #b3261e; border-color: #b3261e; color: #ffffff; }
.cta-button.danger-go:hover { background: #8c1d17; border-color: #8c1d17; }

@media (max-width: 34rem) {
    /* No room to float a panel beside a button on a phone. */
    .danger[open] form {
        position: static; flex-direction: column; align-items: stretch;
        box-shadow: none; margin-top: 0.5rem;
    }
}

/* ---------- the account page ---------- */

/* A sidebar of sections and one section at a time. The tokens used to sit at
   the bottom of one long page, under the games and the bookmarks, which is
   where nobody looking for them would look. */
.manage {
    display: grid; grid-template-columns: 11rem minmax(0, 1fr); gap: 3.5rem;
    max-width: 68rem; padding-top: 2.4rem; padding-bottom: 4rem; align-items: start;
}
.manage-side { position: sticky; top: 1.5rem; display: flex; flex-direction: column; gap: 1.2rem; }
.manage-who { display: flex; flex-direction: column; gap: 0.1rem; padding: 0 0.7rem; }
.manage-who b { font-size: 1.05rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.manage-who a { font-size: 0.8rem; }
.manage-nav { display: flex; flex-direction: column; gap: 0.1rem; }
.manage-nav a {
    display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;
    padding: 0.45rem 0.7rem; border-radius: var(--radius);
    color: var(--text-muted); font-size: 0.92rem; text-decoration: none;
}
.manage-nav a:hover { color: var(--text); background: var(--bg-code); }
.manage-nav a[aria-current="page"] { color: var(--text); background: var(--bg-code); font-weight: 600; }
.manage-count { font-size: 0.75rem; color: var(--text-label); font-variant-numeric: tabular-nums; font-weight: 400; }
.manage-signout { padding: 0.6rem 0.7rem 0; border-top: 1px solid var(--border); }
.manage-signout button {
    border: 0; background: none; padding: 0; cursor: pointer;
    font-family: var(--font-sans); font-size: 0.85rem; color: var(--text-muted);
}
.manage-signout button:hover { color: var(--text); }
.manage-main { min-width: 0; }
.manage-main h1 { margin: 0; font-size: 2rem; }
.manage-sub {
    margin: 2rem 0 0.6rem; font-size: 0.72rem; color: var(--text-label); font-weight: 400;
    letter-spacing: 0.08em; text-transform: uppercase;
}
.manage-hint { margin: 0.5rem 0 0; font-size: 0.88rem; color: var(--text-muted); max-width: 40rem; }
@media (max-width: 48rem) {
    .manage { grid-template-columns: minmax(0, 1fr); gap: 1.4rem; padding-top: 1.4rem; }
    .manage-side { position: static; gap: 0.6rem; }
    .manage-who { padding: 0; }
    .manage-nav { flex-direction: row; flex-wrap: wrap; gap: 0.25rem; }
    .manage-nav a { padding: 0.35rem 0.65rem; }
    .manage-signout { border-top: 0; padding: 0; }
}

/* One person's page. Deliberately plain: the games are the content, and a
   profile that competed with them would be a worse gallery. */
.author { padding: 2.4rem 0 1.4rem; }
.author h1, .author h2 { margin: 0 0 0.5rem; font-size: 2rem; }
/* Sign out, pushed to the far right of the Your games header. Wraps rather
   than crushes the heading on a narrow screen. */
.manage-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
.manage-head h1 { margin-bottom: 0; }
.handle-form { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; flex-wrap: wrap; }
.handle-form label { color: var(--text-muted); }
.handle-form .handle-at { font-family: var(--font-mono); color: var(--text-label); }

/* ---------- copy line ---------- */

/* A value meant to be copied: mono text and a Copy button in one box. The
   text scrolls sideways rather than breaking, because a URL or a token split
   across lines is a URL somebody copies half of. Used for the new token, the
   clone command on the game page and the recipe on the account page. */
.copy-line {
    display: flex; align-items: stretch; min-width: 0;
    border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-code);
    overflow: hidden;
}
.copy-line code {
    flex: 1 1 auto; min-width: 0; padding: 0.55rem 0.8rem;
    font-family: var(--font-mono); font-size: 0.8rem; line-height: 1.5;
    white-space: nowrap; overflow-x: auto; scrollbar-width: thin; user-select: all;
}
.copy-btn {
    flex: none; display: inline-flex; align-items: center;
    padding: 0 0.85rem; border: 0; border-left: 1px solid var(--border);
    background: none; color: var(--text-muted); cursor: pointer;
    font-family: var(--font-sans); font-size: 0.78rem;
}
.copy-btn:hover { background: var(--text); color: var(--bg); }
.copy-btn.done, .copy-btn.done:hover { background: none; color: var(--link); }

/* ---------- access tokens ---------- */

.tokens-lede { max-width: var(--measure); color: var(--text-muted); font-size: 0.95rem; margin-bottom: 1.4rem; }

.token-new { padding: 0.9rem 1rem 1rem; border: 1px solid var(--link); border-radius: var(--radius); margin-bottom: 1.4rem; }
.token-new-head { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.6rem; font-size: 0.88rem; }
.token-new-head b { color: var(--link); }
.token-new-head span { color: var(--text-muted); }

/* The same row as the games list above it, so the page reads as one list of
   things this account holds after another. */
.token-list { display: flex; flex-direction: column; border-top: 1px solid var(--border); }
.token-row {
    display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
    padding: 0.85rem 0; border-bottom: 1px solid var(--border);
}
.token-what { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.token-name { font-size: 1rem; font-weight: 600; display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.token-prefix { font-family: var(--font-mono); font-size: 0.78rem; font-weight: 400; color: var(--text-muted); }
.token-row form { flex: none; }
.token-empty { padding: 0.9rem 0; border-bottom: 1px solid var(--border); font-size: 0.88rem; color: var(--text-muted); }

.token-make { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-top: 1.1rem; }
.token-make label { font-size: 0.85rem; color: var(--text-muted); }
.token-make input {
    font-family: var(--font-mono); font-size: 0.85rem; color: var(--text);
    background: var(--bg-code); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 0.4rem 0.6rem; width: 13rem;
}
.token-make input:focus { outline: none; border-color: var(--text); }

.token-how { margin-top: 2rem; }
.token-how h3 {
    font-size: 0.72rem; color: var(--text-label); font-weight: 400;
    letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 0.7rem;
}
/* What the terminal will actually show, in the order it shows it. */
.token-shell {
    margin: 0; padding: 0.8rem 1rem; overflow-x: auto; scrollbar-width: thin;
    font-family: var(--font-mono); font-size: 0.78rem; line-height: 1.7;
    background: var(--bg-code); border: 1px solid var(--border); border-radius: var(--radius);
    color: var(--text);
}
.token-shell .sh-p { color: var(--text-label); user-select: none; }
.token-shell .sh-c { color: var(--text-muted); }
.token-shell .sh-v { color: var(--link); }
.token-how p { margin-top: 0.7rem; font-size: 0.85rem; color: var(--text-muted); line-height: 1.55; }
.handle-form input {
    font-family: var(--font-mono); font-size: 0.85rem; color: var(--text);
    background: var(--bg-code); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 0.35rem 0.5rem; width: 12rem;
}
.empty-note { color: var(--text-muted); padding: 2rem 0 4rem; }

/* ---------- filters ---------- */

.bar {
    display: flex; align-items: center; gap: 0.5rem;
    /* No rule. This border separated the filters from the site header, and with
       the header gone it only drew a line under the wordmark, which is the bar
       this redesign removed reappearing as a hairline. */
    padding: 0.4rem 0 1.1rem; flex-wrap: wrap;
}
.chip {
    font-family: var(--font-sans); font-size: 0.85rem;
    padding: 0.35rem 0.75rem; border-radius: var(--radius); cursor: pointer;
    background: none; border: 1px solid transparent; color: var(--text-muted);
}
.chip:hover { color: var(--text); background: var(--bg-code); }
.chip[aria-pressed="true"] { border-color: var(--border); color: var(--text); background: var(--bg-code); }
.bar-end { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
.bar .count { font-size: 0.85rem; color: var(--text-muted); }

/* ---------- game grid ---------- */

.grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 1.5rem; padding: 0.6rem 0 3.5rem;
}
.card { display: flex; flex-direction: column; gap: 0.85rem; cursor: pointer; text-decoration: none; color: inherit; }
.card .stage { transition: border-color 0.15s ease; }
.card:hover .stage { border-color: var(--text-muted); }

.card-title { display: flex; align-items: baseline; gap: 0.5rem; }
.card-name { font-family: var(--font-serif); font-size: 1.15rem; color: var(--text); }
.card-by { font-size: 0.85rem; color: var(--text-muted); }
/* What the sketch is, between its name and its numbers. Clamped to two lines so
   a long first sentence cannot make one card taller than the row it sits in. */
.card-desc {
    margin: 0.3rem 0 0.35rem; font-size: 0.87rem; line-height: 1.45;
    color: var(--text-muted);
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    overflow: hidden;
}
.card-sub {
    display: flex; align-items: baseline; gap: 0.7rem;
    font-size: 0.82rem; color: var(--text-muted);
}
.card-sub .sz { font-family: var(--font-mono); color: var(--text); }
.card-sub .ph { opacity: 0.75; }

/* ---------- the games are the only colour on the page ---------- */

/* One cell size drives every board. A card wants 16px cells, the play stage
   wants 68px, and scaling the card up with a transform magnifies the corner
   radii and the gaps along with it, which turns crisp squares into blobs. */
:root { --cell: 16px; --cell-gap: 3px; }

.mini { display: grid; gap: var(--cell-gap); }
.mini-row { display: flex; gap: var(--cell-gap); }
.tile { width: var(--cell); height: var(--cell); border-radius: calc(var(--cell) * 0.13); background: var(--board-empty); }
.tile.hit { background: #538D4E; }
.tile.near { background: #B59F3B; }
.tile.miss { background: #86888a; }

.blocks { display: grid; grid-template-columns: repeat(8, calc(var(--cell) * 0.75)); gap: calc(var(--cell-gap) * 0.7); }
.blk { height: calc(var(--cell) * 0.75); border-radius: calc(var(--cell) * 0.07); background: transparent; }
.blk.e { background: var(--board-empty); }
.blk.i { background: #2DC2D9; } .blk.o { background: #E0BB2E; }
.blk.t { background: #A052C4; } .blk.s { background: #4ABA5C; }
.blk.z { background: #D44242; } .blk.j { background: #386ED2; }
.blk.l { background: #DE802A; }

.sudoku { display: grid; grid-template-columns: repeat(9, calc(var(--cell) * 0.875)); gap: calc(var(--cell-gap) * 0.34); background: var(--board-line); padding: 1px; border-radius: var(--radius); }
.sq { height: calc(var(--cell) * 0.875); background: var(--bg); display: grid; place-items: center; font-family: var(--font-mono); font-size: calc(var(--cell) * 0.53); color: var(--text-muted); }
.sq.given { color: var(--text); }

.pong { position: relative; width: 200px; height: 112px; border: 1px solid var(--board-line); border-radius: var(--radius); }
.pong i { position: absolute; background: var(--text-muted); border-radius: var(--radius); }
.pong .pad { width: 5px; height: 32px; }
.pong .ball { width: 7px; height: 7px; border-radius: 50%; background: var(--text); }
.pong .net { left: 50%; top: 6px; bottom: 6px; width: 1px; background: var(--board-line); border-radius: 0; }

.ctx { display: grid; gap: 5px; width: 200px; }
.ctx-row { display: flex; align-items: center; gap: 7px; }
.ctx-bar { height: 12px; border-radius: var(--radius); }
.ctx-n { font-family: var(--font-mono); font-size: 9px; color: var(--text-muted); width: 24px; text-align: right; }

/* ---------- game page ---------- */

.game-page { display: none; }
/* Under the stage, not above it. The game is the first thing on screen and
   everything about it reads below, which is the arrangement every video page
   converged on for the same reason. */
.game-head { padding: 1rem 0 0.9rem; border-bottom: 1px solid var(--border); }
.game-head h2 {
    font-family: var(--font-serif); font-weight: 400;
    font-size: 1.9rem; line-height: 1.2; color: var(--text); margin-bottom: 0.5rem;
}
/* The author's sentences, under the title and above the row of controls.
   CAPPED AT A MEASURE, NOT RUN TO THE COLUMN. The column is 953px on a wide
   screen, which is about 130 characters a line and roughly twice what anybody
   reads without losing their place returning to the left edge. The rest of
   this page is cards and numbers, which are happy at any width; this is the
   one piece of prose on it. */
.game-desc {
    margin: 0 0 0.9rem; max-width: 68ch;
    color: var(--text-muted); font-size: 1rem; line-height: 1.6;
}
.game-byline { display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap; }
.by { font-size: 0.92rem; color: var(--text-muted); }
.by b { color: var(--text); font-weight: 600; }
.game-actions { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
/* Sized to sit in the row rather than to the icon's own 24px, which came out
   7px taller than the text buttons beside it and read as a different control.
   Scaling the whole SVG is what lucide itself does for a smaller size: the
   viewBox and stroke-width stay put, so the weight stays right. */
/* .cta-button.small also sets padding and outranks a lone .icon-button, so
   this has to carry both classes or the padding here does nothing at all,
   which is what it did until the rendered heights were measured. */
.cta-button.icon-button { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0.6rem; }
.icon-button svg { display: block; width: 20px; height: 20px; }

/* The stage and what is about it, in two columns.
   No width of its own: main.page.wide is the container and carries both the cap
   and the gutter, the same arrangement every other page has. It used to bound
   itself to 1700px, which was harmless while the page was full bleed and became
   a second cap fighting the column's once the column arrived. */
.play-layout {
    display: grid; grid-template-columns: minmax(0, 1fr) 380px;
    gap: 2rem; align-items: start;
    padding: 1.25rem 0 3rem;
}
/* minmax(0, 1fr), not 1fr. A bare 1fr is minmax(auto, 1fr), so the column
   refuses to go below its content, and anything wide inside it drags the whole
   layout viewport open on a phone, which is why the page used to load correctly
   and then jump wider. */
@media (max-width: 1040px) {
    .play-layout { grid-template-columns: minmax(0, 1fr); gap: 1.6rem; }
}

/* Fills its column, so the stage, the title beneath it and the source tabs all
   share one left edge. Fitting the screen is handled by the layout instead:
   the title sits below the stage, which frees the vertical space that made it
   overflow, and the column is bounded so it cannot run away on a wide monitor. */
/* The editor pane's version, pushed to the right of its bar.
   Only the editor. A player opening a game has no use for the container
   version and no way to act on it; an author editing one is watching the pane
   it describes. The game page briefly carried the same reading in a bar of its
   own, and that bar existed for nothing else, so it went with it and the stage
   keeps its own rounded top again. */
.ed-runtime {
    margin-left: auto;
    font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-label);
    white-space: nowrap;
}
/* MARK: the player
   ------------------------------------------------------------------------
   A PLAYER OF ONE SHAPE, AND THE SKETCH FILLS IT.

   Sai, 17 Sep 2026: the player should be a good rect like a video page. A
   sketch has no size of its own (it gets the window it is given and lays
   itself out from useStage), so every sketch page is the same page.

   The cap is a max-width rather than a max-height, and that is not a style
   choice: aspect-ratio yields to max-height, so capping the height directly
   makes a short window silently stretch the box wider than 16:9. Expressing
   the same ceiling along the other axis keeps the shape exact at every window
   size. It bites only on a short one: at 1440x900 the column gives 953x536,
   well inside 78vh. */
.play-stage {
    aspect-ratio: 16 / 9;
    max-width: calc(78vh * 16 / 9);
    margin-inline: auto;
    /* Dark, because this is a player: a light surround turns emissive art into
       a boxed picture of itself, which is the argument the catalog's palette
       was chosen on. The runtime paints the same colour behind the game. */
    background: #14181d;
}

/* THE COVER FILLS ITS BOX, RATHER THAN BEING CENTRED INSIDE IT.
   .gen-cover, the drawn fallback, is "width: 62%", and a percentage needs a
   parent with a width to be a percentage OF. Centred as a grid item, .cover is
   sized by its content, its content is sized from .cover, and the pair resolve
   to nothing: the art vanishes and the stage shows an empty box with a Play
   button on it. Giving it a box breaks the cycle, and the centring moves
   inside, where there is a width to centre against.

   A descendant selector from when two pages nested it differently. The embed
   is the only one left, where it is a direct child of the stage, and the
   selector still reaches it.

   Gallery cards do not render this wrapper, so they are a different path and
   are untouched: the class appears in exactly two components. */
.stage .cover {
    position: absolute; inset: 0;
    display: grid; place-items: center;
}
/* The hero is nearly as wide as the play stage, so it needs the same treatment:
   a card-sized board floating in it reads as a rendering bug, which is exactly
   how it looked when only .play-stage carried these. */

/* The covers are drawn at the size a gallery card wants, about 92px across. The
   play stage is an order of magnitude wider, so it is scaled up into a backdrop
   rather than left as a postage stamp the button then sits on top of. Chunky at
   this magnification, which suits art made of squares.

   This is the fallback, not a placeholder waiting for something better: a
   sketch whose author wants no cover gets it, and that is a supported
   choice. */
.play-stage .cover { --cell: 68px; --cell-gap: 11px; opacity: 0.95; }
/* These two are laid out at fixed pixel widths rather than in cells, so they
   are the one place a transform is still the right tool. */
.play-stage .cover .pong, .play-stage .cover .ctx { transform: scale(3.2); }
@media (max-width: 700px) {
    .play-stage .cover { --cell: 34px; --cell-gap: 6px; }
    .play-stage .cover .pong, .play-stage .cover .ctx { transform: scale(1.6); }
}

/* The whole stage is the button, so the click target is 1288x805 rather than a
   64px circle. The scrim is light on purpose: the cover art is the most
   appealing thing on the page and hiding it behind a wash to make room for a
   play glyph trades the good part for the obvious part. */
.play-stage .overlay {
    position: absolute; inset: 0; z-index: 3;
    display: grid; place-items: center; gap: 0;
    border: 0; padding: 0; cursor: pointer; width: 100%; height: 100%;
    /* Nothing behind the button. The halo that used to sit here smudged the
       cover to make a caption legible, and the caption is gone; the button
       carries its own background. */
    background: none;
    transition: opacity 0.18s ease;
}
.play-stage .overlay:hover { opacity: 0.92; }

.play-cta { display: flex; flex-direction: column; align-items: center; gap: 0.85rem; pointer-events: none; }

/* Speaks the site's button idiom (bordered, hover inverts) rather than the
   video-player idiom, and carries the same pixel language as the wordmark. */
.play-chip {
    display: inline-flex; align-items: center; gap: 0.7rem;
    padding: 0.62rem 1.15rem 0.62rem 1rem;
    border: 1px solid var(--text); border-radius: var(--radius);
    background: var(--bg); color: var(--text);
    font-size: 0.95rem; letter-spacing: 0.18em; text-transform: uppercase;
    transition: background 0.18s ease, color 0.18s ease, transform 0.18s ease;
}
.play-stage .overlay:hover .play-chip { background: var(--text); color: var(--bg); transform: translateY(-1px); }
.play-stage .overlay:focus-visible .play-chip { background: var(--text); color: var(--bg); }

/* Stepped rather than smooth, drawn on a 6x12 grid at crisp edges, so it reads
   as the same hand as the Play wordmark in the header. */
.play-pixel { display: block; width: 12px; height: 24px; fill: currentColor; }

.side { display: flex; flex-direction: column; gap: 1.5rem; }
.panel h3 {
    font-size: 0.78rem; color: var(--text-label); font-weight: 400;
    letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 0.7rem;
}
.kv { display: flex; justify-content: space-between; gap: 1rem; padding: 0.4rem 0; font-size: 0.9rem; border-bottom: 1px solid var(--border); }
.kv:last-child { border-bottom: none; }
.kv span { color: var(--text-muted); }
.kv b { font-family: var(--font-mono); font-weight: 400; color: var(--text); font-variant-numeric: tabular-nums; font-size: 0.85rem; }

.eject {
    display: block; width: 100%; text-align: left; cursor: pointer;
    background: none; color: var(--text);
    border: 1px solid var(--text); border-radius: var(--radius); padding: 1.1rem 1.2rem;
    font-family: var(--font-sans);
}
.eject:hover { background: var(--text); color: var(--bg); }
/* A panel like Lineage above it; the command itself is a .copy-line. */
.clone-note { margin-top: 0.6rem; font-size: 0.82rem; line-height: 1.5; color: var(--text-muted); }
.commit-list { list-style: none; padding: 0; margin: 1rem 0 0; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.85rem; }
.commit-list li { display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap; padding: 0.4rem 0; border-bottom: 1px solid var(--border); }
.commit-list li.live { background: var(--bg-raised, transparent); }
.commit-list code { font-family: var(--font-mono); font-size: 0.78rem; }
.commit-msg { flex: 1 1 12rem; }
.commit-live { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--link); }
.build-error { color: #d0523a; font-size: 0.85rem; }
.eject b { display: block; font-family: var(--font-serif); font-weight: 400; font-size: 1.15rem; margin-bottom: 0.3rem; }
.eject span { font-size: 0.85rem; line-height: 1.5; opacity: 0.8; }

/* ---------- source ---------- */

.tabs::-webkit-scrollbar { display: none; }
.tab[aria-selected="true"] { color: var(--text); background: var(--bg-code); border-color: var(--border); border-bottom-color: var(--bg-code); }
.tab .sz { opacity: 0.65; margin-left: 0.5rem; font-size: 0.75rem; }

.cta-button.on { border-color: var(--text); color: var(--text); }

/* ---------- a page for something not built yet ---------- */

.soon { max-width: var(--measure); padding: 5rem 0 7rem; }
.soon h2 { font-family: var(--font-serif); font-weight: 400; font-size: 2.2rem; margin-bottom: 1rem; }
.soon-what { font-size: 1.05rem; margin-bottom: 1rem; }
.soon-detail { color: var(--text-muted); line-height: 1.7; margin-bottom: 2rem; }

/* ---------- footer ---------- */

footer { border-top: 1px solid var(--border); }
.foot-inner {
    width: 100%; padding: 1.6rem var(--edge) 2.6rem;
    display: flex; gap: 1.5rem; align-items: center; flex-wrap: wrap;
    font-size: 0.85rem; color: var(--text-muted);
}
.foot-inner a { color: var(--text-muted); text-decoration: none; }
.foot-notice { color: var(--text-label); }
.foot-inner a:hover { color: var(--text); }

/* Reads as one of the footer links rather than as a control, because it is a
   preference and not an action on anything. */
.theme-toggle {
    font: inherit; color: var(--text-muted);
    background: none; border: 0; padding: 0; cursor: pointer;
}
.theme-toggle:hover { color: var(--text); }
.theme-toggle:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }

.note-strip {
    border: 1px solid var(--border); border-radius: var(--radius);
    padding: 0.8rem 1rem; margin-top: 1.6rem;
    font-size: 0.88rem; color: var(--text-muted); line-height: 1.55;
}
.note-strip b { color: var(--text); font-weight: 600; }

/* ---------- the playful bit ----------
   The chrome stays calm; the games move. Every animation below is scoped to
   hover (or the easter egg) and every one is switched off under
   prefers-reduced-motion, so the default browsing experience is still. */

@keyframes tile-flip {
    0%   { transform: rotateX(0deg); }
    50%  { transform: rotateX(90deg); }
    100% { transform: rotateX(0deg); }
}
.card:hover .tile, .romp .tile {
    animation: tile-flip 0.55s ease both;
    animation-delay: calc(var(--i, 0) * 55ms);
}

@keyframes piece-drop {
    0%   { transform: translateY(-14px); opacity: 0; }
    70%  { transform: translateY(2px); opacity: 1; }
    100% { transform: translateY(0); opacity: 1; }
}
/* Only the piece still in the air falls; the settled stack stays put. */
.card:hover .blk.t, .romp .blk.t {
    animation: piece-drop 0.5s cubic-bezier(.3,1.4,.6,1) both;
}

@keyframes ball-rally {
    0%   { transform: translate(0, 0); }
    50%  { transform: translate(-72px, 14px); }
    100% { transform: translate(0, 0); }
}
.card:hover .pong .ball, .romp .pong .ball { animation: ball-rally 1.5s ease-in-out infinite; }
@keyframes paddle-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(10px); } }
.card:hover .pong .pad, .romp .pong .pad { animation: paddle-bob 1.5s ease-in-out infinite; }

@keyframes bar-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.card:hover .ctx-bar, .romp .ctx-bar {
    transform-origin: left; animation: bar-grow 0.5s ease both;
    animation-delay: calc(var(--i, 0) * 70ms);
}

@keyframes given-in { from { opacity: 0; transform: scale(.6); } to { opacity: 1; transform: scale(1); } }
.card:hover .sq.given, .romp .sq.given {
    animation: given-in 0.4s ease both;
    animation-delay: calc(var(--i, 0) * 9ms);
}

/* ---------- surprise me ---------- */

/* A browse mode rather than a trinket in the nav: labelled, sitting with the
   filters, and the fastest way into a catalog too big to scan. No spin. */
.surprise {
    font-family: var(--font-sans); font-size: 0.85rem;
    padding: 0.35rem 0.75rem; border-radius: var(--radius); cursor: pointer;
    background: none; border: 1px solid var(--border); color: var(--text-muted);
    margin-left: 0.9rem;
}
.surprise:hover { color: var(--text); border-color: var(--text-muted); }

/* ---------- toast, for the easter egg ---------- */

/* Hidden until it has something to say. The transform alone does not clear the
   viewport: an empty toast is 19px tall, 120% of that is 23px, and it sits
   25.6px above the bottom, leaving a black sliver parked on every page. */
.toast {
    position: fixed; left: 50%; bottom: 1.6rem; transform: translateX(-50%) translateY(120%);
    background: var(--text); color: var(--bg);
    padding: 0.6rem 1.1rem; border-radius: var(--radius); font-size: 0.88rem;
    transition: transform 0.28s cubic-bezier(.2,1.3,.4,1), opacity 0.28s ease, visibility 0.28s;
    z-index: 60; pointer-events: none;
    opacity: 0; visibility: hidden;
}
.toast.up { transform: translateX(-50%) translateY(0); opacity: 1; visibility: visible; }

/* Sign-in: one narrow column, because the form is the whole page. */
.signin { max-width: 26rem; margin: 4rem auto; padding: 0 var(--edge); }
.signin h1 { font-family: var(--font-serif); font-size: 1.8rem; margin: 0 0 0.4rem; }
.signin-lede { color: var(--text-muted); font-size: 0.92rem; margin: 0 0 2rem; }
.signin label {
    display: block; font-size: 0.78rem; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--text-label); margin-bottom: 0.4rem;
}
.signin input {
    width: 100%; box-sizing: border-box; padding: 0.7rem 0.85rem; margin-bottom: 1.1rem;
    background: var(--bg-code); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--radius);
    font-family: var(--font-sans); font-size: 1rem;
}
/* The code is read off a phone and typed one glyph at a time, so it gets the
   monospace face and room between characters. */
.signin #code { font-family: var(--font-mono); letter-spacing: 0.35em; font-size: 1.15rem; }
.signin input:focus-visible { outline: 2px solid var(--link); outline-offset: 1px; }
.signin button {
    width: 100%; padding: 0.72rem 1rem; border: 0; border-radius: var(--radius); cursor: pointer;
    background: var(--text); color: var(--bg);
    font-family: var(--font-sans); font-size: 0.95rem;
}
.signin button:disabled { opacity: 0.55; cursor: default; }
.signin-hint { color: var(--text-muted); font-size: 0.82rem; margin: -0.5rem 0 1.1rem; }
/* Reserves no space until it says something, so the form does not jump. */
.signin-error:not(:empty) { color: #c0392b; font-size: 0.86rem; margin: 0.9rem 0 0; }

/* Editor: files, code, preview. Fills the viewport under the header, because a
   page that scrolls while you are typing in it is the wrong shape. */
.ed {
    display: grid;
    grid-template-columns:
        var(--ed-files, 190px) 5px minmax(160px, 1fr) 5px var(--ed-preview, 1fr);
    /* The header height is measured and written here rather than hardcoded.
       A guess survives until the first browser zoom, where the header wraps or
       rounds differently and the editor either overflows the viewport or leaves
       a gap under it. */
    height: calc(100dvh - var(--ed-top, 64px));
    border-top: 1px solid var(--border);
}
/* Each pane names its own column, so hiding one cannot let another slide into
   the gap it left. Without this, display:none on the code pane re-flows the
   preview into the code's track and the files sidebar keeps the width the
   preview was given. */
.ed-files { grid-column: 1; }
#ed-gutter-files { grid-column: 2; }
.ed-main { grid-column: 3; }
#ed-gutter-preview { grid-column: 4; }
.ed-preview { grid-column: 5; }

/* A pane away, and what is left across the rest. Each hidden pane takes its
   own gutter with it, because a resize handle for something that is not on
   screen is a 5px strip that does nothing when dragged. */
.ed.ed-nocode .ed-main, .ed.ed-nocode #ed-gutter-preview { display: none; }
.ed.ed-nocode .ed-preview { border-left: 0; }
.ed.ed-nofiles .ed-files, .ed.ed-nofiles #ed-gutter-files { display: none; }

/* The tracks stay at five so the named columns above keep meaning what they
   say, and the surviving panes read the same --ed-files / --ed-preview they
   always did: bringing one back restores the width it was dragged to rather
   than a default.

   Only where there ARE columns. Below 900px the editor is one stacked column,
   and a five-track template out here would beat that rule on specificity and
   put the panes back side by side on a phone. display:none above is the whole
   of it at that width, which is what hiding a pane means there anyway. */
@media (min-width: 901px) {
    .ed.ed-nocode { grid-template-columns: var(--ed-files, 190px) 5px 0 0 minmax(0, 1fr); }
    .ed.ed-nofiles { grid-template-columns: 0 0 minmax(160px, 1fr) 5px var(--ed-preview, 1fr); }
    /* Nothing left but the sketch. */
    .ed.ed-nofiles.ed-nocode { grid-template-columns: 0 0 0 0 minmax(0, 1fr); }
}

.ed-gutter {
    cursor: col-resize; background: var(--border);
    /* Widened by its own margin so the grab area is bigger than the line. */
    border-left: 2px solid transparent; border-right: 2px solid transparent;
    background-clip: padding-box;
}
.ed-gutter:hover, .ed-gutter.dragging { background: var(--text-muted); }
/* Pointer capture keeps the drag alive over the iframe, which would otherwise
   swallow the events the moment the pointer crossed into it. */
.ed.dragging { user-select: none; }
.ed.dragging iframe { pointer-events: none; }
.ed-files { border-right: 1px solid var(--border); overflow: auto; min-width: 0; }
/* All three pane headers share one height. They used to be set by whatever was
   inside them, so the file list came out at 35, the preview at 42 and the
   editor at 48 because a button is taller than a line of text. */
.ed { --ed-bar-h: 42px; }
.ed-files-head {
    display: flex; align-items: center; justify-content: space-between;
    box-sizing: border-box; height: var(--ed-bar-h); padding: 0 0.5rem 0 0.9rem;
    font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-label); border-bottom: 1px solid var(--border);
}
.ed-files-head button {
    border: 0; background: none; cursor: pointer; color: var(--text-muted);
    font-size: 1rem; line-height: 1; padding: 0 0.3rem;
    display: inline-flex; align-items: center;
}
.ed-files-head button:hover { color: var(--text); }
/* Together at the right. The header is space-between, which is right for one
   action and wrong for two: it put the Plus in the middle of the pane. */
.ed-head-actions { display: flex; align-items: center; gap: 0.15rem; }
.ed-act { width: 15px; height: 15px; display: block; }
.ed-files ul { list-style: none; margin: 0; padding: 0.4rem 0; }
.ed-files li { display: flex; align-items: center; }
.ed-files li.on { background: var(--bg-code); }
.ed-file {
    flex: 1; min-width: 0; text-align: left; border: 0; background: none; cursor: pointer;
    /* No padding shorthand on either row type: the left is set by the indent
       rule above, and a shorthand here would silently win by being later. */
    padding-top: 0.34rem; padding-bottom: 0.34rem; padding-right: 0.9rem;
    color: var(--text);
    font-family: var(--font-mono); font-size: 0.8rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ed-del {
    border: 0; background: none; cursor: pointer; color: var(--text-muted);
    padding: 0 0.6rem; font-size: 0.95rem; line-height: 1;
}

/* The overlay, and the one panel every action opens inside it.

   Fixed to the viewport rather than absolute inside .ed, because .ed is a grid
   with its own stacking and an absolutely positioned child of it is clipped by
   whichever pane it starts in. */
.ed-overlay {
    position: fixed; inset: 0; z-index: 40;
    display: flex; align-items: center; justify-content: center;
    background: rgba(0, 0, 0, 0.45); padding: 1rem;
}
.ed-overlay[hidden] { display: none; }
.ed-panel {
    background: var(--bg); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--radius);
    width: min(30rem, 100%); max-height: min(32rem, 100%);
    display: flex; flex-direction: column; overflow: hidden;
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
}
.ed-panel-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 0.7rem 0.9rem; border-bottom: 1px solid var(--border);
}
.ed-panel-head h2 { margin: 0; font-size: 0.9rem; font-weight: 600; }
.ed-panel-x {
    border: 0; background: none; cursor: pointer; color: var(--text-muted);
    font-size: 1.1rem; line-height: 1; padding: 0 0.2rem;
}
.ed-panel-x:hover { color: var(--text); }
/* min-height: 0, or a flex child refuses to shrink below its content and a tall
   panel clips its bottom, Publish button included, instead of scrolling. */
.ed-panel-body { padding: 0.9rem; overflow: auto; flex: 1 1 auto; min-height: 0; }
.ed-panel-row { display: block; margin-bottom: 0.7rem; }
.ed-panel-label {
    display: block; font-size: 0.7rem; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--text-label); margin-bottom: 0.3rem;
}
.ed-panel-input {
    width: 100%; box-sizing: border-box;
    padding: 0.45rem 0.55rem; color: var(--text);
    background: var(--bg-code); border: 1px solid var(--border); border-radius: var(--radius);
    font-family: var(--font-mono); font-size: 0.82rem;
}
.ed-panel-input:focus { outline: 2px solid var(--link); outline-offset: -1px; }
/* The Stats panel's rows. Label left, value right, the shape the sketch's old
   sidebar used, so the numbers read the same as they always did. */
.ed-stats { display: grid; gap: 0.45rem; margin: 0 0 0.9rem; }
.ed-stat { display: flex; align-items: baseline; justify-content: space-between; gap: 1.2rem; font-size: 0.86rem; }
.ed-stat span { color: var(--text-muted); }
.ed-stat b { font-weight: 600; text-align: right; }

.ed-panel-hint { font-size: 0.72rem; color: var(--text-label); margin: 0.3rem 0 0; }
/* A hint that explains one row sits flush under it; the next row starts a new
   question and needs air, or "playable by anyone" reads as the label of the
   source setting below it. */
.ed-panel-hint + .ed-panel-row { margin-top: 1rem; }
.ed-panel-error {
    margin: 0.5rem 0 0; padding: 0.45rem 0.55rem; border-radius: var(--radius);
    font-size: 0.78rem; color: #fff; background: #b3261e;
}
.ed-panel-error[hidden] { display: none; }
.ed-panel-group { margin: 1rem 0 0.4rem; }
.ed-panel-chip {
    border: 1px solid var(--border); border-radius: 999px; cursor: pointer;
    background: none; color: var(--text); padding: 0.25rem 0.7rem;
    font-family: var(--font-mono); font-size: 0.75rem; margin: 0 0.35rem 0.35rem 0;
}
.ed-panel-chip:hover { border-color: var(--link); color: var(--link); }
/* Stands in for the two eject links when the author has turned copying off,
   so the panel says why rather than going quietly empty. */
.eject-off {
    margin: 0; padding: 0.7rem 0.8rem; border-radius: var(--radius);
    border: 1px dashed var(--border); color: var(--text-muted);
    font-size: 0.82rem; line-height: 1.45;
}
.ed-panel-check {
    display: flex; align-items: center; gap: 0.45rem;
    margin-top: 1rem; cursor: pointer; font-size: 0.85rem;
}
.ed-panel-check input { margin: 0; }
.ed-panel-change {
    margin: 0.25rem 0 0; font-size: 0.78rem; color: var(--text-muted);
    font-family: var(--font-mono);
}
.ed-panel-change b { color: var(--text); font-weight: 600; }
.ed-panel-warn { color: #b45309; }
.ed-panel-actions { display: flex; justify-content: flex-end; margin-top: 1rem; }
.ed-panel-go {
    border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer;
    background: var(--bg-code); color: var(--text);
    padding: 0.4rem 0.9rem; font-size: 0.8rem;
}
.ed-panel-go:hover { border-color: var(--link); color: var(--link); }

/* The row of a file the Cover button has just written.
   A mark on the row rather than a toast, because the file is visibly there
   and a toast would be a second thing to read. It fades out on its own: the
   point is to say WHICH row is new in a sorted list, not to be dismissed. */
@keyframes ed-new-row {
    from { background: color-mix(in srgb, var(--link) 34%, transparent); }
    to { background: transparent; }
}
.ed-files li.ed-new { animation: ed-new-row 2s ease-out 1; border-radius: var(--radius); }
@media (prefers-reduced-motion: reduce) {
    .ed-files li.ed-new { animation-duration: 0.01s; }
}

/* The upload panel's target. Paste lands here, which is why it is focusable. */
.ed-drop {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
    gap: 0.35rem; padding: 1.6rem 1rem; text-align: center;
    border: 1px dashed var(--border); border-radius: var(--radius);
    color: var(--text-muted); font-size: 0.82rem;
}
.ed-drop:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
.ed-drop.ed-dropping { border-color: var(--link); color: var(--link); }
.ed-staged-row {
    display: flex; align-items: center; gap: 0.4rem; margin-top: 0.5rem;
}
.ed-staged-row .ed-panel-input { flex: 1; }

/* Looking at an asset, in the cell the code usually has. */
.ed-look { display: grid; grid-template-rows: auto minmax(0, 1fr); min-width: 0; }
.ed-look[hidden] { display: none; }
.ed-look-bar {
    display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
    box-sizing: border-box; height: var(--ed-bar-h); padding: 0 0.9rem;
    border-bottom: 1px solid var(--border);
}
.ed-look-name {
    font-family: var(--font-mono); font-size: 0.8rem; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ed-look-body {
    display: flex; align-items: center; justify-content: center;
    padding: 1rem; overflow: auto; background: var(--bg-code);
}
/* Contained rather than stretched: a sprite sheet blown up to the pane tells
   you nothing about the sprite, and a 16px icon blown up tells you less. */
.ed-look-img { max-width: 100%; max-height: 100%; object-fit: contain; }
.ed-look-media { max-width: 100%; }
.ed-look-note { color: var(--text-label); font-size: 0.82rem; margin: 0; }

/* The tree. Source files and assets are one list, because they are one game.

   A folder row and the rows beneath it have to STACK, and every other li in
   this pane is a flex ROW (a name, then a size, then a delete). So ed-dir opts
   out of that rather than the rule being weakened for everyone, and the nested
   ul drops the outer list's padding, which would otherwise compound at every
   level and march the tree off the right edge by about depth three.

   Indentation is an inline padding-left computed from depth rather than a
   margin on the nested ul, so the highlight on the open file still spans the
   full width of the pane at any depth. */
.ed-files li.ed-dir { display: block; }
.ed-files .ed-dir ul { padding: 0; }

/* Depth arrives as --depth on the row and the arithmetic lives here, so the
   twist's width is written down once, beside the twist. A leaf carries no
   twist, so without --twist-w its name would sit a twist to the LEFT of a
   sibling folder's name, which is the one thing an indented tree has to get
   right. */
.ed-files li { --depth: 0; }
.ed { --ed-twist-w: 0.9rem; }
.ed-folder { padding-left: calc(var(--depth) * 12px + 6px); }
.ed-file, .ed-asset { padding-left: calc(var(--depth) * 12px + 6px + var(--ed-twist-w)); }
.ed-folder {
    display: flex; align-items: center; gap: 0.3rem; width: 100%;
    text-align: left; border: 0; background: none; cursor: pointer;
    padding-top: 0.3rem; padding-bottom: 0.3rem; padding-right: 0.9rem;
    color: var(--text-muted);
    font-family: var(--font-mono); font-size: 0.78rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ed-folder:hover { color: var(--text); }
.ed-twist {
    display: inline-block; width: 0.6rem; flex: none;
    font-size: 0.55rem; color: var(--text-label);
}
/* The kind glyph. Stroked on currentColor so it inherits the row's colour and
   every hover and muted state defined for the text applies to it too. */
.ed-kind {
    width: 13px; height: 13px; flex: none;
    fill: none; stroke: currentColor; stroke-width: 1.25;
    stroke-linecap: round; stroke-linejoin: round;
    opacity: 0.55;
}
.ed-file, .ed-asset { display: flex; align-items: center; gap: 0.4rem; }
.ed-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Assets: the rows that are bytes rather than text. They cannot be opened, so
   clicking one copies the assetUrl call instead, and the size is shown because
   it is the number an author actually wants: a game has a budget and a sprite
   sheet is most of it. */
.ed-asset {
    flex: 1; min-width: 0; text-align: left; border: 0; background: none; cursor: pointer;
    padding-top: 0.34rem; padding-bottom: 0.34rem; padding-right: 0;
    color: var(--text);
    font-family: var(--font-mono); font-size: 0.8rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* An asset that exists only in this tab. The game cannot load it until a
   publish, and the editor previewing it happily is what makes that confusing. */
.ed-asset-pending {
    font-size: 0.62rem; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--bg); background: var(--text-label);
    border-radius: var(--radius); padding: 0.05rem 0.25rem; white-space: nowrap;
}
.ed-asset-size {
    font-size: 0.68rem; color: var(--text-label); white-space: nowrap;
    font-variant-numeric: tabular-nums; padding-left: 0.4rem;
}
.ed-files li.ed-empty {
    padding: 0.34rem 0.9rem; font-size: 0.75rem; color: var(--text-label);
}
/* min-width on every one of these, not just the columns.
   A grid item defaults to min-width:auto, which means it refuses to shrink
   below its own content. Monaco reports the width it last laid out at, so
   dragging the divider left moved the track but left the editor box where it
   was, and its content and scrollbar drew over the preview. Growing looked
   fine, because nothing has to shrink for that. */
.ed-main { display: grid; grid-template-rows: auto minmax(0, 1fr); min-width: 0; }
/* Bar, game, seam, console. The console row is a variable so it can be dragged
   and remembered like the two columns already are. */
.ed-preview {
    display: grid; min-width: 0;
    grid-template-rows: auto minmax(0, 1fr) 5px var(--ed-console, 140px);
}
/* The console away, and its seam with it. Two rows rather than four with the
   last two at zero, so the frame is the last thing in the column and nothing
   can sit in a track below it. --ed-console is left as it was dragged, which
   is what brings the console back at the same height. Rows, not columns, so
   this holds at every width, the stacked phone layout included. */
.ed.ed-noconsole .ed-console, .ed.ed-noconsole #ed-gutter-console { display: none; }
.ed.ed-noconsole .ed-preview { grid-template-rows: auto minmax(0, 1fr); }

/* The editor's own header. Short: every row here is taken from the code. */
header.chrome {
    border-bottom: 1px solid var(--border); background: var(--bg);
    position: sticky; top: 0; z-index: 20;
    /* Full bleed, unlike the site header. The bare header rule sets a reading
       column's padding of up to 3rem, and the panes below this one start hard
       against the window, so inheriting it left the chrome indented from
       everything it sits above. The small inset this row wants lives on
       .chrome-inner, where the pane toolbars keep theirs. */
    padding: 0;
}
.chrome-inner {
    display: flex; align-items: center; gap: 0.7rem;
    height: 38px; padding: 0 0.7rem;
}
.chrome-inner .logo.small { display: flex; align-items: center; color: var(--brand); }
.chrome-inner .logo.small .logo-mark { width: 18px; height: 18px; }
.chrome-title {
    font-weight: 600; font-size: 0.9rem; color: var(--text); text-decoration: none;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 40%;
}
.chrome-title:hover { color: var(--link); text-decoration: underline; }
.chrome-meta {
    font-size: 0.76rem; color: var(--text-label); white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
}
/* The pane toggles. They take the auto margin that used to push the nav over,
   so the two groups stay together at the right end with the nav after them:
   two autos would split the free space and land the toggles in the middle of
   the row, pointing at nothing. */
.chrome-views { margin-left: auto; display: flex; align-items: center; gap: 4px; }
.chrome-nav { display: flex; align-items: center; gap: 0.9rem; margin-left: 0.9rem; }
.chrome-nav a { font-size: 0.82rem; color: var(--text-muted); text-decoration: none; }
.chrome-nav a:hover { color: var(--text); }
/* The workspace fills what is left of the window rather than flowing. */
main.workspace { display: block; }

/* The console. One place for everything the editor says, with a history. */
.ed-console {
    display: grid; grid-template-rows: auto minmax(0, 1fr);
    min-height: 0; border-top: 1px solid var(--border); background: var(--bg);
}
.ed-console-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 0 0.7rem; height: 26px; border-bottom: 1px solid var(--border);
    font-size: 0.68rem; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-label);
}
.ed-console-lines {
    list-style: none; margin: 0; padding: 0.3rem 0; overflow: auto;
    font-family: var(--font-mono); font-size: 0.75rem; line-height: 1.5;
}
.ed-line { display: flex; gap: 0.5rem; padding: 0.05rem 0.7rem; }
.ed-line-time { color: var(--text-label); flex: none; font-variant-numeric: tabular-nums; }
.ed-line-text { white-space: pre-wrap; word-break: break-word; min-width: 0; }
.ed-line-warn .ed-line-text { color: #b45309; }
.ed-line-error .ed-line-text { color: #b3261e; }
/* How many times the line above repeated. Beside the text and subordinate to
   it: the count is a fact about the line, not a second line. */
.ed-line-count {
    flex: none; align-self: flex-start; margin-left: 0.15rem;
    padding: 0 0.32rem; border-radius: 999px;
    background: var(--bg-code); color: var(--text-label);
    font-size: 0.68rem; font-variant-numeric: tabular-nums;
}
/* A horizontal seam, for the console. The column gutters are vertical. */
.ed-gutter.row { cursor: row-resize; width: auto; height: 5px; }
.ed-preview { border-left: 1px solid var(--border); }
.ed-bar {
    display: flex; align-items: center; gap: 0.8rem;
    box-sizing: border-box; height: var(--ed-bar-h); padding: 0 0.9rem;
    border-bottom: 1px solid var(--border);
}
/* Sized to sit inside the bar rather than to define it. */
.ed-bar .cta-button.small { height: 28px; padding: 0 0.7rem; font-size: 0.8rem; }

/* Font size. A magnifier says "zoom" without a word of explanation, and the
   pair reads as one control rather than two unrelated buttons. */
/* One square button, and everything on these bars is one: Run, Cover, Fork,
   Eject, Stats, the two pane toggles and fullscreen. Same border, same radius,
   same muted until hovered, so a row of them reads as a row rather than as
   eight decisions. The word each one used to carry is in its title and its
   aria-label, which is where an icon button has to keep its name. */
.ed-icon-button {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; padding: 0; cursor: pointer;
    border: 1px solid var(--border); border-radius: var(--radius);
    background: none; color: var(--text-muted);
    font: inherit;
}
.ed-icon-button:hover { border-color: var(--text); color: var(--text); }
.ed-icon-button:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
.ed-icon-button svg { width: 15px; height: 15px; display: block; }
/* Eject is an anchor wearing the same button, because it downloads a file and
   that is what an anchor is for. Only the underline has to be talked out of it. */
a.ed-icon-button { text-decoration: none; }
/* Fork posts, so it needs a form, and a form in a flex row brings a margin and
   a block box that would put the button on its own line. */
.ed-bar-form { margin: 0; display: inline-flex; }
/* Fork, Eject and Stats as one group, tight, so the bar's wide gap falls
   between them and the publishing controls rather than between each pair of
   icons. The same 4px the header's pane toggles use. */
.ed-bar-acts { display: flex; align-items: center; gap: 4px; }

.ed-zoom { margin-left: auto; display: flex; align-items: center; gap: 2px; }
.ed-zoom button {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; padding: 0; cursor: pointer;
    border: 1px solid var(--border); border-radius: var(--radius);
    background: none; color: var(--text-muted);
}
.ed-zoom button:hover { border-color: var(--text); color: var(--text); }
.ed-zoom button:disabled { opacity: 0.4; cursor: default; border-color: var(--border); }
.ed-zoom svg { width: 15px; height: 15px; display: block; }
.ed-zoom-size {
    min-width: 2.2em; text-align: center; font-size: 0.72rem; color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.ed-status { color: var(--text-muted); font-size: 0.78rem; }

/* MARK: the preview bar's controls
   ------------------------------------------------------------------------
   Sai, 17 Sep 2026: this strip was messy. It held a button that disappeared,
   a word that appeared where the button had been, an Unpublish link, a Cover
   button and a status line that anything could write a sentence into, all in
   42 pixels. Four things now, each a fixed width: the audience, the link to
   the public page, Cover, and one word of state. Sentences go to the console.

   The select is dressed as the buttons beside it rather than left native: a
   platform dropdown in a dark toolbar is the one control on the page that does
   not belong to the page. appearance: none removes the chrome, and the arrow is
   an inline SVG in the background so it still reads as a dropdown. */
.ed-bar-select {
    appearance: none; -webkit-appearance: none;
    height: 28px; cursor: pointer;
    border: 1px solid var(--border); border-radius: var(--radius);
    background: none; color: var(--text-muted);
    font: inherit; font-size: 0.8rem;
    padding: 0 1.6rem 0 0.7rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none' stroke='%23888' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 0.55rem center;
}
.ed-bar-select:hover { border-color: var(--text); color: var(--text); }
.ed-bar-select:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
/* Disabled while the request is out, which is the only sign the bar gives that
   a publish is in flight. Publishing is one column and takes a moment. */
.ed-bar-select:disabled { opacity: 0.5; cursor: progress; }
/* The option list is drawn by the platform, so it gets the page's colours
   explicitly or it inherits the toolbar's transparent background and renders
   as black on black wherever the platform does not intervene. */
.ed-bar-select option { background: var(--bg); color: var(--text); }

/* The state, at the right of the bar's own controls: a dot and one word from
   FLAG_WORDS. Pushed away from the buttons so it cannot be read as a label for
   whichever one it happens to sit beside, and never wider than "problem", so
   nothing in this bar moves when it changes. */
.ed-bar-end {
    margin-left: auto; display: inline-flex; align-items: center; gap: 0.9rem;
    min-width: 0;
}
.ed-flag {
    display: inline-flex; align-items: center; gap: 0.4rem;
    font-size: 0.75rem; color: var(--text-label);
}
.ed-flag[hidden] { display: none; }
.ed-flag-dot {
    width: 6px; height: 6px; border-radius: 50%; flex: none;
    background: currentColor;
}
/* Only one of the three is coloured. A problem is the one state worth an
   interruption, and a green tick on every save would be three seconds of
   celebration per thought. */
.ed-flag[data-state="problem"] { color: var(--danger); }
/* Inside .ed-bar-end the group owns the push, so the runtime drops the auto
   margin it carries as a direct child of the bar. Two auto margins in one flex
   row split the free space between them, which would leave the state floating
   in the middle. */
.ed-bar-end .ed-runtime { margin-left: 0; }

/* Cancel and confirm, side by side. The other panels have one button and push
   it right; a question needs both answers and the safe one first. */
.ed-panel-actions.ed-panel-pair { align-items: center; gap: 1rem; }

/* MARK: the panel that asks a question
   ------------------------------------------------------------------------
   Sai, 17 Sep 2026: the dialog's type was too small, buttons included, and
   Cancel was the only thing at a readable size.

   Cancel was the accident rather than the standard: .ed-link is "font:
   inherit", so it alone kept the page's 17px while the sentence above it sat
   at .ed-panel-hint's 0.72rem and the confirm at .ed-panel-go's 0.8rem. That
   scale is right for the editor, which is a dense workspace where every row
   of chrome is taken from the code. A dialog is not that. It interrupts, it
   carries one sentence, and it asks for a decision, so this panel opts out
   rather than the scale moving under every other panel in the editor.

   Everything here inherits, so the whole dialog lands on the page's own size
   and Cancel is matched UP to rather than trimmed down. */
.ed-panel.ed-panel-ask { width: min(27rem, 100%); }
.ed-panel-ask .ed-panel-head { padding: 0.9rem 1.1rem; }
.ed-panel-ask .ed-panel-head h2 { font-size: 1.12rem; line-height: 1.3; }
.ed-panel-ask .ed-panel-body { padding: 1.1rem; }
/* The consequence, which is the substance of the decision and the reason the
   dialog exists at all. */
.ed-ask { margin: 0; font-size: inherit; line-height: 1.5; color: var(--text-muted); }
.ed-panel-ask .ed-panel-actions { margin-top: 1.4rem; }
/* Both answers at the page's size and weight. Which is which is carried by
   colour and by the border, never by one being bigger: a confirm that shouts
   in type is a dialog answering its own question. */
.ed-panel-ask .ed-panel-go { font: inherit; padding: 0.45rem 1.1rem; }
.ed-panel-ask .ed-link { font: inherit; }

/* No phone rule: .ed-overlay already pads 1rem all round and the width is
   min(27rem, 100%), so on a 390px screen the panel is the 358px between the
   gutters. A media query here would subtract that padding twice. */
/* Text, not a link: it goes nowhere. As a link it was the accent colour and
   underlined, which made backing out the loudest thing in the panel. */
.ed-link.ed-quiet { color: var(--text-muted); text-decoration: none; }
.ed-link.ed-quiet:hover { color: var(--text); text-decoration: underline; }
/* .ed-draft went with the draft branch, and its comment described a Publish
   button that no longer exists either. A dead rule whose comment lies about a
   control is worse than no rule.

   A word in a sentence, not a control. Used for Cancel and for the quiet
   actions inside a panel; .ed-link-danger turns red only once a confirm is
   showing and the next click is the one that does it. */
.ed-link { background: none; border: 0; padding: 0; font: inherit; color: var(--link);
           text-decoration: underline; cursor: pointer; }
.ed-link:hover { color: var(--link-hover); }
.ed-link:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; border-radius: var(--radius); }
.ed-link-danger { color: var(--danger); }
/* overflow as well as min-width: the first lets the box shrink, the second
   keeps anything Monaco has not re-laid-out yet inside it. */
.ed-code { min-width: 0; min-height: 0; overflow: hidden; }
/* Only takes space when it has something to say. */
.ed-errors {
    margin: 0; padding: 0.8rem 0.9rem; max-height: 30%; overflow: auto;
    border-top: 1px solid var(--border); background: var(--bg-code);
    color: #c0392b; font-family: var(--font-mono); font-size: 0.76rem;
    white-space: pre-wrap;
}
.ed-frame { position: relative; background: #14181d; min-width: 0; min-height: 0; overflow: hidden; }
.ed-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* The frame alone fills the screen, so the sketch is the only thing on it. The
   iframe inside is already inset:0, so it follows without a rule of its own. */
.ed-frame:fullscreen { background: #000; }

/* The controls about looking rather than doing: the two pane toggles in the
   header and fullscreen on the preview bar. Pressed state is the aria-pressed
   the button already carries, so the look and the announcement cannot
   disagree. */
.ed-view[aria-pressed="true"], .ed-view.on { color: var(--link); }
/* A warning or an error the hidden console is holding. The same 6px dot the
   status flag uses, on the button's corner, because the button is the only
   part of the console left on screen. */
.ed-view.unseen { position: relative; }
.ed-view.unseen::after {
    content: ""; position: absolute; top: 3px; right: 3px;
    width: 6px; height: 6px; border-radius: 50%; background: var(--danger);
}

/* Stacked below the width where three columns each become useless. */
@media (max-width: 900px) {
    .ed { grid-template-columns: 1fr; height: auto; }
    /* Every pane back into the one column there is.

       Without this the panes keep the explicit grid-column each was given
       above, and a grid whose template declares ONE column answers an item
       placed in column 5 by inventing columns 2 to 5 as implicit auto tracks.
       So "stacked" was still three side-by-side strips, 505px of them inside a
       390px phone, and the layout that has been shipping here is one the
       stylesheet believes it turned off. Measured at 390x844, not guessed. */
    .ed-files, .ed-main, .ed-preview { grid-column: 1; }
    .ed-gutter { display: none; }
    .ed-files { border-right: 0; border-bottom: 1px solid var(--border); }
    .ed-preview { border-left: 0; border-top: 1px solid var(--border); }
    .ed-code { height: 55vh; }
    .ed-frame { height: 55vh; }
}

/* ---------- embeds ---------- */

/* The embed fills whatever box the host gave it, and the sketch fills the
   embed: a sketch has no aspect of its own, only the window it is given. */
.embed-body { margin: 0; background: var(--bg-code); overflow: hidden; }
.embed-stage {
    position: fixed; inset: 0; width: 100%; height: 100%;
    /* Every bound the player carries is off here, max-width included: that one
       is a height ceiling in disguise, and inside an embed vh is the HOST's
       frame, so a short wide embed would have had its width clamped to
       something unrelated to what the host asked for. The background stays the
       site's rather than the player's dark, because this box is the host's
       rect and the game fills it. */
    aspect-ratio: auto; max-height: none; max-width: none;
    background: var(--bg-code);
    border: 0; border-radius: 0;
}

/* The corner mark: where this came from, readable but out of the way. Sits
   above the game's frame so it survives the game starting, and grows its
   byline on hover so at rest it is a mark rather than a caption. */
.embed-mark {
    position: absolute; right: 8px; bottom: 8px; z-index: 5;
    display: inline-flex; align-items: center; gap: 0.4rem;
    padding: 0.3rem 0.6rem 0.3rem 0.45rem;
    border: 1px solid var(--border); border-radius: var(--radius);
    background: color-mix(in srgb, var(--bg) 82%, transparent);
    color: var(--text); font-size: 0.72rem; text-decoration: none;
    opacity: 0.85; transition: opacity 0.15s ease, background 0.15s ease;
    max-width: calc(100% - 16px); white-space: nowrap; overflow: hidden;
}
.embed-mark:hover, .embed-mark:focus-visible { opacity: 1; background: var(--bg); }
.embed-mark .logo-mark { width: 14px; height: 14px; color: #ea580c; flex: none; }
.embed-mark span { overflow: hidden; text-overflow: ellipsis; }
.embed-mark-by { color: var(--text-muted); display: none; }
.embed-mark:hover .embed-mark-by, .embed-mark:focus-visible .embed-mark-by { display: inline; }

/* The snippet dialog on the game page. */
.embed-dialog {
    /* The reset above zeroes every margin, and margin: auto is how a modal
       dialog centres itself in the viewport. */
    margin: auto;
    background: var(--bg); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--radius);
    /* Padding lives on the form, so a click anywhere on the dialog element is
       a click on the backdrop, which closes it. */
    padding: 0; width: min(560px, calc(100vw - 2rem));
}
.embed-dialog form { padding: 1.25rem 1.4rem; }
.embed-dialog::backdrop { background: rgba(0, 0, 0, 0.55); }
.embed-dialog h3 { margin: 0 0 0.5rem; font-size: 1rem; }
.embed-dialog p { margin: 0 0 0.9rem; font-size: 0.85rem; color: var(--text-muted); }
.embed-dialog code { font-family: var(--font-mono); font-size: 0.8rem; }
.embed-dialog textarea {
    display: block; width: 100%; box-sizing: border-box; resize: vertical;
    font-family: var(--font-mono); font-size: 0.74rem; line-height: 1.45;
    color: var(--text); background: var(--bg-code);
    border: 1px solid var(--border); border-radius: var(--radius); padding: 0.6rem;
    white-space: pre-wrap; word-break: break-all;
}
.embed-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 0.9rem; }

@media (prefers-reduced-motion: reduce) {
    * { transition: none !important; animation: none !important; }
}

/* MARK: an empty profile
   The page a first sign-in lands on. Centred, quiet, one way forward. */
.author-empty { text-align: center; padding: 4rem 0 5rem; }
.author-empty p { color: var(--text-muted); margin: 0 0 1.5rem; }

/* The teaching sketches on an empty profile. Quiet by construction: a lead-in
   and four text links, under the primary button and subordinate to it. */
.author-teach { margin-top: 2.5rem; }
.author-teach p { color: var(--text-label); font-size: 0.82rem; margin: 0 0 0.6rem; }
.author-teach ul {
    list-style: none; margin: 0; padding: 0;
    display: inline-flex; flex-direction: column; gap: 0.35rem; text-align: left;
}
.author-teach a { color: var(--text-muted); font-size: 0.88rem; text-decoration: none; }
.author-teach a b { color: var(--text); font-weight: 600; margin-right: 0.4rem; }
.author-teach a:hover { color: var(--link); }

/* The handle, centred, with the counts under it. Sai, 20 Sep 2026. It was a
   space-between row back when an action sat on the other end of it; with the
   action on the wordmark row and the Account link gone, space-between on a
   single child is left-aligned with extra machinery.

   The counts are centred here rather than on .card-sub, which the catalog
   cards, /manage and the commit list all share: this is the profile's own
   header, not a card. */
.author-top { display: flex; align-items: center; justify-content: center; gap: 1rem; flex-wrap: wrap; }
.author-top h1 { margin: 0; }
.author .card-sub { justify-content: center; }

/* MARK: one centered column, everywhere but the editor
   ------------------------------------------------------------------------
   Sai's brief: no top bar on any page, and content centred rather than run to
   the window's edge. A full-bleed page is not more content, it is the same
   content with further to travel for the eye, and on a wide monitor it reads
   as a spreadsheet. --page is wider than --measure because these pages carry
   grids of cards as well as prose. */
:root { --page: 64rem; }

/* The exception, and there is exactly one: a sketch's own page.
   Sai, 17 Sep 2026: the sketch page should take the window's width, the way a
   video site does. A measure is right for prose and for a grid of cards, and
   wrong for the one page whose subject is a stage: capping it at 64rem left a
   600px game on a 1440px screen with the sidebar crushed beside it. Wide is a
   deliberate opt-in rather than the default, so no other page drifts back to
   full bleed by accident. 1700px, because past that the eye travels further
   than the stage grows. */
:root { --page-wide: 1700px; }
main.page.wide, footer.wide .foot-inner { max-width: var(--page-wide); }

/* The docs column: the reading measure plus the two sidebars beside it.
   The docs are three columns where there is room, and .docs used to carry this
   width itself, back when it was a layout of its own beside the page. It is
   inside main.page now, so its own max-width could never bind and its own edge
   padding was added to the page's: 13rem of sidebar, 11rem of contents and two
   3rem gaps came out of 64rem less four gutters, which left the middle column
   at 394px on a 1440px screen. Stated here with the other two so all three
   page widths are one table, and so the wordmark row and the footer sit on the
   same edges as the content. */
:root { --page-docs: 76rem; }
main.page.docs-column, footer.docs-column .foot-inner { max-width: var(--page-docs); }

/* The footer is a sibling of main, not a child, so the sticky-footer job is
   the body's. Without this a short page (an empty profile) leaves the footer
   floating under the content rather than at the bottom. */
body { display: flex; flex-direction: column; min-height: 100vh; }
main.page { flex: 1; }

main.page, footer .foot-inner {
    max-width: var(--page); margin: 0 auto;
    padding-left: var(--edge); padding-right: var(--edge);
    width: 100%;
}
/* The mark's row, as a page element. No bar, no rule, no sticky, no
   background: the only thing this adds over a bare wordmark is somewhere for
   two small controls to sit on a line that was otherwise empty. */
main.page > .mark-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem; margin: 1.6rem 0 1.4rem;
}
.mark-side { display: flex; align-items: center; gap: 1rem; }
/* Text, at the footer's weight rather than the content's. A button here would
   be the loudest thing on a page whose whole point is that the chrome is
   quiet. */
.mark-action {
    font-size: 0.85rem; color: var(--text-muted); text-decoration: none;
}
.mark-action:hover { color: var(--text); }

/* New sketch, as the square-pen. Sized to the text beside it rather than to
   the 24px the svg declares: the icon is one of three things on a quiet row,
   and at its natural size it is the loudest thing on the page. The svg strokes
   currentColor, so the hover is one property here and none on the mark. */
.mark-icon {
    display: inline-flex; align-items: center;
    color: var(--text-muted); text-decoration: none;
}
.mark-icon svg { width: 1.15rem; height: 1.15rem; display: block; }
.mark-icon:hover { color: var(--text); }


/* Centred, like the content above it, and the same width. */
footer .foot-inner { justify-content: center; flex-wrap: wrap; }

/* MARK: the app header's avatar
   Your page, reached by your own initial. A link rather than a menu: the only
   thing behind it is your profile, and a menu with one item is a menu that
   makes you click twice. Sign out lives at the foot of /manage's sidebar. */
.avatar-link { display: inline-flex; align-items: center; text-decoration: none; }
.avatar {
    width: 1.75rem; height: 1.75rem; border-radius: 50%; flex: none;
    display: grid; place-items: center;
    background: var(--link); color: #fff;
    font-size: 0.82rem; font-weight: 600; line-height: 1;
}
.avatar-link:hover .avatar { background: var(--link-hover); }

/* MARK: the landing
   ------------------------------------------------------------------------
   The front door: logo, one explanation, the way in, a centered footer. It
   borrows the site's tokens rather than introducing a second palette, because
   a landing that does not match the page behind it is the incoherence this
   redesign exists to remove. */

/* No width and no padding of its own: main.page is the column now, and the
   landing is one page inside it rather than a layout beside it. What is left
   is the height, which keeps the panel off the bottom of a tall window. */
.landing {
    min-height: 74vh; display: flex; flex-direction: column;
}
.landing-split {
    flex: 1;
    display: grid; grid-template-columns: 1fr 1px 21rem;
    gap: calc(var(--step) * 6); align-items: center;
    padding: calc(var(--step) * 8) 0;
}
.landing-rule { background: var(--border); align-self: stretch; }

.landing-pitch h1 {
    font-size: clamp(1.8rem, 3.4vw, 2.4rem); line-height: 1.14;
    letter-spacing: -0.02em; margin: 0 0 calc(var(--step) * 3);
    /* The break is per sentence, set in the markup. balance only tidies what
       happens inside a sentence on a narrow screen. */
    text-wrap: balance; max-width: 26ch;
}
.landing-pitch h1 .headline-line { display: block; }
.landing-pitch p {
    margin: 0; color: var(--text-muted);
    font-size: 1.02rem; line-height: 1.6; max-width: 46ch;
}

.landing-panel {
    border: 1px solid var(--border); border-radius: var(--radius);
    padding: calc(var(--step) * 4); background: var(--bg);
}
.landing-panel h2 { font-size: 1.05rem; margin: 0 0 calc(var(--step) * 3); }
.landing-panel form { margin: 0; }
.landing-panel label {
    display: block; font-size: 0.78rem; text-transform: uppercase;
    letter-spacing: 0.04em; color: var(--text-label); margin-bottom: 0.3rem;
}
.landing-panel input {
    width: 100%; padding: 0.55rem 0.65rem; margin-bottom: var(--step);
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--bg); color: var(--text); font: inherit; font-size: 0.95rem;
}
.landing-panel #signin-submit {
    width: 100%; padding: 0.6rem 0.9rem; border: 1px solid var(--link);
    border-radius: var(--radius); background: var(--link); color: #fff;
    font: inherit; font-weight: 600; font-size: 0.95rem; cursor: pointer;
}
.landing-panel #signin-submit:hover {
    background: var(--link-hover); border-color: var(--link-hover);
}
.landing-fine {
    font-size: 0.8rem; color: var(--text-muted);
    margin: calc(var(--step) * 2) 0 0; line-height: 1.5;
}

/* The provider sign-ins. Links rather than buttons, because starting one is a
   navigation: it sets a state cookie and redirects. They were disabled buttons
   marked "soon" until the secrets existed. */
.provider {
    width: 100%; display: flex; align-items: center; justify-content: center;
    gap: 0.5rem; padding: 0.55rem 0.9rem; margin-bottom: var(--step);
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--bg); color: var(--text);
    font: inherit; font-size: 0.92rem; text-decoration: none;
}
.provider:hover { border-color: var(--text-label); }
.provider:focus-visible { outline: 2px solid var(--link); outline-offset: 1px; }

.landing-or {
    display: flex; align-items: center; gap: 0.6rem;
    margin: calc(var(--step) * 3) 0; color: var(--text-label); font-size: 0.8rem;
}
/* The agreement line under the sign-in button, and the legal pages it points
   at. Both are quiet: one is fine print and the other is a document somebody
   reads rather than a page that sells anything. */
.signin-agree {
    margin: calc(var(--step) * 2) 0 0; font-size: 0.78rem; line-height: 1.5;
    color: var(--text-label);
}
/* Underlined, because somebody is being told they agree to these: a link at
   the same weight and colour as the sentence around it does not read as one. */
.signin-agree a { color: var(--text-muted); text-decoration: underline; }
.signin-agree a:hover { color: var(--text); }

.legal { max-width: var(--measure); padding-bottom: calc(var(--step) * 8); }
.legal h1 { margin: 0 0 calc(var(--step) * 2); }
.legal-lede {
    margin: 0 0 calc(var(--step) * 6); color: var(--text-muted);
    font-size: 1.02rem; line-height: 1.6;
}
.legal h2 {
    margin: calc(var(--step) * 8) 0 calc(var(--step) * 3);
    font-size: 1.15rem;
}
/* Paragraphs need air. Without an explicit margin these inherit a tight one
   and a page of prose this long reads as one block. */
.legal p { margin: 0 0 calc(var(--step) * 3); line-height: 1.65; }
.legal li { line-height: 1.65; }
.legal strong { font-weight: 650; }
.legal ul { padding-left: 1.2rem; }
.legal li { margin: calc(var(--step) * 1.5) 0; }
.legal code {
    font-family: var(--font-mono); font-size: 0.9em;
    background: var(--bg-code); padding: 0.1em 0.3em; border-radius: var(--radius);
}

.landing-or::before, .landing-or::after {
    content: ""; flex: 1; height: 1px; background: var(--border);
}

@media (max-width: 46rem) {
    .landing-split {
        grid-template-columns: 1fr; gap: calc(var(--step) * 5);
        padding: calc(var(--step) * 5) 0;
    }
    .landing-rule { display: none; }
}
