/*
 * joinus.games
 *
 * THE :root BLOCK IS TRANSCRIBED FROM `src/theme/tokens.ts` AND IS CHECKED
 * AGAINST IT. `site/site.test.ts` imports the app's tokens and fails if any
 * colour or type step here disagrees -- the site and the app are one brand, and
 * "same house, louder room" is the UX Direction's phrase for it. Do not tune a
 * value here; tune the token and re-run the test.
 *
 * The site cannot import the app's stylesheet because the app has none: its
 * styles are React Native `StyleSheet` objects. See docs/plans/homepage-plan.md
 * section 1.3 for why this page redraws rather than shares.
 */

:root {
  /* color, from src/theme/tokens.ts */
  --color-bg: #12101a;
  --color-surface: #1e1b2b;
  --color-surface-raised: #2a2540;
  --color-ink: #f6f4ff;
  --color-ink-muted: #a29fbb;
  --color-accent: #ffb020;
  --color-accent-ink: #1a1305;
  --color-correct: #3dd68c;
  --color-wrong: #ff6b6b;
  --color-double: #c77dff;
  --color-border: #332e47;
  --color-border-strong: #7a7396;

  /* type, from src/theme/tokens.ts */
  --type-display-size: 40px;
  --type-display-line: 48px;
  --type-title-size: 28px;
  --type-title-line: 34px;
  --type-heading-size: 22px;
  --type-heading-line: 28px;
  --type-body-size: 16px;
  --type-body-line: 26px;
  --type-caption-size: 13px;
  --type-caption-line: 18px;

  /* space, radius and the touch target, same source */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 12px;
  --space-base: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-xxl: 48px;
  --space-xxxl: 64px;
  --space-gutter: 20px;
  --radius-card: 12px;
  --radius-pill: 999px;
  --radius-sheet: 16px;
  --radius-token: 18px;
  --radius-compact: 14px;

  /* The edges a token stands on, from the app's tokens: the buttons are the
     app's Game Box tokens, on this page's own palette. */
  --color-key-line: #0a0d1e;
  --color-accent-edge: #b36f00;
  --touch-min: 44px;
  --duration-state: 200ms;

  /* The site's own reading column. Wider than the app's 560pt handset column:
     this is a web page read on a laptop, not a screen inside a phone chassis. */
  --column-quiet: 680px;
  --column-rules: 800px;
  --measure-rules: 40em;
  --column-wide: 1120px;

  --font-display: 'Bricolage Grotesque', 'Trebuchet MS', 'Segoe UI', sans-serif;
  /* The box's voice: headings, game names and the play buttons, as in the app. */
  --font-box: 'Grandstander Black', 'Trebuchet MS', 'Segoe UI', sans-serif;
  --font-body: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/*
 * Self-hosted, and that is a requirement rather than a preference: the approved
 * privacy policy tells the visitor this page asks nothing of another service,
 * and Google Fonts would be two hosts it does not name (PRD criterion 18).
 * Subset by `site/fonts.mjs`; see its header.
 *
 * `swap` on purpose -- the text is readable from the first paint and the fallback
 * stacks above are sized so the swap does not push the page down.
 */
@font-face {
  font-family: 'Grandstander Black';
  src: url('fonts/Grandstander_900Black.woff2') format('woff2');
  font-weight: 900;
  font-display: swap;
}

@font-face {
  font-family: 'Bricolage Grotesque';
  src: url('fonts/BricolageGrotesque_800ExtraBold.woff2') format('woff2');
  font-weight: 800;
  font-display: swap;
}

@font-face {
  font-family: 'Bricolage Grotesque';
  src: url('fonts/BricolageGrotesque_600SemiBold.woff2') format('woff2');
  font-weight: 600;
  font-display: swap;
}

@font-face {
  font-family: Inter;
  src: url('fonts/Inter_400Regular.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: Inter;
  src: url('fonts/Inter_500Medium.woff2') format('woff2');
  font-weight: 500;
  font-display: swap;
}

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

html {
  /* Never hijacked, per the design spec -- this is the only scroll behaviour the
     site sets, and it applies to the quiet pages' contents links. */
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
  -webkit-text-size-adjust: 100%;
}

a {
  color: var(--color-ink);
}

/* Underlined, never colour alone -- docs/standards/design.md forbids colour as
   the only carrier of meaning, and a link is meaning. */
main a {
  text-decoration: underline;
  text-underline-offset: 2px;
}

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: var(--space-xs);
}

img {
  max-width: 100%;
}

/* ---------------------------------------------------------------- chrome
 *
 * REPEATED VERBATIM IN EVERY PAGE, between the BEGIN/END chrome markers, and
 * `site.test.ts` asserts the copies are byte-identical. There is no templating
 * step and that is deliberate (plan section 1.2); B049 records what it costs and
 * what would change the answer.
 */

.chrome {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-base);
  padding: var(--space-md) var(--space-gutter);
  border-bottom: 1px solid var(--color-border);
  background: var(--color-bg);
}

.chrome-mark {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--type-heading-size);
  line-height: var(--type-heading-line);
  text-decoration: none;
  letter-spacing: -0.01em;
}

.chrome-mark .us {
  color: var(--color-accent);
}

/* TOKENS, as the app's buttons are (design-refresh, *Token*): a face standing
   on a 5px edge, the edge's height kept below it so a press moves nothing
   around it. Pressed, the face sinks until 1px of edge shows. */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-min);
  margin-bottom: 5px;
  padding: 0 var(--space-lg);
  border-radius: var(--radius-token);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--type-body-size);
  text-decoration: none;
  transition:
    transform 90ms ease,
    box-shadow 90ms ease;
}

.button:active {
  transform: translateY(4px);
}

/* Amber: the next thing to press, in the box's voice. */
.button-primary {
  background: var(--color-accent);
  color: var(--color-accent-ink);
  box-shadow: 0 5px 0 var(--color-accent-edge);
  font-family: var(--font-box);
  font-weight: 400;
}

.button-primary:active {
  box-shadow: 0 1px 0 var(--color-accent-edge);
}

/* Navy: every other token. */
.button-outline {
  background: var(--color-surface-raised);
  color: var(--color-ink);
  box-shadow: 0 5px 0 var(--color-key-line);
}

.button-outline:active {
  box-shadow: 0 1px 0 var(--color-key-line);
}

.button-compact {
  min-height: 42px;
  padding: 0 var(--space-base);
  border-radius: var(--radius-compact);
  margin-bottom: 4px;
  font-size: 15px;
  box-shadow: 0 4px 0 var(--color-accent-edge);
}

.site-footer {
  border-top: 1px solid var(--color-border);
  padding: var(--space-xl) var(--space-gutter) var(--space-xxl);
}

/* Three columns -- games, site, language -- stacked on a phone. */
.footer-cols {
  display: grid;
  gap: var(--space-lg);
  max-width: var(--column-wide);
  margin: 0 auto;
}

.footer-heading {
  margin: 0 0 var(--space-sm);
  color: var(--color-ink);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-heading-size);
  line-height: var(--type-heading-line);
}

.site-footer nav {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--space-base);
}

.site-footer a,
.site-footer nav > span {
  display: flex;
  align-items: center;
  min-height: var(--touch-min);
  color: var(--color-ink-muted);
  /* Between caption and body: the footer's links, a step under the page's text. */
  font-size: 14px;
  line-height: 20px;
}

.site-footer a {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The current game or language: plain text, in ink, marked current. */
.site-footer nav > [aria-current] {
  color: var(--color-ink);
}

@media (min-width: 900px) {
  /* Three equal columns across the wide column. Below 900 the Games third is
     too narrow for its two lists. */
  .footer-cols {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: 0;
  }

  /* A rule between columns, centred in the space between them. */
  .footer-col + .footer-col {
    padding-left: var(--space-xxl);
    border-left: 1px solid var(--color-border);
  }

  .footer-col:not(:last-child) {
    padding-right: var(--space-xxl);
  }

  .site-footer nav:not(.site-games) {
    grid-template-columns: 1fr;
  }
}

/* ------------------------------------------------------------ quiet pages
 *
 * S-HOME2 and S-HOME3. One reading column, no motion at all.
 */

.quiet {
  max-width: var(--column-quiet);
  margin: 0 auto;
  padding: var(--space-xxl) var(--space-gutter);
}

.quiet h1 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
}

.quiet h2 {
  margin: var(--space-xl) 0 var(--space-sm);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-heading-size);
  line-height: var(--type-heading-line);
}

.quiet p,
.quiet li {
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
}

.quiet .date {
  margin-top: var(--space-sm);
  color: var(--color-ink-muted);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
}

/* A table is the one thing on these pages that cannot reflow, so it scrolls in
   its own container rather than shrinking its type (design spec, S-HOME2). */
.quiet .table-scroll {
  overflow-x: auto;
}

.quiet table {
  border-collapse: collapse;
  min-width: 480px;
}

.quiet th,
.quiet td {
  border-bottom: 1px solid var(--color-border);
  padding: var(--space-sm) var(--space-md);
  text-align: left;
  vertical-align: top;
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
}

.back {
  display: inline-block;
  margin-top: var(--space-xxl);
}

@media (min-width: 600px) {
  .quiet {
    padding-inline: var(--space-xl);
  }
}

/* ------------------------------------------------------------------- hero
 *
 * S-HOME1. Designed at 360pt: the characters, the wordmark, the line and
 * **Play now** all fit the first 640pt of a phone.
 */

.hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--space-xl) var(--space-gutter) var(--space-xxl);
}

/* THE TRIO. Each outer character tucks about a fifth of its width behind the
   middle one, which is drawn last so it sits in front, and is raised a little
   so the group reads as a huddle rather than a row. */
.trio {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  margin-bottom: var(--space-lg);
}

/* `flex: none` because a flex item shrinks by default, and these did: every
   width below was being quietly reduced to fit, so the characters came out
   smaller than asked for and their fixed overlap no longer covered a fifth of
   them. They looked like a row standing apart rather than a huddle. */
.ch-slot {
  flex: none;
  width: 96px;
  display: flex;
  justify-content: center;
}

/* The bob's wrapper is a flex item too, so it needed its own width -- without
   it the drawing inside sized to the SVG's own 100 units and every breakpoint
   below did nothing at all. */
.ch-bob {
  display: block;
  flex: none;
  width: 100%;
}

.ch-slot svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* Left: smaller, leaning out of the group about its feet, and 3px further out.
   The lean is on the slot rather than the drawing so the tail tucks behind the
   middle one rather than swinging in front of it. */
.ch-0 {
  width: 72px;
  margin-right: -18px;
  transform: translate(-3px, -6px) rotate(-14deg);
  transform-origin: 50% 100%;
}

/* Middle: in front, and raised. */
.ch-1 {
  position: relative;
  z-index: 1;
  transform: translateY(-8px);
}

/* Right: behind, and upright -- it already stands on its point. */
.ch-2 {
  margin-left: -18px;
}

.wordmark {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 64px;
  line-height: 68px;
  letter-spacing: -0.02em;
}

.wordmark .us {
  color: var(--color-accent);
}

.tagline {
  margin: 0;
  color: var(--color-ink-muted);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
}

.hero-line {
  max-width: 22ch;
  margin: var(--space-lg) 0 var(--space-xl);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-heading-size);
  line-height: var(--type-heading-line);
}

.button-hero {
  min-width: 220px;
  font-size: var(--type-heading-size);
  min-height: 58px;
}

/* THE OVERLAP SCALES WITH THE CHARACTERS. Each outer one tucks about a fifth of
   its width behind the middle, and a fixed -18px held that only at phone size --
   at 176px they stood apart like a row rather than a huddle. Owner-reported. */
@media (min-width: 600px) {
  .ch-slot {
    width: 148px;
  }
  .ch-0 {
    width: 111px;
    margin-right: -28px;
  }
  .ch-2 {
    margin-left: -28px;
  }
  .hero-line {
    max-width: 28ch;
  }
}

@media (min-width: 1024px) {
  .wordmark {
    font-size: 120px;
    line-height: 120px;
  }
  .ch-slot {
    width: 224px;
  }
  .ch-0 {
    width: 168px;
    margin-right: -42px;
    transform: translate(-5px, -10px) rotate(-14deg);
  }
  .ch-1 {
    transform: translateY(-14px);
  }
  .ch-2 {
    margin-left: -42px;
  }
}

@media (max-width: 340px) {
  .wordmark {
    font-size: 56px;
    line-height: 60px;
  }
}

/* --------------------------------------------------------- the hero's motion
 *
 * EVERY ANIMATION ON THIS SITE IS SWITCHED OFF IN ONE PLACE, at the bottom of
 * this file, and `site.test.ts` fails if a new one is not covered there.
 *
 * Nothing here runs until `site.js` adds `.motion` to the root, and nothing
 * here carries information: the resting state below IS the picture.
 */

/* THE BOB RUNS A WHOLE NUMBER OF CYCLES AND ENDS AT REST.
 *
 * It used to be `infinite` with the class pulled off after five seconds, and
 * that is a stop mid-air: the element snapped back to zero wherever it happened
 * to be. Owner-reported. A finite count ends on the keyframe it started from, so
 * the group settles instead of dropping -- and WCAG 2.2.2's five seconds becomes
 * a property of the CSS rather than a promise the script has to keep.
 *
 * Three periods that do not divide into each other, so the three never sync.
 */
.motion .trio.alive .ch-0 .ch-bob {
  animation: bob 1.4s ease-in-out 3;
}

.motion .trio.alive .ch-1 .ch-bob {
  animation: bob 1.2s ease-in-out 4;
}

.motion .trio.alive .ch-2 .ch-bob {
  animation: bob 1.6s ease-in-out 3;
}

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

/* The teardrop leans a further 6 degrees out and back, once, as the page opens.
   ON THE SVG, while the bob is on the wrapper: two `animation` declarations on
   one element means the loser never runs. */
.motion .ch-0 svg {
  animation: lean-out 1.4s ease-in-out 0.2s 1;
  transform-origin: 50% 100%;
}

@keyframes lean-out {
  0%,
  100% {
    rotate: 0deg;
  }
  45% {
    rotate: -6deg;
  }
}

/* ------------------------------------------------------------- the header
 *
 * Over the hero it is out of the way; past it, it slides down and holds the
 * mark and a compact Play now.
 */

/* ONLY THE COVER PAGE HIDES ITS HEADER. The quiet pages are read top to bottom
   and their header is simply always there, in the flow -- a store reviewer
   landing straight on /privacy should not have to scroll to find the way out.
   So this is scoped to `.home` rather than applied to `.chrome` everywhere. */
.home .chrome {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  background: rgb(18 16 26 / 92%);
  backdrop-filter: blur(6px);
}

.motion .home .chrome,
.home.motion .chrome {
  transform: translateY(-100%);
  transition: transform var(--duration-state) ease;
}

.motion .home.past-hero .chrome,
.motion.past-hero .home .chrome {
  transform: translateY(0);
}

/* The header only joins the tab order while it is on screen. */
.motion:not(.past-hero) .home .chrome {
  visibility: hidden;
}

@media (prefers-reduced-transparency: reduce) {
  .home .chrome {
    background: var(--color-bg);
    backdrop-filter: none;
  }
}

/* ------------------------------------------------- the hero, side by side
 *
 * Desktop puts the trio at 1.5x beside the wordmark and the line, rather than
 * above them (design spec, Responsive Behavior).
 */

@media (min-width: 1024px) {
  .hero {
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: var(--space-xxl);
    max-width: var(--column-wide);
    margin: 0 auto;
    text-align: left;
  }

  .hero .trio {
    margin-bottom: 0;
    flex-shrink: 0;
  }

  .hero-words {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
  }

  .hero-line {
    max-width: 18ch;
  }
}

/* ------------------------------------------------------- reduced motion
 *
 * ONE BLOCK, AND IT COVERS EVERY ANIMATION ON THE SITE. Every scene and every
 * idle motion rests on its final frame, so this only has to stop things --
 * nothing here has to put anything back.
 */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
  }

  .home .chrome {
    transform: none;
    visibility: visible;
  }
}

/* ------------------------------------------------- Pick your poison
 *
 * One stage per public game. The words are generated from the registry
 * (`site/build.mjs`); the colours are here, because the alternative is an inline
 * `style` attribute and `_headers` forbids inline styles on purpose.
 *
 * EACH GAME'S ACCENT IS THE REGISTRY'S. `site.test.ts` reads both and fails if
 * they differ, so a retuned accent in the app is a failing test rather than two
 * products that disagree about what colour a game is.
 */

[data-game='singularity'] {
  --accent: #39ff14;
}

[data-game='drift'] {
  --accent: #5ac8fa;
}

[data-game='dead_giveaway'] {
  --accent: #ffb020;
}

[data-game='okey'] {
  --accent: #3dd68c;
}

[data-game='smear'] {
  --accent: #c77dff;
}

[data-game='yacht'] {
  --accent: #f2e7d5;
}

[data-game='frontier'] {
  --accent: #da754c;
}

[data-game='cats'] {
  --accent: #e08fa0;
}

[data-game='warriors'] {
  --accent: #ff6b6b;
}

.games {
  max-width: var(--column-wide);
  margin: 0 auto;
  padding: var(--space-xl) var(--space-gutter) var(--space-xxl);
}

.games-heading {
  margin: 0 0 var(--space-lg);
  font-family: var(--font-box);
  font-weight: 400;
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
  text-align: center;
}

.stages {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

/* A `surface` panel with the game's accent as a rail down its left edge. The
   EDGE stays `borderStrong` and the accent goes in the rail -- the front door's
   card rule, so a stage and a dashboard card read as the same object. */
.stage {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--color-border-strong);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius-card);
  background: var(--color-surface);
}

/* The scene's box. The drawing inside it rests at its FINAL FRAME -- the CSS
   below moves pieces in from elsewhere, never out -- so the picture is whole
   before any motion, with reduced motion on, and with the script off. */
/* THE SCENE'S GROUND IS THE STAGE'S OWN `surface`, not `surface-raised`. The
   spec draws the stage as a `surface` panel and the pieces on it as
   `surface-raised`; having both be `surface-raised` made three story cards
   invisible against the ground they were lying on. */
.scene {
  height: 240px;
  padding: var(--space-md);
  background: var(--color-surface);
}

.scene .art {
  display: block;
  width: 100%;
  height: 100%;
}

.scene .faint {
  opacity: 0.45;
}

.stage-words {
  padding: var(--space-lg);
}

.stage-name {
  margin: 0;
  font-family: var(--font-box);
  font-weight: 400;
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
}

/* THE WHOLE CARD IS THE LINK to the game's rules page. The link is the name,
   so its accessible name is the game's; a layer from it covers the card, and
   the Play button sits above that layer and still hosts the game directly. An
   anchor cannot hold another anchor, which is why it is a layer and not a
   wrapper. */
.stage-name a {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
}

.stage-name a::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
}

.stage-name a:focus-visible {
  outline: none;
}

.stage:has(.stage-name a:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.stage-words .button {
  position: relative;
  z-index: 2;
}

.stage:has(.stage-name a) {
  transition:
    transform var(--duration-state) ease-out,
    border-color var(--duration-state) ease-out;
}

.stage:has(.stage-name a):hover {
  transform: translateY(-2px);
  border-color: var(--color-ink-muted);
}

/* On `surface`, where registry.test.ts already measures every accent at 4.5:1
   or better. Nowhere else on this page does an accent carry text. */
.stage-tagline {
  margin: var(--space-xs) 0 0;
  color: var(--accent);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
}

.stage-line {
  margin: var(--space-md) 0 0;
  color: var(--color-ink);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
}

.stage-meta {
  margin: var(--space-sm) 0 var(--space-lg);
  color: var(--color-ink-muted);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
}

@media (min-width: 1024px) {
  .scene {
    height: 320px;
  }

  /* Two columns, scene and words, alternating sides from one game to the next
     (design spec, Responsive Behavior). */
  .stage {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: center;
  }

  .stage .scene {
    height: 100%;
    min-height: 320px;
  }

  .stage:nth-child(even) .scene {
    order: 2;
  }

  .stage-words {
    padding: var(--space-xl);
  }
}

/* ------------------------------------------------------------- the scenes
 *
 * EVERY RULE HERE MOVES A PIECE *TO* WHERE IT ALREADY IS. The scene's resting
 * state is its final frame, so each keyframe starts somewhere else and ends at
 * `none`. Nothing is animated out of view, which is what makes the picture whole
 * before the motion, under reduced motion, and with the script off.
 *
 * A scene plays when its stage is 40% in view -- `site.js` adds `.seen` -- and
 * plays once. It replays only if the stage leaves the viewport entirely.
 */

/* The screen's glass: a glow on dark, not a lamp. The glyphs are the accent, so
   the wash has to stay faint enough for them to read against it. */
.sg-wash {
  opacity: 0.14;
}

.stage-words > * {
  /* The words rise and fade in; their resting state is where they belong.
     BACKWARDS, NOT BOTH: a fill that outlives the animation leaves a transform
     on the name, which traps the link layer that makes the stage clickable. */
  animation-fill-mode: backwards;
}

/* A SCENE MAY NOT SHOW ITS ENDING AND THEN START OVER.
 *
 * The resting state is the final frame, which is what makes the picture whole
 * without motion -- but with motion on it meant the finished picture appeared
 * on load and then snapped back to begin. Owner-reported.
 *
 * So a scene armed and not yet played is simply not there, and its own pieces
 * bring it in. `armed` comes from the script and only where there is an
 * observer to reveal them, so no-script and reduced-motion still show the frame.
 */
.armed .stage:not(.seen) .scene,
.armed .stage:not(.seen) .stage-words > * {
  opacity: 0;
}

.motion .stage.seen .stage-words > * {
  animation: rise 200ms ease-out backwards;
}

.motion .stage.seen .stage-words > :nth-child(1) {
  animation-delay: 60ms;
}
.motion .stage.seen .stage-words > :nth-child(2) {
  animation-delay: 120ms;
}
.motion .stage.seen .stage-words > :nth-child(3) {
  animation-delay: 180ms;
}
.motion .stage.seen .stage-words > :nth-child(4) {
  animation-delay: 240ms;
}
.motion .stage.seen .stage-words > :nth-child(5) {
  animation-delay: 300ms;
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
}

/* -- Dead Giveaway: dealt in a fan, the middle one turned, three faces watching */
.motion .stage.seen .dg-card {
  animation: deal 900ms cubic-bezier(0.2, 0.8, 0.3, 1) both;
}
.motion .stage.seen .dg-1 {
  animation:
    deal 900ms cubic-bezier(0.2, 0.8, 0.3, 1) 150ms both,
    turn 600ms ease-in-out 1000ms both;
}
.motion .stage.seen .dg-2 {
  animation-delay: 300ms;
}
.motion .stage.seen .dg-face {
  animation: peek 500ms ease-out 1500ms both;
}

@keyframes deal {
  from {
    opacity: 0;
    translate: -120px 40px;
  }
}
@keyframes turn {
  50% {
    transform: scaleX(0.02);
  }
}
@keyframes peek {
  from {
    opacity: 0;
    translate: 0 34px;
  }
}

/* -- Okey: tiles slide in from the right and click into a run, the last from above */
.motion .stage.seen .ok-tile {
  animation: slide-in 500ms cubic-bezier(0.2, 0.9, 0.3, 1) both;
}
.motion .stage.seen .ok-1 {
  animation-delay: 180ms;
}
.motion .stage.seen .ok-2 {
  animation: drop-in 520ms cubic-bezier(0.3, 1.5, 0.5, 1) 420ms both;
}

@keyframes slide-in {
  from {
    opacity: 0;
    translate: 150px 0;
  }
}
@keyframes drop-in {
  from {
    opacity: 0;
    translate: 0 -90px;
  }
}

/* -- Drift: a scribble draws itself, then a second sheet redraws it */
.dr-ink {
  stroke-dasharray: 1;
}
.motion .stage.seen .dr-cat {
  animation: draw 1100ms ease-in-out both;
}
.motion .stage.seen .dr-b {
  animation: slide-in 600ms ease-out 1200ms both;
}
.motion .stage.seen .dr-moon {
  animation: draw 900ms ease-in-out 1700ms both;
}
.motion .stage.seen .dr-flag {
  animation: draw 500ms ease-in-out 2600ms both;
}

@keyframes draw {
  from {
    stroke-dashoffset: 1;
  }
  to {
    stroke-dashoffset: 0;
  }
}

/* -- Smear: three cards fan out of the stack, and the spade slaps back down */
.motion .stage.seen .sm-card {
  animation: fan 600ms cubic-bezier(0.2, 0.9, 0.3, 1) both;
}
.motion .stage.seen .sm-1 {
  animation:
    fan 600ms cubic-bezier(0.2, 0.9, 0.3, 1) 120ms both,
    slap 420ms cubic-bezier(0.3, 0, 0.6, 1.6) 900ms both;
}
.motion .stage.seen .sm-2 {
  animation-delay: 240ms;
}

@keyframes fan {
  from {
    opacity: 0;
    translate: -46px 22px;
    rotate: -18deg;
  }
}
@keyframes slap {
  40% {
    translate: 0 -26px;
  }
}

/* -- Singularity: the glass warms up (twice, never three times), then it looks at you */
.motion .stage.seen .sg-wash {
  animation: warm-up 700ms steps(1, end) both;
}
.motion .stage.seen .sg-eye {
  animation: open-eye 500ms ease-out 800ms both;
  transform-origin: center;
}
.motion .stage.seen .sg-eye-r {
  animation-delay: 950ms;
}
.motion .stage.seen .sg-type {
  animation: type 900ms steps(15, end) 1500ms both;
  clip-path: inset(0 0 0 0);
}

/* WCAG 2.3.1: two flashes, and never a third in any second. */
@keyframes warm-up {
  0%,
  40% {
    opacity: 0;
  }
  20%,
  60% {
    opacity: 0.14;
  }
}
@keyframes open-eye {
  from {
    scale: 1 0.05;
  }
}
@keyframes type {
  from {
    clip-path: inset(0 100% 0 0);
  }
}

/* -- Yacht: five dice tumble in, the last landing a beat late */
.motion .stage.seen .yc-die {
  animation: tumble 600ms cubic-bezier(0.2, 0.9, 0.3, 1) both;
}
.motion .stage.seen .yc-1 {
  animation-delay: 140ms;
}
.motion .stage.seen .yc-2 {
  animation-delay: 280ms;
}
.motion .stage.seen .yc-3 {
  animation-delay: 420ms;
}
.motion .stage.seen .yc-4 {
  animation-delay: 900ms;
}

@keyframes tumble {
  from {
    opacity: 0;
    translate: -130px -40px;
    rotate: -160deg;
  }
}

/* -- Frontier: three hexes click into an island, one road, one keep */
.motion .stage.seen .fr-hex {
  animation: settle 520ms cubic-bezier(0.3, 1.4, 0.5, 1) both;
  transform-origin: center;
}
.motion .stage.seen .fr-1 {
  animation-delay: 180ms;
}
.motion .stage.seen .fr-2 {
  animation-delay: 360ms;
}
/* The keep goes down first, then its road is laid from it. */
.motion .stage.seen .fr-keep {
  animation: drop-in 460ms cubic-bezier(0.3, 1.5, 0.5, 1) 900ms both;
}
.motion .stage.seen .fr-road {
  stroke-dasharray: 1;
  animation: draw 420ms ease-out 1400ms both;
}

@keyframes settle {
  from {
    opacity: 0;
    translate: 0 -40px;
    scale: 0.85;
  }
}

/* -- Sneaky Beans: three cats creep in, and the cone drops on the one in front */
.motion .stage.seen .ct-cat {
  animation: creep 620ms cubic-bezier(0.2, 0.9, 0.3, 1) both;
}
.motion .stage.seen .ct-1 {
  animation-delay: 220ms;
}
.motion .stage.seen .ct-2 {
  animation-delay: 440ms;
}
/* Last, and from above: the cone is what happens TO a cat, so it arrives after
   all three are sitting there. */
.motion .stage.seen .ct-cone {
  animation: drop-in 520ms cubic-bezier(0.3, 1.5, 0.5, 1) 900ms both;
  transform-origin: 120px 158px;
}

@keyframes creep {
  from {
    opacity: 0;
    translate: -26px 0;
  }
}

/* -- Doodle Duel: two warriors draw themselves, then the left one swings */
.dd-ink {
  stroke-dasharray: 1;
}
.motion .stage.seen .dd-tape {
  animation: settle 420ms cubic-bezier(0.3, 1.4, 0.5, 1) both;
}
.motion .stage.seen .dd-a {
  animation: swing 520ms cubic-bezier(0.3, 1.5, 0.5, 1) 2100ms both;
}
.motion .stage.seen .dd-b {
  animation: reel 520ms cubic-bezier(0.3, 1.4, 0.5, 1) 2200ms both;
}
/* Drawn one stroke at a time, left warrior first, in the order a person draws:
   head, spine, arms, legs. */
.motion .stage.seen .dd-a .dd-ink {
  animation: draw 320ms ease-out both;
}
.motion .stage.seen .dd-b .dd-ink {
  animation: draw 320ms ease-out both;
}
.motion .stage.seen .dd-a .dd-ink:nth-child(2) {
  animation-delay: 200ms;
}
.motion .stage.seen .dd-a .dd-ink:nth-child(3) {
  animation-delay: 400ms;
}
.motion .stage.seen .dd-a .dd-ink:nth-child(4) {
  animation-delay: 560ms;
}
.motion .stage.seen .dd-a .dd-ink:nth-child(5) {
  animation-delay: 700ms;
}
.motion .stage.seen .dd-b .dd-ink {
  animation-delay: 900ms;
}
.motion .stage.seen .dd-b .dd-ink:nth-child(2) {
  animation-delay: 1080ms;
}
.motion .stage.seen .dd-b .dd-ink:nth-child(3) {
  animation-delay: 1260ms;
}
.motion .stage.seen .dd-b .dd-ink:nth-child(4) {
  animation-delay: 1420ms;
}
.motion .stage.seen .dd-b .dd-ink:nth-child(5) {
  animation-delay: 1560ms;
}
/* The blow lands where the two meet, after both are on the paper. */
.motion .stage.seen .dd-hit {
  animation: land 420ms cubic-bezier(0.3, 1.6, 0.5, 1) 2250ms both;
  transform-origin: 126px 95px;
}

@keyframes swing {
  from {
    rotate: -14deg;
  }
}
@keyframes reel {
  from {
    rotate: 10deg;
    translate: 10px 0;
  }
}
@keyframes land {
  from {
    opacity: 0;
    scale: 0.2;
  }
}

/* ------------------------------------------------------- S-RULES1
 *
 * A game's page: the quiet pages' column, text held to 40em (about 75
 * characters), the game's homepage scene as its hero. `--accent` comes from the
 * `data-game` on `main`; a closing tile carries its own.
 */

.rules {
  max-width: var(--column-rules);
  margin: 0 auto;
  padding: var(--space-lg) var(--space-gutter) var(--space-xxl);
}

/* German compounds outgrow a phone's line; other languages wrap at spaces. */
.rules:lang(de) {
  hyphens: auto;
}

.rules p {
  max-width: var(--measure-rules);
  margin: 0 0 var(--space-base);
}

.stage.rules-stage {
  display: block;
}

.rules-stage .scene {
  height: 160px;
}

.rules h1 {
  margin: var(--space-lg) 0 0;
  font-family: var(--font-box);
  font-weight: 400;
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
}

/* On `bg`, where registry.test.ts measures every accent at 4.5:1 or better. */
.rules .rules-tagline {
  margin: var(--space-xs) 0 0;
  color: var(--accent);
}

.rules .rules-meta {
  margin: var(--space-sm) 0 var(--space-lg);
  color: var(--color-ink-muted);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
}

.rules h2 {
  margin: var(--space-xxl) 0 var(--space-sm);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-heading-size);
  line-height: var(--type-heading-line);
  scroll-margin-top: var(--space-base);
}

.rules h3 {
  margin: var(--space-lg) 0 var(--space-sm);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
}

.rules-contents ul {
  margin: var(--space-lg) 0 0;
  padding: 0;
  list-style: none;
}

.rules-contents a {
  display: flex;
  align-items: center;
  min-height: var(--touch-min);
}

.rules-close {
  margin-top: var(--space-xxl);
  text-align: center;
}

.rules .rules-more {
  margin-top: var(--space-xxl);
  font-family: var(--font-box);
  font-weight: 400;
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
}

.rules-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-md);
  margin: var(--space-lg) 0 0;
  padding: 0;
  list-style: none;
}

.rules-tile {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-sm);
  height: 100%;
  padding: var(--space-base);
  border: 1px solid var(--color-border-strong);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  font-family: var(--font-box);
  text-decoration: none;
  overflow-wrap: anywhere;
}

.rules-tile .mark {
  height: 40px;
  width: auto;
  max-width: 100%;
}

/* A sheet's lists: Yacht's categories and Frontier's cards as name and line,
   Smear's six points as the sheet's bullets. */
.rules dl,
.rules ul:not([class]) {
  max-width: var(--measure-rules);
  margin: 0 0 var(--space-base);
}

.rules dt {
  margin-top: var(--space-sm);
  font-weight: 500;
}

.rules dd {
  margin: 0;
  color: var(--color-ink-muted);
}

.rules .rules-aside {
  color: var(--color-ink-muted);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
}

/* The Dead Giveaway story, as the homepage's story card. */
.rules-story {
  max-width: var(--measure-rules);
  margin: 0 0 var(--space-base);
  padding: var(--space-base);
  border-radius: var(--radius-card);
  background: var(--color-surface-raised);
}

/* ---------------------------------------------------------- figures
 *
 * Drawn by site/figures.mjs. A `surface` panel with the accent rail; the
 * pieces scale with the column, never with the text.
 */

.rules-figure {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  margin: 0 0 var(--space-base);
  padding: var(--space-base);
  border: 1px solid var(--color-border-strong);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius-card);
  background: var(--color-surface);
}

.fig-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: center;
  gap: var(--space-md);
  max-width: 100%;
}

.fig-fit {
  flex-wrap: nowrap;
}

.fig-fit > *,
.fig-fit .fig-group > * {
  min-width: 0;
  flex: 0 1 auto;
}

/* Frontier's resource cards: three across on a phone, six from 600. */
.fig-cards {
  display: grid;
  grid-template-columns: repeat(3, auto);
  gap: var(--space-sm) var(--space-md);
}

.fig-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  max-width: 100%;
}

.fig-equals {
  align-self: center;
  color: var(--color-ink-muted);
  font-family: var(--font-box);
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
}

.fig-group {
  display: flex;
  gap: 2px;
  max-width: 100%;
}

.fig-art {
  display: block;
  max-width: 100%;
  max-height: 220px;
  height: auto;
}

.fig-group .fig-art {
  min-width: 0;
  flex: 0 1 auto;
}

.fig-label,
.fig-lead,
.fig-tally {
  max-width: 14em;
  color: var(--color-ink-muted);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  text-align: center;
}

.fig-lead {
  align-self: center;
  min-width: 1em;
  font-weight: 700;
}

.fig-tally {
  color: var(--accent);
}

/* Okey's rack: two tiers on a wooden shelf. */
.fig-rack {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  max-width: 100%;
  padding: var(--space-sm);
  border-radius: var(--space-sm);
  background: #5a3d2b;
}

.fig-raised {
  display: block;
  transform: translateY(-8px);
}

.fig-story {
  max-width: 22em;
  padding: var(--space-base);
  border-radius: var(--radius-card);
  background: var(--color-surface-raised);
  text-align: center;
}

/* Drift's written steps: the phrase on paper, the size of a drawn sheet. */
.fig-paper {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 104px;
  min-height: 82px;
  padding: var(--space-sm);
  border-radius: 6px;
  background: var(--color-ink);
  color: var(--color-bg);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
  text-align: center;
}

.fig-sheet {
  width: 104px;
}

.fig-chain .fig-item {
  width: 104px;
}

.fig-duel {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  max-width: 100%;
}

.fig-bubble {
  margin-left: 24px;
  padding: var(--space-xs) var(--space-sm);
  border: 2px solid var(--color-bg);
  border-radius: var(--space-md);
  background: var(--color-ink);
  color: var(--color-bg);
  font-family: var(--font-box);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line);
}

@media (max-width: 359px) {
  .rules-stage .scene {
    height: 140px;
  }
}

@media (min-width: 600px) {
  .rules {
    padding-inline: var(--space-xl);
  }

  .rules-stage .scene {
    height: 240px;
  }

  .rules-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .fig-art {
    max-height: 280px;
  }

  .fig-cards {
    grid-template-columns: repeat(6, auto);
  }
}

@media (min-width: 1024px) {
  .rules-stage .scene {
    min-height: 0;
  }
}

/* ----------------------------------------------------------- S-HOME4
 *
 * Somewhere you arrive by accident. The scene is Drift's final frame and it
 * never plays -- `.seen` is never added here, because there is no stage to
 * observe. A not-found page should not perform.
 */

.lost {
  text-align: center;
}

.lost-scene {
  height: 200px;
  border-radius: var(--radius-card);
  margin-bottom: var(--space-lg);
}

.lost h1 {
  margin-bottom: var(--space-xl);
}

/* ------------------------------------------------------- the closing band
 *
 * The last thing on the page, and the second place **Play now** appears -- the
 * spec repeats it so a visitor who read to the bottom does not have to scroll
 * back up to act.
 *
 * Its store row is the hero's, from the same markup (see *getting in* below).
 */

.band {
  position: relative;
  margin-top: var(--space-xxl);
  padding: 0 var(--space-gutter) var(--space-xxl);
  background: var(--color-surface-raised);
  text-align: center;
}

/* Heads over a sofa: the group sits on the band's top edge, half above it. */
.band-crew {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  margin-top: -56px;
  padding-bottom: var(--space-lg);
}

.band-crew .ch-slot {
  width: 80px;
}

.band-crew .ch-0 {
  width: 60px;
  margin-right: -15px;
}

.band-crew .ch-2 {
  margin-left: -15px;
}

.band-heading {
  margin: 0;
  font-family: var(--font-box);
  font-weight: 400;
  font-size: var(--type-title-size);
  line-height: var(--type-title-line);
}

.band-line {
  margin: var(--space-sm) 0 var(--space-lg);
  color: var(--color-ink-muted);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line);
}

@media (min-width: 600px) {
  .band-crew {
    margin-top: -76px;
  }

  .band-crew .ch-slot {
    width: 108px;
  }

  .band-crew .ch-0 {
    width: 81px;
    margin-right: -20px;
  }

  .band-crew .ch-2 {
    margin-left: -20px;
  }
}

/* --------------------------------------------------------- getting in
 *
 * The store row and the browser button, in the hero and in the band. With no
 * script, or on Android, Google Play leads and the browser is the quiet
 * button (design spec, *One store listed*). `site.js` marks every other
 * device: an iPhone cannot install from Google Play, so its row goes and the
 * browser is the loud action again; a desktop keeps the row but puts the
 * browser first (PRD criterion 3).
 */

.get-in {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-lg);
}

.store-offer {
  display: flex;
  flex-direction: column;
  align-items: inherit;
}

.store-label {
  margin: 0 0 var(--space-md);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-heading-size);
  line-height: var(--type-heading-line);
}

.store-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  justify-content: center;
}

.store img {
  display: block;
  height: 54px;
  width: auto;
}

.platform-ios .store-offer {
  display: none;
}

.platform-other .button-browser {
  order: -1;
}

.platform-ios .button-browser,
.platform-other .button-browser {
  background: var(--color-accent);
  color: var(--color-accent-ink);
  box-shadow: 0 5px 0 var(--color-accent-edge);
  font-family: var(--font-box);
  font-weight: 400;
}

.platform-ios .button-browser:active,
.platform-other .button-browser:active {
  box-shadow: 0 1px 0 var(--color-accent-edge);
}

/* One row on a desktop hero: the button and the badge, without the label and
   which the badge says for itself. The trio is centred against the
   words column, so any height the row added would be space above and below the
   trio. The band keeps the full row. */
@media (min-width: 1024px) {
  .hero .get-in {
    flex-direction: row;
    align-items: center;
  }

  .hero .store-label {
    display: none;
  }

  .hero .button-browser {
    white-space: nowrap;
  }
}

/* The row is wider than the wordmark, so a narrow desktop takes the room from
   the gap and the trio rather than push the trio off the left edge. */
@media (min-width: 1024px) and (max-width: 1199px) {
  .hero {
    gap: var(--space-lg);
  }

  .hero .get-in {
    gap: var(--space-md);
  }

  .hero .ch-slot {
    width: 200px;
  }

  .hero .ch-0 {
    width: 150px;
    margin-right: -38px;
  }

  .hero .ch-2 {
    margin-left: -38px;
  }
}
