/* The whole site's stylesheet.

   HOW IT SHIPS. The build reads this file, appends the per-game theme CSS, and writes the
   result to a content-hashed /css/site.<hash>.css served immutable — a fixed /site.css
   once let new HTML pair with a stale cached stylesheet. No build step of its own beyond
   that fingerprinting, no external font, no CDN: the same self-contained shape the games
   ship in. Only `transform` and `opacity` are ever animated here; nothing in this file
   animates anything else.

   THIS FILE IS PUBLISHED, COMMENTS AND ALL. Nothing is stripped on the way out, so every
   line below can be read with view-source. Comments here describe the CSS: what a token is
   for, what it measured at, what breaks if you change it. Why the studio chose something,
   what it is building next, and anything naming a file in the repository stay with the
   source and out of the shipped bytes — the site linter fails the build if that kind of
   note comes back.

   THE PALETTE, one line each. COAL is the container, CANARY is the signal, VERDIGRIS is
   the way through. Coal is what the hero band and the footer are cut from, and it is a
   deep neutral on purpose: it is the one ground every game's art can sit on without a
   fight. Canary is a mark, a rule, a single primary button, a "you are here" pill — never
   a field, because a page-wide sheet of the brightest yellow on the wheel is the worst
   container there is for somebody else's artwork. Verdigris carries links, buttons, done.

   NONE OF IT IS ANY GAME'S. --coal and --paper belong to the studio. Each game keeps its
   own colours inside its own frame, through the body.game-<slug> rules the build appends,
   used only around that game's own art.

   THEME. Every colour is a token defined twice: once on :root for the light theme, once
   under prefers-color-scheme: dark. The BAND tokens are the exception and are deliberately
   constant — the hero band and the footer are the brand's signature and are the same in
   both themes, which is what lets one set of measured ratios cover both. Yellow is 1.48:1
   on --paper and can never carry a graphic on the light theme; --signal-edge does that job
   (3.54:1), and in dark it becomes the yellow itself (10.06:1).

   MEASURED, NOT EYEBALLED. Every ratio quoted below is WCAG and was computed over the
   emitted stylesheet rather than over a copy of these values. Text clears 4.5:1 and every
   control boundary clears 3:1 in both themes; the linter re-measures both on each build,
   so a token that drifts under a floor is a red build and not a live defect. */

:root {
  color-scheme: light dark;

  /* the container */
  --coal: #12191C;
  --coal-lift: #1A2225;
  --paper: #F6F6F3;
  --surface: #ECEDE7;
  --card: #FFFFFF;
  --line: #DEE0D8;

  /* ink — 15.90:1, 6.98:1 and 5.01:1 on --paper; the faint step is 4.60:1 on --surface,
     which is the tightest pair in the light theme and the one a previous grey failed */
  --ink: #161C1F;
  --ink-dim: #4B565A;
  --ink-faint: #5F6C72;

  /* the signal. --signal is a FIELD colour for small marks only; --signal-edge is what
     may draw a line or a rule on the light theme; --signal-ink is what may sit ON yellow;
     --signal-text is the only member of the family that is legible as text on paper. */
  --signal: #FFC24B;
  --signal-edge: #A97A0B;
  --signal-ink: #3A2A00;
  --signal-text: #6A4A05;

  /* the way through: links, buttons, "done" */
  --accent: #1F6B5E;
  --accent-deep: #14514A;
  --accent-on: #FFFFFF;
  --accent-faint: rgba(31, 107, 94, .38);

  /* CONTROL BOUNDARIES, and why they are not --line. WCAG 1.4.11 asks 3:1 of the edge that
     identifies a control, and this site is held to the same floor the games are. --line is
     a HAIRLINE BETWEEN BLOCKS OF CONTENT — table rules, card edges, the footer divider —
     and lifting it to 3:1 would redraw every fact table as a grid of heavy rules. A
     button's edge IS the button, so it gets its own value. This one measures 3.82:1 on
     --paper, 3.51:1 on --surface and 4.13:1 on --card; --surface is the tightest of the
     three and is what a new value has to be checked against. The hover tint is the fill
     appearing under the pointer, never a colour a boundary is measured from. */
  --btn-edge: #727F83;
  --btn-tint: rgba(31, 107, 94, .09);

  /* One hairline, defined per theme, used wherever a box has to separate from a ground the
     stylesheet does not control — the studio mark's coal field on the DARK page, where the
     two are the same colour and the mark otherwise floats loose. This was a color-mix() for
     an hour; it is a literal because color-mix()'s support baseline is a fact about the
     world outside this repo that this session did not verify, an unsupported declaration is
     dropped silently, and a silently-dropped hairline is the defect it was added to fix. */
  --hairline: rgba(22, 28, 31, .16);

  /* the signature band — CONSTANT across themes on purpose. It is cut twice: the hero at
     the top of the landing page and the footer at the bottom of every page, so the site is
     bookended in the studio's own colour and the paper between them reads as the content
     rather than as the whole design. Measured on --band: --band-ink 15.95:1, --band-dim
     9.47:1, --band-faint 6.79:1, --band-link 8.46:1, --signal 11.06:1. --band-btn-edge is
     the control boundary up here, 3.99:1, and is the band's answer to --btn-edge; the
     hairline is decorative and separates nothing that carries meaning. */
  --band: #12191C;
  --band-ink: #F2F3EF;
  --band-dim: #B6BFBF;
  --band-faint: #98A2A2;
  --band-link: #67C3AE;
  --band-btn-edge: #6E7A78;
  --band-line: rgba(242, 243, 239, .13);
  --band-tint: rgba(242, 243, 239, .07);

  /* The phone bezel and the /play stage frame. It used to be a hard-coded near-black, which
     worked when the band behind it was yellow and vanished the moment the band became coal —
     and vanished again on the dark theme, where the PAGE is coal. It is deliberately a step
     LIGHTER than the band in both themes, which is the only way a device frame reads as a
     device rather than as a hole. */
  --device: #242D31;

  --tile: #FFFFFF;
  --tile-edge: #D4D8CF;
  --game-ground: #12191C;
  --game-accent: #FFC24B;
  --shadow: rgba(6, 14, 16, .45);
  --wrap: 1080px;

  /* Type: a real pairing, and both halves are already here. Outfit does display and every
     piece of UI furniture — headings, nav, buttons, tables, pills, captions — and the
     platform's own serif does running prose. The studio's pages are argumentative long-form
     (`/studio`, the devlog, the FAQ answers), and a reading face makes them read as a studio
     with something to say rather than as a landing page with paragraphs on it. It costs zero
     new bytes and zero new hosts, which is the constraint: no font may be added to the CSP. */
  --sans: "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --serif: ui-serif, Charter, "Iowan Old Style", "Source Serif Pro", Georgia, "Times New Roman", serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #12191C;
    --surface: #1A2225;
    --card: #1A2225;
    --line: #2C3639;
    --ink: #EAECE8;
    --ink-dim: #B6BFBF;
    --ink-faint: #8E9A9B;
    --signal-edge: #FFC24B;
    --signal-text: #FFC24B;
    --accent: #67C3AE;
    --accent-deep: #8FD6C5;
    --accent-on: #12191C;
    --accent-faint: rgba(103, 195, 174, .42);
    /* 3.53:1 on --paper and 3.21:1 on --surface/--card in this theme */
    --btn-edge: #647177;
    --btn-tint: rgba(103, 195, 174, .12);
    --hairline: rgba(234, 236, 232, .18);
    --tile-edge: #38424A;
    --shadow: rgba(0, 0, 0, .7);
  }
}

/* Outfit (SIL Open Font License, assets/fonts/OFL-Outfit.txt), self-hosted: one geometric
   family with a variable weight axis, so headlines, buttons and UI share a voice without a
   second file. Latin subset only, 32 KB. `font-display: swap` keeps text readable on first
   paint; the system stack below is the fallback and is metrically close enough not to jump. */
@font-face {
  font-family: "Outfit";
  src: url("/fonts/outfit-latin.woff2") format("woff2");
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* THE PAIRING, in two rules. Outfit is the default above and keeps every piece of UI
   furniture; the serif is switched on for running prose inside <main> and switched back
   off for the small sans-shaped things that live inside main and would otherwise be caught
   by `main p` / `main li` — captions, dates, breadcrumbs, fact rows, pills, buttons.
   Serif x-heights are smaller than Outfit's, so prose steps up a point and gains leading;
   without that the pairing reads as a mistake rather than a choice. */
main p, main li, .lead { font-family: var(--serif); font-size: 18px; line-height: 1.62; }
.small, .byline, .entry-date, .crumbs, .crumbs li, .phone-note, .tray-note, .pill,
.facts-list li, .where li, .assets li, .stage-state, .btn, .badge, .promise-mark,
footer.site p, footer.site li, .shot figcaption, code {
  font-family: var(--sans);
  font-size: inherit;
  line-height: inherit;
}

a { color: var(--accent); text-decoration: underline; text-decoration-color: var(--accent-faint); text-decoration-thickness: 2px; text-underline-offset: 3px; }
a:hover { color: var(--accent-deep); text-decoration-color: var(--accent-deep); }
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px; }

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 24px; }
main.wrap { padding-top: 32px; padding-bottom: 96px; }

/* --- site chrome ------------------------------------------------------- */

/* Skip to content: the first focusable thing on every page, so a keyboard or switch user
   is not tabbing the brand and five nav links on each of twenty-odd pages before reaching
   what the page is for. Off-screen by TRANSFORM rather than display:none or a 1px clip —
   it stays in the accessibility tree and stays focusable, and transform is one of the two
   properties this file is allowed to animate.

   Full-bleed (left:0/right:0), not a small pill pinned at left:16/top:12: a fixed-size
   pill at narrow widths (screenshotted at 500px) sat on TOP of the topbar's wrapped brand
   text — a solid box covering only part of it, so the tail of the studio name and the
   start of the nav peeked out from behind the pill on focus. A bar spanning the
   full topbar width covers it edge-to-edge instead, so nothing behind it is ever
   partially visible, at any viewport width. translateY(-100%) (of its own height,
   not a guessed -200%) keeps it fully off-canvas until :focus regardless of how tall
   the bar's padding makes it. */
.skip {
  position: absolute;
  z-index: 100;
  left: 0;
  right: 0;
  top: 0;
  padding: 16px 24px;
  background: var(--ink);
  color: var(--paper);
  font-weight: 600;
  text-decoration: none;
  transform: translateY(-100%);
  transition: transform .15s ease;
}
.skip:focus { transform: translateY(0); }
main:focus { outline: none; }   /* it takes focus as the skip target, not as a control */

/* EVERY CONTROL IN HERE IS 44px TALL, and that is why the bar's own padding is small.
   A 15px nav link is a 24px-high hit area, half of the 44x44 the platform guidance asks
   for, and the structural pass reported both the nav and the brand under it on every page
   of the site. The fix has to be the CONTROL's box, not the BAR's padding: vertical
   padding on an inline element grows the rect the pass measures without growing the line
   box, so the bar looks unchanged while the links start overlapping each other. They are
   inline-flex with a min-height instead, which grows both together. */
.topbar { background: var(--paper); border-bottom: 1px solid var(--line); }
.topbar .wrap { padding-top: 10px; padding-bottom: 10px; display: flex; flex-wrap: wrap; gap: 6px 28px; align-items: center; }
.brand { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; font-weight: 800; font-size: 18px; letter-spacing: -0.01em; color: var(--ink); text-decoration: none; margin-right: auto; }
/* The hairline is not decoration. The mark's own field is --coal, which on the DARK theme is
   also the page ground: without a ring the box vanishes and the bird floats loose in the
   topbar, and in the /studio crest it collided with the game icon beside it. The ring is drawn
   from the ink token so it is dark-on-light and light-on-dark without a second rule. */
.brand .mark { width: 32px; height: 32px; display: inline-block; border-radius: 9px; box-shadow: 0 0 0 1px var(--hairline); }
.brand .mark svg { display: block; width: 100%; height: 100%; }
.topbar nav { display: flex; flex-wrap: wrap; gap: 0 8px; }
.topbar nav a { display: inline-flex; align-items: center; min-height: 44px; padding: 0 10px; border-radius: 10px; font-size: 15px; font-weight: 600; color: var(--ink-dim); text-decoration: none; }
.topbar nav a:hover { color: var(--ink); background: var(--surface); }

/* THE FOOTER IS THE SECOND CUT OF COAL. The hero band opens the landing page in the
   studio's own colour; this closes every page in it, so the paper in between reads as the
   content rather than as the whole design, and a page with no hero still ends somewhere
   deliberate instead of trailing off into a grey strip. It uses the BAND tokens, which are
   constant across themes, so the block below the fold is identical in light and dark and
   one set of measured ratios covers it: --band-dim 9.47:1, --band-faint 6.79:1 and the
   headings' --band-faint 6.79:1, all on --band. Column headings are set in small caps with
   real tracking, which is the one place on the site where a label is allowed to shout. */
/* The hairline and the lift are for the DARK theme, where the page ground is the same coal
   this block is cut from and the footer would otherwise be indistinguishable from the end
   of the article. On the light theme the colour change already does that work and both are
   invisible. This is the same problem the hero band solves with its radial lift, and the
   same answer. */
footer.site { background: var(--band); background-image: linear-gradient(to bottom, rgba(255,194,75,.05), rgba(255,194,75,0) 240px); border-top: 1px solid var(--band-line); color: var(--band-dim); font-size: 15px; }
footer.site .wrap { padding-top: 56px; padding-bottom: 48px; display: grid; grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr)); gap: 32px 32px; }
footer.site .brand { align-self: start; margin-right: 0; color: var(--band-ink); }
footer.site .brand .mark { width: 30px; height: 30px; box-shadow: 0 0 0 1px rgba(242,243,239,.22); }
footer.site .foot-line { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px 24px; border-top: 1px solid var(--band-line); padding-top: 20px; margin-top: 4px; font-size: 13.5px; color: var(--band-faint); }
footer.site h4 { margin: 0 0 12px; font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: var(--band-faint); font-weight: 700; }
footer.site ul { list-style: none; margin: 0; padding: 0; max-width: none; }
footer.site li { margin: 0 0 10px; }
footer.site a { color: var(--band-dim); text-decoration: none; }
footer.site a:hover { color: var(--band-ink); text-decoration: underline; text-decoration-color: var(--signal); text-decoration-thickness: 2px; text-underline-offset: 3px; }
footer.site p { margin: 0; }

/* --- type -------------------------------------------------------------- */

/* THE SCALE. 52 / 30 / 20 over an 18px serif body, which is a real ratio rather than four
   sizes that happen to differ. The previous 44/26/19 sat close enough to the body copy
   that a section heading read as bold text, and the pass that found this site's typography
   defects found them as SPACING, not as size — so the vertical rhythm is stated here too
   and is the thing to be careful with: 72px above an h2 and 14px below it is what makes
   the heading belong to what follows it rather than float between two blocks. */
h1 { font-size: 52px; line-height: 1.06; letter-spacing: -0.03em; font-weight: 800; margin: 0 0 14px; max-width: 20ch; text-wrap: balance; }
h2 { font-size: 30px; line-height: 1.18; letter-spacing: -0.02em; font-weight: 800; margin: 72px 0 14px; text-wrap: balance; }
h2.tight { margin-top: 40px; }
h3 { font-size: 20px; line-height: 1.3; margin: 0 0 6px; font-weight: 700; letter-spacing: -0.01em; }
p { margin: 0 0 18px; max-width: 68ch; }
ul { margin: 0 0 18px; padding-left: 22px; max-width: 68ch; }
li { margin-bottom: 8px; }
.tagline { color: var(--ink-dim); margin: 0 0 24px; font-size: 19px; }
.lead { font-size: 22px; line-height: 1.42; color: var(--ink-dim); max-width: 46ch; }
.small { font-size: 14px; color: var(--ink-dim); }

/* THE SECTION MARK. A 44x3 rule in the signal colour opens every page and every top-level
   section — the same mark .promise-mark already carries, promoted to the whole site so
   that structure is visible rather than implied by whitespace alone. It is drawn on a
   pseudo-element, so no copy carries it and no length of heading changes it.

   The selectors are deliberately CHILD selectors. A descendant selector here would reach
   the question headings inside .qa, the h2s inside a devlog article and the h1 inside a
   game's .game-head crest, all of which are headings inside a component that already has
   its own opening — and a mark on every one of them is not punctuation, it is noise. */
main.wrap > h1::before,
main.wrap > h2::before,
.section > h2::before,
.two-col > div > h2::before {
  content: "";
  display: block;
  width: 44px;
  height: 3px;
  border-radius: 2px;
  margin: 0 0 20px;
  background: var(--signal-edge);
}
code { background: var(--surface); border: 1px solid var(--line); padding: 1px 6px; border-radius: 5px; font-size: 14px; }

/* --- landing hero: the yellow band ------------------------------------ */

/* The band bleeds to the viewport edge; main.wrap's side padding is undone here and
   re-applied inside so the grid still lines up with everything below it.

   IT IS COAL, IN BOTH THEMES, AND THAT IS THE POINT. This block is the one place the
   studio's identity is stated at full size, and it has to hold whatever art is dropped
   into it — a near-black screen one week, warm paper the next. A deep neutral is the only
   ground all of them can sit on without one winning; the yellow it replaced put maximum
   contrast behind the artwork and turned the device into a sticker. The radial lift is the
   band's signature and does one more job on the DARK theme, where the page ground is the
   same coal: it is what still reads as a lit block rather than as more page. Warm, because
   it is the signal colour at 16%, not a grey.

   THE STAGE. The device does not float on the band, it stands on it: an elliptical pool of
   light behind the frame, and the frame overlapping the band's bottom edge so the art is
   presented rather than pasted. Both are drawn on pseudo-elements, so the markup is
   unchanged and nothing has to be authored per game. */
/* overflow:hidden is load-bearing, not tidiness: the stage glow below is wider than the
   device it sits behind, and without a clip it would push the document's scroll width past
   the viewport and give the whole site a horizontal scrollbar. */
.hero-band { background: var(--band); background-image: radial-gradient(78% 96% at 70% 20%, rgba(255,194,75,.16) 0%, rgba(255,194,75,0) 70%); color: var(--band-ink); margin: -32px calc(50% - 50vw) 0; padding: 0 calc(50vw - 50%); overflow: hidden; }
/* THE COPY COLUMN IS THE SHORT ONE — a headline and three lines against a 668px device —
   so where it sits inside the row is the whole composition. Top-aligned it leaves a 330px
   hole under the buttons; centred against the device it leaves the same air above and
   below, which is what makes the band read as a stage rather than as a container somebody
   forgot to fill. Centred is therefore deliberate, and the top inset is what keeps the
   band from starting flush against the topbar. Do not swap it for `start` to "tidy up" the
   top: the hole moves, it does not close. */
.hero { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); gap: 32px 56px; align-items: center; padding: 72px 24px 0; max-width: var(--wrap); margin: 0 auto; }
.hero h1 { font-size: 88px; line-height: .96; letter-spacing: -0.037em; max-width: 11ch; margin-bottom: 20px; }
/* The byline is the one line of yellow type on the page — the studio signing the game's
   name — which is exactly the job the signal colour is for. 11.06:1 on the band. The rule
   in front of it is the same 3px signal mark that opens every section further down, turned
   on its side; it is drawn on a pseudo-element and sized in px, so it survives any length
   of byline and any rewrite of it. */
.hero .byline { display: flex; align-items: center; gap: 14px; font-size: 15px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--signal); margin: 0 0 22px; }
.hero .byline::before { content: ""; flex: 0 0 auto; width: 34px; height: 3px; border-radius: 2px; background: var(--signal); }
.hero .lead { margin-bottom: 32px; color: var(--band-dim); max-width: 42ch; font-size: 23px; }
.hero .small { color: var(--band-faint); margin-top: 16px; }
.hero a { color: var(--band-link); text-decoration-color: rgba(103,195,178,.42); }
/* --band-btn-edge, not a translucent white: the boundary a control is identified by has to
   clear 3:1 against what it sits on, and the .34 white this replaced measured 2.92:1. */
.hero .btn { color: var(--band-ink); border-color: var(--band-btn-edge); background: var(--band-tint); }
.hero .btn:hover { border-color: var(--band-ink); background: rgba(242,243,239,.13); }
/* One yellow button per page, and it is the one that starts a game. */
.hero .btn.primary { background: var(--signal); border-color: var(--signal); color: var(--signal-ink); box-shadow: 0 4px 0 #C08B14; }
.hero .btn.primary:hover { background: #FFCF6E; border-color: #FFCF6E; }
.hero-copy > * { opacity: 0; transform: translateY(14px); animation: rise .7s cubic-bezier(.2,.7,.2,1) forwards; }
.hero-copy > :nth-child(2) { animation-delay: .08s; }
.hero-copy > :nth-child(3) { animation-delay: .16s; }
.hero-copy > :nth-child(4) { animation-delay: .24s; }

.btnrow { display: flex; flex-wrap: wrap; gap: 12px; margin: 0 0 8px; }
/* THE EDGE IS --btn-edge, NEVER --line. A secondary button here is identified by its
   boundary and nothing else, so that boundary carries the 3:1 non-text floor on its own.
   Drawn from --line it measured 1.23:1 on the light theme and 1.43:1 on the dark one —
   a control the eye finds by reading its label and then guessing where its edge is. The
   fix is a token of the button's own rather than a darker hairline everywhere, because
   --line also draws every table rule and card edge on the site and those are not
   controls. Measured after the change: 3.82:1 light, 3.53:1 dark, 3.99:1 on the band. */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 50px; padding: 0 24px; border-radius: 12px;
  border: 2px solid var(--btn-edge); background: transparent;
  color: var(--ink); font-weight: 700; font-size: 16px; text-decoration: none;
}
.btn:hover { border-color: var(--ink); background: var(--btn-tint); }
/* The plinth is a shade of black rather than --accent-deep: the deep token is the HOVER
   colour and on the dark theme it is LIGHTER than the button, which would light the button
   from underneath. */
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-on); box-shadow: 0 4px 0 rgba(0,0,0,.34); }
.btn.primary:hover { transform: translateY(-1px); background: var(--accent); }
.btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }

/* The phone: the real playable build, framed. The iframe lays the game out at its
   native 390x844 and is scaled down with a transform, so the game sees a true
   phone viewport and no layout code is shared with the frame. */
.phone { position: relative; width: 320px; margin: 0 auto; justify-self: center; opacity: 0; transform: rotate(-9deg) translateY(28px); animation: settle 1s cubic-bezier(.2,.7,.2,1) .2s forwards; }
/* The pool of light the device stands in — a soft ellipse of the signal colour behind the
   frame. It is what makes the art read as PRESENTED: without it a rotated rectangle on a
   flat field is a sticker, which is the exact failure a full-bleed yellow band produced
   and the reason the band is coal. No markup, no image, no animation. */
.phone::before { content: ""; position: absolute; left: 50%; top: 4%; width: 128%; height: 96%; transform: translateX(-50%); background: radial-gradient(closest-side, rgba(255,194,75,.20), rgba(255,194,75,.05) 58%, rgba(255,194,75,0) 100%); pointer-events: none; }
.phone-shell { position: relative; width: 320px; height: 668px; border-radius: 42px; background: var(--device); padding: 10px; box-shadow: 0 34px 60px -24px var(--shadow), 0 0 0 1px rgba(255,255,255,.16) inset, 0 0 0 1px rgba(255,255,255,.10); }
.phone-screen { position: relative; width: 300px; height: 648px; border-radius: 32px; overflow: hidden; background: var(--game-ground); }
.phone-screen iframe { display: block; width: 390px; height: 844px; border: 0; transform: scale(0.7692); transform-origin: 0 0; }
.phone-note { text-align: center; margin: 14px 0 0; font-size: 14px; color: var(--band-faint); }

/* The tray: seven real tiles from the game's default theme, dealt along the bottom
   edge of the band the way the tray sits under the board. Rotations alternate
   per tile so the row reads as dealt, not stamped. Same coal as the hero, so hero and
   tray read as one block rather than two stripes. */
.tray { display: flex; justify-content: center; gap: 12px; padding: 28px 24px 0; margin: 0 auto; max-width: var(--wrap); overflow: hidden; transform: translateY(32px); }
.tray-band { background: var(--band); margin: 0 calc(50% - 50vw) 64px; padding: 0 calc(50vw - 50%); }
.tile { width: 64px; height: 64px; flex: 0 0 auto; border-radius: 14px; background: var(--tile); border-bottom: 4px solid var(--tile-edge); padding: 9px; box-shadow: 0 10px 18px -8px var(--shadow); }
.tile svg { display: block; width: 100%; height: 100%; }
.tile:nth-child(odd) { transform: rotate(-4deg); }
.tile:nth-child(even) { transform: rotate(3deg) translateY(3px); }
.tile:nth-child(4) { transform: rotate(0) translateY(-4px); }
.tray-note { text-align: center; font-size: 14px; color: var(--ink-dim); margin: -8px 0 48px; }

/* The promises: a plain two-column list. The marker used to be one of the HERO GAME's
   theme tiles, which made a studio-wide promise about every game we ship depend on the art
   of one of them — the same borrowed-identity defect the palette above was rewritten to
   fix, one level down. It is a studio-owned rule and index now: no game art, and it works
   on a page whose hero game has no tray at all. */
.promises { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 56px; border-top: 1px solid var(--line); }
.promise { display: grid; grid-template-columns: 36px minmax(0, 1fr); gap: 0 18px; align-items: start; padding: 26px 0 22px; border-bottom: 1px solid var(--line); }
.promise-mark { display: block; width: 36px; padding-top: 6px; font-family: var(--sans); font-size: 13px; font-weight: 800; letter-spacing: .06em; color: var(--signal-text); border-top: 3px solid var(--signal-edge); }
.promise h3 { font-size: 21px; letter-spacing: -0.01em; margin-bottom: 6px; }
.promise p { margin: 0; font-size: 16px; color: var(--ink-dim); }

/* --- landing sections ------------------------------------------------- */

/* The h2's own 72px top margin is what separates two sections; this only has to keep them
   from touching when a section ends in a card. Stacking a large section margin ON TOP of a
   large heading margin is how a page ends up looking like several pages glued together. */
.section { padding: 0; }
.section + .section { margin-top: 32px; }
.two-col { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 24px 64px; align-items: start; }
.two-col h2 { margin-top: 0; }

.feature { display: grid; grid-template-columns: 104px minmax(0, 1fr); gap: 24px 30px; align-items: start; padding: 30px; border-radius: 24px; background: var(--card); border: 1px solid var(--line); box-shadow: 0 18px 40px -26px var(--shadow); }
.feature + .feature { margin-top: 18px; }
.feature .icon { width: 104px; height: 104px; border-radius: 24px; overflow: hidden; box-shadow: 0 12px 26px -12px var(--shadow); }
.feature h3 { font-size: 26px; letter-spacing: -0.02em; margin-bottom: 4px; }
.feature .tagline { margin-bottom: 14px; font-size: 17px; }
.feature p { max-width: 60ch; }
.feature .btnrow { margin-top: 20px; }

.facts-list { list-style: none; padding: 0; margin: 0; border-top: 1px solid var(--line); }
.facts-list li { display: grid; grid-template-columns: 9em minmax(0, 1fr); gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--line); margin: 0; font-size: 16px; }
.facts-list li span:first-child { color: var(--ink-dim); }

.posts { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; }
/* THE DEVLOG CARD, and it must stay spelled `a.post`. The landing page's card is the only
   `.post` the generator emits as a card (build.mjs, "From the devlog") — but a devlog POST
   page also carries `<body class="post">` and `<article class="post">`, so a bare `.post`
   here reached all three. Measured 2026-09-09 on the live /devlog/two-games-one-kit: the
   card's `margin: 0` stripped every paragraph break out of the article, `--ink-dim` at 15px
   dimmed and shrank the whole body copy, and `background: var(--card)` + the 18px radius and
   border wrapped the entire PAGE — nav and footer included — in a card. A class shared by a
   component and a page is not a component selector. */
a.post { display: block; padding: 24px 26px; border-radius: 18px; background: var(--card); border: 1px solid var(--line); text-decoration: none; color: var(--ink); box-shadow: 0 14px 32px -26px var(--shadow); }
a.post:hover { border-color: var(--signal-edge); transform: translateY(-2px); }
a.post h3 { margin-bottom: 8px; }
a.post p { margin: 0; color: var(--ink-dim); font-size: 15px; }
a.post .entry-date { margin-bottom: 6px; }

/* --- blocks shared by the inner pages --------------------------------- */

.callout { background: var(--surface); border: 1px solid var(--line); border-left: 5px solid var(--signal); border-radius: 14px; padding: 20px 22px; margin: 0 0 26px; max-width: 72ch; }
.callout p:last-child { margin-bottom: 0; }

.card { background: var(--card); border: 1px solid var(--line); border-radius: 18px; padding: 22px 24px; margin-bottom: 14px; box-shadow: 0 14px 32px -28px var(--shadow); }
.card p:last-child { margin-bottom: 0; }
.card p { color: var(--ink-dim); font-size: 16px; }
a.card { display: block; text-decoration: none; color: var(--ink); }
a.card:hover { border-color: var(--signal-edge); transform: translateY(-2px); }
a.card h3 { color: var(--ink); }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; }

.facts { border-collapse: collapse; width: 100%; max-width: 72ch; margin: 0 0 18px; }
.facts td { border-bottom: 1px solid var(--line); padding: 10px 12px 10px 0; vertical-align: top; font-size: 16px; }
.facts td:first-child { color: var(--ink-dim); width: 36%; }

.game-head { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: 20px 24px; align-items: center; margin-bottom: 18px; }
.game-head h1 { margin: 0; }
.game-head .tagline { margin: 4px 0 0; }
.icon { width: 88px; height: 88px; border-radius: 20px; overflow: hidden; box-shadow: 0 10px 24px -10px var(--shadow); }
.icon svg { display: block; width: 100%; height: 100%; }

.pills { margin: 0 0 20px; }
.pill { display: inline-block; border: 1px solid var(--line); background: var(--surface); border-radius: 999px; padding: 4px 12px; margin: 0 6px 8px 0; font-size: 14px; color: var(--ink-dim); }

/* --- screenshots ------------------------------------------------------- */

/* The committed captures. A game page shows the whole set as ONE horizontally scrolling
   row rather than a wrapping grid: these are 390x844 phone frames, and a grid of them at
   any legible width is several screens tall. The row scrolls INSIDE itself, so the body
   never scrolls sideways, and each figure is a fixed width so the scroll has a rhythm.
   Every img carries its intrinsic width/height, so the boxes are reserved before a byte
   of PNG arrives and the captions below never jump. */
.shots { display: flex; gap: 18px; overflow-x: auto; overflow-y: hidden; padding: 4px 0 18px; margin: 0 0 24px; scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch; }
.shot { flex: 0 0 210px; margin: 0; scroll-snap-align: start; }
.shot img { display: block; width: 210px; height: auto; border-radius: 18px; background: var(--game-ground); border: 1px solid var(--line); box-shadow: 0 14px 30px -18px var(--shadow); }
.shot figcaption { font-size: 13px; line-height: 1.4; color: var(--ink-dim); margin-top: 10px; }

/* The landing page's ONE image per game, inside its feature card. */
.feature.has-shot { grid-template-columns: 96px minmax(0, 1fr) 168px; }
.feature-shot { margin: 0; align-self: center; }
.feature-shot img { display: block; width: 168px; height: auto; border-radius: 18px; background: var(--game-ground); border: 1px solid var(--line); box-shadow: 0 14px 30px -18px var(--shadow); }

/* "In development" is a label, not a decoration: it has to read as a qualifier on the
   name it follows, so it sits inline with the heading and borrows the sun — the one
   colour here that means "look at this" rather than "go". */
.badge { display: inline-block; vertical-align: middle; font-size: 13px; font-weight: 700; color: var(--signal-ink); background: var(--signal); border-radius: 999px; padding: 3px 12px; margin-left: 10px; position: relative; top: -2px; }
h1 .badge { font-size: 15px; }
a.card .badge, .feature h3 .badge { margin-left: 8px; }
.dev-notice { border-left-color: var(--signal-edge); }

/* The press kit's asset list: one link per file, with what it is beside it. */
.assets { list-style: none; padding: 0; margin: 0 0 20px; max-width: 72ch; border-top: 1px solid var(--line); }
.assets li { padding: 12px 0; border-bottom: 1px solid var(--line); margin: 0; }
.assets li a { font-weight: 700; }
.assets li .small { display: block; margin-top: 2px; }

/* --- breadcrumbs -------------------------------------------------------
   The visible half of the BreadcrumbList every inner page already emitted as JSON-LD.
   Same array, same order: a trail a machine can read and a reader cannot see was half
   a feature. It sits above the h1 and is the first thing in <main>. */

.crumbs { margin: 0 0 30px; font-size: 14px; color: var(--ink-faint); }
.crumbs ol { list-style: none; display: flex; flex-wrap: wrap; gap: 0 6px; margin: 0; padding: 0; max-width: none; }
.crumbs li { margin: 0; }
.crumbs li + li::before { content: "/"; color: var(--ink-faint); margin-right: 6px; }
.crumbs a { color: var(--ink-dim); text-decoration-color: var(--ink-faint); }
.crumbs a:hover { color: var(--ink); }
.crumbs [aria-current="page"] { color: var(--ink-faint); }

/* The current section in the primary nav. A weight change alone is not a distinction a
   reader with low vision can rely on, so it carries the sun underline as well — the same
   colour the footer's link hover uses for "you are here / this is the one". */
.topbar nav a[aria-current="page"] { color: var(--ink); text-decoration: underline; text-decoration-color: var(--signal-edge); text-decoration-thickness: 3px; text-underline-offset: 6px; }

/* --- where you can play it --------------------------------------------
   Three columns on a wide screen, stacked under 700px. The state word carries a dot
   whose colour is the whole signal, so it is never the only signal: the word "Playable
   now" / "Not yet" is written out beside it. */

.where { list-style: none; padding: 0; margin: 0 0 24px; max-width: 72ch; border-top: 1px solid var(--line); }
.where li { display: grid; grid-template-columns: minmax(0, 12em) minmax(0, 9em) minmax(0, 1fr); gap: 4px 18px; align-items: baseline; padding: 14px 0; border-bottom: 1px solid var(--line); margin: 0; }
.where .where-name { font-weight: 700; }
.where .where-state { font-size: 14px; color: var(--ink-dim); white-space: nowrap; }
.where .where-state::before { content: ""; display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 7px; vertical-align: 1px; background: var(--ink-faint); }
.where .is-live .where-state { color: var(--accent-deep); font-weight: 700; }
.where .is-live .where-state::before { background: var(--accent); }
.where .where-note { font-size: 15px; color: var(--ink-dim); }

/* --- the studio pages --------------------------------------------------
   The crest is the site's own artwork and costs no new bytes: the studio mark at display
   size with each registered game's real brand icon beside it, all SVG already in the
   repository. It grows a tile when a game is registered. */

.crest { display: flex; align-items: center; gap: 14px; margin: 0 0 26px; }
.crest-mark { width: 76px; height: 76px; border-radius: 20px; box-shadow: 0 0 0 1px var(--hairline), 0 14px 26px -14px var(--shadow); }
.crest-mark svg, .crest-icon svg { display: block; width: 100%; height: 100%; }
.crest-icon { width: 56px; height: 56px; border-radius: 15px; overflow: hidden; box-shadow: 0 10px 20px -10px var(--shadow); }
.crest-icon:nth-of-type(odd) { transform: rotate(-4deg); }
.crest-icon:nth-of-type(even) { transform: rotate(3deg) translateY(2px); }

/* A lead paragraph that is the page's own summary rather than a hero line wants the
   measure of body copy, not the 44ch of a headline deck. */
.lead.wide { max-width: 60ch; }

.facts th { text-align: left; font-size: 13px; letter-spacing: .02em; text-transform: uppercase; color: var(--ink-faint); font-weight: 700; padding: 0 12px 8px 0; border-bottom: 1px solid var(--line); }

/* The roadmap ladder. An ordered list drawn as a rail: the marker's fill says done /
   in progress / not started, and the state is ALSO spelled out beside the heading, so
   the drawing is never carrying the meaning by itself. */
.stages { list-style: none; padding: 0; margin: 0 0 24px; max-width: 72ch; }
.stage { display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 0 16px; margin: 0; padding: 0 0 26px; position: relative; }
.stage::before { content: ""; position: absolute; left: 10px; top: 22px; bottom: 0; width: 2px; background: var(--line); }
.stage:last-child::before { display: none; }
.stage-mark { width: 22px; height: 22px; border-radius: 50%; margin-top: 3px; border: 2px solid var(--ink-faint); background: var(--paper); position: relative; z-index: 1; }
.stage.is-done .stage-mark { background: var(--accent); border-color: var(--accent); }
.stage.is-now .stage-mark { background: var(--signal); border-color: var(--signal-edge); box-shadow: 0 0 0 5px rgba(255,194,75,.32); }
.stage h3 { margin-bottom: 6px; }
.stage p { color: var(--ink-dim); margin: 0; max-width: 62ch; }
.stage-state { display: inline-block; font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--ink-faint); border: 1px solid var(--line); border-radius: 999px; padding: 2px 9px; margin-left: 8px; vertical-align: 2px; }
.stage.is-done .stage-state { color: var(--accent-deep); border-color: var(--accent-faint); }
/* --signal-ink, never --signal-text: the text token flips to the yellow itself in the dark
   theme, and yellow on yellow is the pill disappearing. 8.65:1 in both themes. */
.stage.is-now .stage-state { color: var(--signal-ink); background: var(--signal); border-color: #C08B14; }

/* Questions. Each answer is one self-contained paragraph and is styled to be read as a
   unit, because that is how it is meant to be quoted. */
.qa { border-top: 1px solid var(--line); max-width: 72ch; }
.qa .q { padding: 22px 0 6px; border-bottom: 1px solid var(--line); }
.qa .q h2 { font-size: 21px; margin: 0 0 8px; letter-spacing: -0.01em; }
.qa .q p { color: var(--ink-dim); }

/* --- the self-hosted playable page ------------------------------------ */

.embed-stage { background: var(--game-ground); border: 8px solid var(--device); border-radius: 30px; overflow: hidden; max-width: 390px; margin: 0 auto 18px; box-shadow: 0 30px 50px -24px var(--shadow); }
.embed-stage iframe { display: block; width: 100%; aspect-ratio: 390 / 844; border: 0; }

/* --- devlog ------------------------------------------------------------ */

.entry-date { color: var(--ink-dim); font-size: 14px; margin: 0 0 20px; }
article.post h2 { font-size: 24px; }
article.post h3 { font-size: 19px; color: var(--ink); margin-top: 24px; }
article.post p, article.post ul { max-width: 68ch; }

/* --- motion ------------------------------------------------------------ */

@keyframes rise { to { opacity: 1; transform: none; } }
@keyframes settle { to { opacity: 1; transform: rotate(-3deg) translateY(0); } }

@media (prefers-reduced-motion: reduce) {
  .hero-copy > *, .phone { animation: none; opacity: 1; transform: none; }
  .btn.primary:hover, a.post:hover, a.card:hover { transform: none; }
  /* The skip link still MOVES here — it has to, that is how it becomes visible — but it
     arrives instantly rather than sliding. `transform: none` would strand it on screen. */
  .skip { transition: none; }
}

/* --- responsive -------------------------------------------------------- */

@media (max-width: 900px) {
  .hero { grid-template-columns: 1fr; padding-top: 56px; gap: 8px 0; }
  .hero .phone { margin-top: 20px; }
  .hero h1 { font-size: 58px; max-width: 12ch; }
  h1 { font-size: 44px; }
  h2 { font-size: 27px; margin-top: 56px; }
  .two-col { grid-template-columns: 1fr; }
  .promises { grid-template-columns: 1fr; }
  footer.site .wrap { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  footer.site .brand { grid-column: 1 / -1; }
  .phone { width: 280px; }
  .phone-shell { width: 280px; height: 586px; padding: 9px; border-radius: 38px; }
  .phone-screen { width: 262px; height: 568px; border-radius: 30px; }
  .phone-screen iframe { transform: scale(0.6718); }
  .tray { gap: 8px; }
  .tile { width: 52px; height: 52px; padding: 7px; border-radius: 12px; }
  /* The feature card's third column is the first thing to go: at this width the copy
     needs the room more than the screenshot does, and the strip on the game's own page
     is where the images are the point. */
  .feature.has-shot { grid-template-columns: 96px minmax(0, 1fr); }
  .feature-shot { grid-column: 1 / -1; justify-self: start; }
}

@media (max-width: 480px) {
  html, body { font-size: 16px; }
  .wrap { padding: 0 18px; }
  /* The nav keeps its 44px height and gives up its side padding: five items at 10px a side
     wrap to a second 44px row at this width, which is most of the first screen spent on a
     menu. The hit area is what the guidance is about, and it is the height that carries it. */
  .topbar nav { gap: 0 2px; }
  .topbar nav a { padding: 0 7px; font-size: 14.5px; }
  h1 { font-size: 36px; }
  h2 { font-size: 24px; margin-top: 48px; }
  .hero { padding-top: 40px; }
  .hero h1 { font-size: 46px; }
  .hero .lead { font-size: 19px; }
  .lead { font-size: 19px; }
  main.wrap > h1::before, main.wrap > h2::before,
  .section > h2::before, .two-col > div > h2::before { width: 36px; margin-bottom: 16px; }
  footer.site .wrap { padding-top: 44px; padding-bottom: 40px; }
  .feature { grid-template-columns: 1fr; padding: 22px; }
  .game-head { grid-template-columns: 64px minmax(0, 1fr); }
  .icon, .game-head .icon { width: 64px; height: 64px; border-radius: 16px; }
  .facts-list li { grid-template-columns: 1fr; gap: 2px; }
  .tile:nth-child(n+6) { display: none; }
  .feature.has-shot { grid-template-columns: 1fr; }
  .shot { flex: 0 0 168px; }
  .shot img { width: 168px; }
  .badge { margin-left: 8px; }
  /* At phone width the where-to-play row stacks: three columns of 12em/9em/rest do not
     fit, and the state has to stay on the same line as nothing. */
  .where li { grid-template-columns: 1fr; gap: 2px; }
  .where .where-state { order: -1; }
  .crest-mark { width: 60px; height: 60px; border-radius: 16px; }
  .crest-icon { width: 46px; height: 46px; border-radius: 12px; }
  .stage { grid-template-columns: 26px minmax(0, 1fr); gap: 0 12px; }
  .stage::before { left: 8px; }
  .stage-mark { width: 18px; height: 18px; }
  /* Three columns of game / genre / status do not fit a phone: the game name wraps to
     three lines to leave room for a one-word genre nobody came for. The genre is on each
     game's own page, one tap away, so it is the column that goes. */
  .standings td:nth-child(2), .standings th:nth-child(2) { display: none; }
  .standings td:first-child { width: 55%; }
}

/* per-game palettes, from games/<slug>/game.json "brand" */
body.g-hush-tiles { --game-ground: #0B1026; --game-accent: #45D4E8; }
body.g-sort { --game-ground: #F4EFE6; --game-accent: #C4562F; }
