/* GRAPHITE & SIGNAL (ADR 0044), with the finish ADR 0049 adds. Neutral ground,
   hairlines instead of outlines, no tilt, no dot grid, one typeface; the art is
   the colour. Three accents with three jobs, the same three the stage uses:
   --accent ACTS (purple, joto.games' own — the button that starts something), --mark MARKS (pink —
   the pick, the thing that changed), --point POINTS (yellow — where you are, a
   room code). Graphite only (owner, 2026-10-20: "keep the joto site in all
   black/dark and not white, these will be played on and they prefer dark
   mode"): one block of tokens, no light branch and no switch, so play.joto.games,
   party.joto.games and the television read as one JoTo.

   SHADOWS: still none, with one exception (0049) — a soft accent GLOW, and only
   on four things: the Party capsule, the join card, a primary button while
   it is hovered or focused, and a selected state (the rail's current page, the
   capsule's thumbnail on screen, the join page's next cell). The --glow-*
   tokens are the whole vocabulary; an ordinary card never takes one. */
:root {
  color-scheme: dark;
  --bg: #121212;
  --bg-2: #161616;
  --card: #1c1c1c;
  --card-2: #242424;
  --ink: #121212;
  --line: rgba(255, 255, 255, 0.12);
  --line-2: rgba(255, 255, 255, 0.28);
  --line-soft: rgba(255, 255, 255, 0.1);
  --text: #f4f4f2;
  --muted: #a8a8a4;
  --dim: #8a8a86;
  --accent: #8b5cf6;
  --accent-hi: #7c3aed;
  --accent-ink: #ffffff;
  /* The purple is joto.games' own. Anything that has to READ as it on graphite —
     a caret, a focus ring, an edge, a link's underline — takes the lighter one. */
  --accent-edge: #a78bfa;
  --mark: #ff3d8b;
  --point: #ffc233;
  /* Kept for the rules that still read them; both now say "where you are"
     in the one colour that job has. */
  --purple: #f4f4f2;
  --violet: #a8a8a4;
  --sky: #161616;
  --mint: #1c1c1c;
  --rr: #f6c98a;
  --pp: #ff6ba5;
  --ar: #7fd3e6;
  --live: #6ee7a8;
  --warn: #f6c98a;
  --down: #ff7a7a;
  --r: 14px;
  --r-lg: 16px;
  --r-card: 20px;
  --r-xl: 24px;
  --shadow: none;
  --shadow-sm: none;
  /* The glow (0049). */
  --feature-edge: rgba(139, 92, 246, 0.34);
  --glow-feature: 0 0 40px -12px rgba(139, 92, 246, 0.34);
  --glow-feature-hi: 0 0 52px -10px rgba(139, 92, 246, 0.48);
  --glow-btn: 0 0 0 3px rgba(139, 92, 246, 0.2), 0 0 26px -2px rgba(139, 92, 246, 0.5);
  --glow-point: 0 0 22px -6px rgba(255, 194, 51, 0.5);
}
*,
*::before,
*::after {
  box-sizing: border-box;
}
html {
  scroll-behavior: smooth;
  /* `clip`, not `hidden`: hidden would make the root a scroll container and break
     the sticky header. Guards against any future off-canvas element too — and it
     is what keeps the capsule's glow from ever adding a scrollbar. */
  overflow-x: clip;
}
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family:
    "Figtree",
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    sans-serif;
  font-weight: 400;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}
.main,
.site-footer {
  position: relative;
  z-index: 1;
}
img {
  max-width: 100%;
  display: block;
}
a {
  color: inherit;
  text-decoration: none;
}
/* One face (0044): the heads are the reading face, heavier and tighter. */
h1,
h2,
h3 {
  margin: 0;
  font-family: "Figtree", sans-serif;
  line-height: 1.15;
  letter-spacing: -0.02em;
  font-weight: 700;
}
p {
  margin: 0;
}
/* The light purple: a ring has to be seen on graphite. */
:focus-visible {
  outline: 2px solid var(--accent-edge);
  outline-offset: 3px;
  border-radius: 6px;
}

/* ---- the icon set -------------------------------------------------------- */
/* play.html's sprite holds the glyphs; this holds their weight, so a symbol is
   a path and nothing else. 2px on a 24px grid at every size, round ends. */
.sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}
.ico {
  width: 18px;
  height: 18px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---- buttons --------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font: inherit;
  font-size: 14.5px;
  font-weight: 600;
  font-family: "Figtree", sans-serif;
  border-radius: 999px;
  padding: 12px 22px;
  border: 1px solid var(--line);
  cursor: pointer;
  transition:
    background 0.2s ease,
    border-color 0.2s ease,
    box-shadow 0.2s ease,
    color 0.2s ease;
}
.btn-primary {
  background: var(--accent);
  border-color: transparent;
  color: var(--accent-ink);
  font-weight: 700;
}
/* A primary button glows while it is pointed at or focused (0049) — the one
   motion here, and it says "this is the thing that starts". */
.btn-primary:hover,
.btn-primary:focus-visible {
  background: var(--accent-hi);
  box-shadow: var(--glow-btn);
}
.btn-ghost {
  background: var(--card);
  color: var(--text);
}
.btn-ghost:hover {
  background: var(--card-2);
}
.btn-lg {
  min-height: 54px;
  padding: 14px 28px 14px 24px;
  gap: 10px;
  font-size: 16px;
}
.btn-lg .ico {
  width: 20px;
  height: 20px;
}
.filter-note {
  font-size: 12.5px;
  color: var(--muted);
}

/* ---- the arcade shelf's cards ---------------------------------------------- */
/* min() rather than a bare 300px: below ~340 the track was wider than the viewport
   and the row overflowed. The track can never exceed the space. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  gap: 20px;
}
/* An ordinary card: a hairline, no glow (0049 keeps the glow for featured
   surfaces). Hover lifts it 3px and darkens the hairline. */
.card {
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  overflow: hidden;
  transition:
    transform 0.18s ease,
    border-color 0.18s ease;
}
.card:hover {
  transform: translateY(-3px);
  border-color: var(--line-2);
}
/* Hover is not available to half the visitors. Touch gets the press instead. */
.card:active {
  transform: translateY(0);
}

/* The cards settle onto the page rather than appearing on it. One pass, 300ms,
   staggered to the sixth card and then not staggered at all, or a shelf of
   forty spends two seconds assembling itself. Nothing waits on a scroll
   observer.

   TRANSFORM ONLY, deliberately: a keyframe that starts at opacity 0 renders
   an invisible card anywhere animations do not run — a background tab, a
   thumbnailer, the headless browser `pnpm smoke` uses. Moving 9px is a
   flourish that costs nothing when it is skipped. */
@keyframes card-settle {
  from {
    transform: translateY(9px);
  }
}
.card {
  animation: card-settle 0.3s ease-out both;
}
.card:nth-child(2) {
  animation-delay: 0.04s;
}
.card:nth-child(3) {
  animation-delay: 0.08s;
}
.card:nth-child(4) {
  animation-delay: 0.12s;
}
.card:nth-child(5) {
  animation-delay: 0.16s;
}
.card:nth-child(6) {
  animation-delay: 0.2s;
}
.card:nth-child(n + 7) {
  animation-delay: 0.24s;
}
.card[data-state="offline"],
.card[data-state="degraded"] {
  opacity: 0.82;
}
.card-media {
  display: block;
}
.shot-wrap {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: linear-gradient(135deg, var(--art-a, #2a3038), var(--art-b, #12161b)), var(--card-2);
  border-bottom: 1px solid var(--line);
}
.shot-wrap::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0 2px, transparent 2px 11px),
    radial-gradient(120% 90% at 20% 0%, rgba(255, 255, 255, 0.16), transparent 60%);
  opacity: 0.75;
}
.art-mark {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 44px;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: rgba(255, 255, 255, 0.9);
  text-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
  z-index: 1;
}
.shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 2;
  transition: transform 0.35s ease;
}
.card:hover .shot {
  transform: scale(1.04);
}
/* Hover clip sits on the JPEG and never takes clicks; opacity 0 until the
   file has actually started, so a missing webm does not flash a black frame. */
.shot-preview {
  opacity: 0;
  pointer-events: none;
}
.shot-wrap.is-previewing .shot-preview {
  opacity: 1;
}
/* Top and bottom both: the pills sit on the top edge of whatever the art is. */
.shot-scrim {
  position: absolute;
  inset: 0;
  z-index: 3;
  background:
    linear-gradient(to bottom, rgba(6, 6, 9, 0.38), transparent 34%),
    linear-gradient(to top, rgba(6, 6, 9, 0.4), transparent 40%);
  pointer-events: none;
}
.media-top {
  position: absolute;
  z-index: 4;
  top: 12px;
  left: 12px;
  right: 12px;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}
/* The left-hand stack: the catalogue tag, the debug rung, the device guard and
   NEW / UPDATED. It wraps inside its own column, so the status pill keeps the
   top-right corner however many there are. */
.media-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  min-width: 0;
}
.media-top .live-pill {
  margin-left: auto;
  flex: none;
}

/* EVERYTHING ON THE ART IS ON THE ART, in both themes: a pill over a
   screenshot cannot take its colours from the page's ground, because the
   ground is not what is behind it. The glass is graphite at 80%, sized
   against the worst case — pure white art — where its label still clears
   8:1. */
.tag,
.pill {
  display: inline-flex;
  align-items: center;
  height: 24px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  border: 1px solid transparent;
}
.tag {
  padding: 0 10px;
}
/* The catalogue's colour, filled, with dark ink. Play is the player-facing
   word for the arcade-catalogue shelf (solo / local / island). Party / raid
   rows keep their own tags if they ever render as cards. */
.tag-play {
  color: #141413;
  background: #ffc233;
}
.tag-arcade {
  color: #062028;
  background: #5ccfe6;
}
.tag-roomraid {
  color: #1f1403;
  background: #f6c98a;
}
.tag-partypile {
  color: #1f0612;
  background: #ff6ba5;
}
/* The debug shelf's rung (www/catalog.js §1b): HIDDEN or DEV, outlined in pink
   because it marks what a visitor would not get. Only ever drawn under ?debug. */
.tag-debug {
  color: #f4f4f2;
  background: rgba(18, 18, 18, 0.8);
  border: 1.5px solid var(--mark);
}
/* Shown only on a touch-only device, where it answers "can I even tap this".
   `html[data-touch-only]` below, set in catalog.js. */
.tag-desk {
  display: none;
  color: #f4f4f2;
  background: rgba(18, 18, 18, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
}
.pill {
  gap: 6px;
  padding: 0 10px 0 8px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: none;
  color: #f4f4f2;
  background: rgba(18, 18, 18, 0.8);
  border-color: rgba(255, 255, 255, 0.14);
}
.dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #a8a8a4;
  flex: none;
}
.pill[data-state="checking"] .dot {
  animation: blink 1.1s ease-in-out infinite;
}
.pill[data-state="live"] .dot {
  background: #6ee7a8;
  box-shadow: 0 0 0 3px rgba(110, 231, 168, 0.22);
}
/* Playable and labelled: amber, which is none of the three accents' jobs. */
.pill[data-state="beta"] .dot {
  background: #f6c98a;
  box-shadow: 0 0 0 3px rgba(246, 201, 138, 0.2);
}
.pill[data-state="waking"] .dot {
  background: #f6c98a;
  animation: blink 1.4s ease-in-out infinite;
}
.pill[data-state="degraded"] .dot {
  background: #ff7a7a;
}
/* NEW and UPDATED: what releases.json says changed in the last 14 days — a
   first release, or a later one that touched the game's own files (catalog.js,
   section 4b). Both pink, because pink MARKS the thing that changed (ADR 0044);
   purple is kept for what starts a game and yellow for where you are.

   NEW is filled and UPDATED is not, because a new game is the bigger news and
   the shelf should say so at a glance. Dark ink on the filled pink is 5.6:1;
   the outline keeps the glass ground, so its label is the glass's 8:1 and its
   pink hairline clears 3:1 against the glass even over white art. */
.pill.release-pill {
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 10.5px;
  font-weight: 700;
}
.pill.release-pill[data-release="new"] {
  color: #121212;
  background: var(--mark);
  border-color: transparent;
  padding: 0 10px;
}
.pill.release-pill[data-release="updated"] {
  border-color: var(--mark);
  padding: 0 10px 0 8px;
}
.pill.release-pill[data-release="updated"] .dot {
  background: var(--mark);
}
@keyframes blink {
  50% {
    opacity: 0.35;
  }
}
@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
  }
  html {
    scroll-behavior: auto;
  }
}
.card-body {
  padding: 18px 20px 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1;
}
.card h3 {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.015em;
}
.tagline {
  font-size: 14px;
  font-weight: 500;
  color: var(--muted);
  margin-top: -4px;
}
/* Two lines, clipped: the whole blurb stays in the document for a screen
   reader and for search; the card only shows its start. */
.desc {
  font-size: 14px;
  color: var(--muted);
  line-height: 1.55;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
/* WHAT YOU HOLD, AND WHERE EVERYBODY IS.
   The row is on EVERY card, with the glyphs a game does not use greyed out.
   That is load-bearing: carried only by some cards it would read as a warning;
   carried by all of them it reads as a spec line. Built by inputRowHTML() from
   the catalogue row's `plays` tags, so a game that gains phone support gains
   the glyph by adding one tag to its row and nothing here changes. */
.holds {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 2px 0;
}
.hold {
  display: inline-flex;
  color: var(--text);
}
.hold .ico {
  width: 18px;
  height: 18px;
}
.hold.off {
  opacity: 0.28;
}
.where {
  display: inline-flex;
  align-items: center;
  height: 22px;
  margin-left: 2px;
  padding: 0 9px;
  border-radius: 999px;
  border: 1px solid var(--line-2);
  color: var(--text);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.who {
  margin-left: auto;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px 16px 20px;
}
.note {
  font-size: 12px;
  color: var(--muted);
  line-height: 1.4;
}
/* The round arrow. Its words — Play now, Coming soon, Temporarily down,
   Desktop game — are its accessible name and its tooltip (paint() in
   catalog.js); the status pill on the art and the note beside it say the same
   thing in sight. It fills purple when its card is pointed at, because purple is
   what starts a game, and glows only when it is itself hovered or focused. */
.btn-play {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 40px;
  height: 40px;
  margin-left: auto;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--card-2);
  color: var(--text);
  transition:
    background 0.2s ease,
    color 0.2s ease,
    border-color 0.2s ease,
    box-shadow 0.2s ease;
}
.btn-play .ico {
  width: 18px;
  height: 18px;
}
.card:hover .btn-play,
.btn-play:hover,
.btn-play:focus-visible {
  background: var(--accent);
  border-color: transparent;
  color: var(--accent-ink);
}
.btn-play:hover,
.btn-play:focus-visible {
  box-shadow: var(--glow-btn);
}
.btn-play:focus-visible {
  border-radius: 50%;
}
.btn-play[aria-disabled="true"],
.card:hover .btn-play[aria-disabled="true"] {
  background: transparent;
  border-color: var(--line);
  color: var(--dim);
  box-shadow: none;
  pointer-events: none;
}
@media (pointer: coarse) {
  .btn-play {
    width: 44px;
    height: 44px;
  }
}
.btn-sr,
.side-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* THE DEVICE GUARD. A touch-only visitor cannot play a keyboard game, and the
   honest place to say so is before the tap rather than after the load. The
   card stays on the shelf and the URL still works: this is a label, not a
   lock (ADR 0033). `data-phone` comes from the same glyph data, so a game
   that gains phone support leaves this state by gaining a tag. */
html[data-touch-only] .card[data-phone="0"] .tag-desk {
  display: inline-flex;
}
html[data-touch-only] .card[data-phone="0"] .card-media {
  pointer-events: none;
}
html[data-touch-only] .card[data-phone="0"] .shot-wrap {
  filter: saturate(0.55);
}
.empty {
  text-align: center;
  color: var(--muted);
  padding: 60px 0;
  font-size: 15px;
}
/* THE BAND. The one full-bleed element in the content column, and the only
   place the rail's ground reappears there, which is what makes it read as a
   change of room rather than another heading. Rendered by bandHTML() right
   after the arcade shelf and hidden with that shelf by syncSections(). */
.zone-band {
  display: flex;
  align-items: baseline;
  gap: 6px 12px;
  flex-wrap: wrap;
  margin: 4px -32px -36px;
  padding: 18px 32px;
  background: var(--sky);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.zone-band strong {
  font-family: "Figtree", sans-serif;
  font-size: 17px;
  font-weight: 700;
  color: var(--text);
}
.zone-band span {
  font-size: 14px;
  color: var(--muted);
}
.zone-band a {
  color: var(--text);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--accent-edge);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

/* ---- the debug banner (catalog.js §1b) -------------------------------------- */
/* In the content column's flow, NOT fixed or sticky: this page already has a
   sticky sidebar and a sticky topbar pinned at top:0, and a third competitor
   there pushed the whole shelf out of place. Coral with dark ink, the same in
   both themes (8:1), because it is not part of the page: it says what you are
   looking at is not what a visitor gets. The × hides it for this page view
   only; the per-card rung badges stay. */
.debug-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 10px 9px 16px;
  border-radius: 14px;
  background: #ff8a7a;
  color: #2a0a06;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.4;
}
.debug-bar > .ico {
  width: 20px;
  height: 20px;
}
.debug-text {
  flex: 1;
  min-width: 0;
}
.debug-text strong {
  font-weight: 700;
}
.debug-off,
.debug-close {
  flex: none;
  font: inherit;
  color: inherit;
  background: transparent;
  cursor: pointer;
}
.debug-off {
  height: 34px;
  padding: 0 16px;
  border: 1.5px solid #2a0a06;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 700;
}
.debug-close {
  display: inline-grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 50%;
}
.debug-off:hover,
.debug-close:hover {
  background: rgba(42, 10, 6, 0.12);
}
.debug-off:focus-visible,
.debug-close:focus-visible {
  outline: 2px solid #2a0a06;
  outline-offset: 2px;
}

/* ---- footer ------------------------------------------------------------------ */
/* Sits in `.main` rather than `.content` so its border and ground run the
   full width of the column and meet the sidebar's edge. */
.site-footer {
  border-top: 1px solid var(--line);
  padding: 44px 32px 28px;
  background: var(--bg-2);
}
.footer-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 32px;
}
.footer-grid h3 {
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 14px;
  font-weight: 700;
}
.footer-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.footer-links a {
  font-size: 14px;
  color: var(--muted);
  transition: color 0.2s ease;
}
.footer-links a:hover {
  color: var(--text);
}
noscript .empty {
  color: var(--muted);
}

/* =========================================================================
   THE SHELL  (R-69)
   A portal, not a landing page: a persistent sidebar for where you are, a top
   bar for search and the one action this site has that Poki and CrazyGames do
   not — joining a room. The UI carries almost no colour on purpose; the key
   art is the only saturated thing on screen.
   ====================================================================== */
.app {
  display: flex;
  min-height: 100vh;
}

/* ---- sidebar ------------------------------------------------------------ */
.sidebar {
  width: 236px;
  flex-shrink: 0;
  background: var(--sky);
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 20px 0 24px;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  z-index: 95;
}
.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 0 24px;
  min-height: 44px;
}
.brand-img {
  position: relative;
  height: 26px;
  width: auto;
  display: block;
}
/* The white wordmark (media/joto-wordmark-white.svg, traced from joto-wide.png),
   drawn as it is: no filter, because the ground is always graphite. */
.brand-img img {
  display: block;
  height: 100%;
  width: auto;
}
.brand-img::after {
  content: "";
  position: absolute;
  inset: 0;
  background: url("media/joto-wink-white.svg") center / 100% 100% no-repeat;
  pointer-events: none;
  visibility: hidden;
}
/* Keep the shared top stroke; only the second o below it closes. */
@media (hover: hover) {
  :is(.brand, .join-brand):hover .brand-img img {
    clip-path: polygon(0 0, 100% 0, 100% 19%, 72% 19%, 72% 100%, 0 100%);
  }
  :is(.brand, .join-brand):hover .brand-img::after {
    visibility: visible;
  }
}
.brand-sub {
  font-family: "Figtree", sans-serif;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--accent);
}
.side-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0 12px;
}
/* One over each rail group that needs naming: "Play together" over Party,
   "Play", "Show" over the shelf's filters, and the three lists below them,
   which carry a glyph as well. Small tracked caps in --muted: 7:1 on the rail. */
.side-label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.16em;
  line-height: 1.3;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0 14px 8px;
}
.side-label .ico {
  width: 14px;
  height: 14px;
}
.side-link,
.side-cat {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: 999px;
  font-family: inherit;
  font-size: 14.5px;
  font-weight: 600;
  color: var(--muted);
  transition:
    background 0.2s ease,
    color 0.2s ease,
    box-shadow 0.2s ease;
}
.side-link .ico,
.side-cat .ico {
  width: 20px;
  height: 20px;
}
/* `--card` is a step up from the rail. */
.side-link:hover,
.side-cat:hover {
  background: var(--card);
  color: var(--text);
}
/* Yellow is "where you are" (0044), the one job it has in this UI: a filled
   pill, dark text, and the selected state's glow (0049). */
.side-link.current {
  background: var(--point);
  color: #121212;
  box-shadow: var(--glow-point);
}
.side-cat {
  width: 100%;
  background: none;
  text-align: left;
  cursor: pointer;
}
/* A pressed filter is a choice, not a place: a card ground and a hairline,
   never the yellow and never a glow. The border is there unpressed too, in
   transparent, so pressing one moves nothing. */
.side-cat[aria-pressed="true"] {
  background: var(--card);
  border-color: var(--line);
  color: var(--text);
}
.side-cat .cnt {
  margin-left: auto;
  display: inline-grid;
  place-items: center;
  min-width: 26px;
  height: 22px;
  padding: 0 7px;
  border-radius: 999px;
  background: var(--card-2);
  color: var(--text);
  font-size: 11.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* The global ring is the deep purple; a rail link is not an action, so it takes
   the text colour instead. */
.side-link:focus-visible,
.side-cat:focus-visible,
.side-game:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 2px;
  border-radius: 999px;
}
.side-game:focus-visible {
  border-radius: 12px;
}
/* A hairline (0044), in the line token. */
.side-rule {
  height: 1px;
  background: var(--line);
  margin: 0 24px;
}
/* The studio's two pages, About and Press: last in the rail's markup and shown
   only in the phone drawer, small and under the games (owner, 2026-10-20). The
   desktop rail is games only; there they are in the footer. */
.side-studio {
  display: none;
  border-top: 1px solid var(--line);
  margin: 4px 24px 0;
  padding: 14px 0 0;
}
.side-studio .side-link {
  min-height: 40px;
  padding: 0 2px;
  font-size: 13.5px;
  font-weight: 500;
}
.side-studio .side-link:hover {
  background: none;
  color: var(--text);
}

/* ---- the rail's three lists: New & updated, Recently played, Most played --
   catalog.js fills them (section 4b) and hides each one whole — rule, label
   and rows — when it has nothing to name, so an empty heading never stands in
   the rail. A row is a link like the ones above it: 44px tall, a hairline
   thumb, the title, and under it the TV chip and at most one short fact. */
.side-section {
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.side-section[hidden] {
  display: none;
}
.side-games {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.side-game {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 4px 10px;
  border-radius: 12px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--muted);
  transition:
    background 0.2s ease,
    color 0.2s ease;
}
.side-game:hover {
  background: var(--card);
  color: var(--text);
}
/* The card's own tile, small: the two art colours and the monogram, with the
   screenshot over them when there is one. */
.side-thumb {
  position: relative;
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 9px;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, var(--art-a), var(--art-b));
  border: 1px solid var(--line);
  color: #fff;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.02em;
}
.side-thumb img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Title on the first line, the kind and the one fact on the second. */
.side-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.25;
}
.side-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.side-sub {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* A shared-screen game opens a room on a television, not a game on this
   device; the row says so before the tap. */
.side-tv {
  flex: none;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.08em;
  line-height: 13px;
  padding: 0 4px;
  border: 1px solid var(--line-2);
  border-radius: 4px;
  color: var(--muted);
}

/* ---- main column -------------------------------------------------------- */
.main {
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.topbar {
  height: 76px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 32px;
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  background: var(--bg);
  z-index: 40;
}
.search {
  flex: 1 1 auto;
  max-width: 600px;
  display: flex;
  align-items: center;
  gap: 12px;
  height: 48px;
  padding: 0 20px;
  border-radius: 999px;
  background: var(--card);
  border: 1px solid var(--line);
  color: var(--muted);
  cursor: text;
  transition: border-color 0.2s ease;
}
.search:hover {
  border-color: var(--line-2);
}
.search:focus-within {
  border-color: var(--line-2);
  outline: 2px solid var(--text);
  outline-offset: 2px;
}
.search .ico {
  width: 19px;
  height: 19px;
}
.search input {
  flex-grow: 1;
  min-width: 0;
  background: none;
  border: 0;
  outline: none;
  color: var(--text);
  font: inherit;
  font-size: 15px;
}
.search input::placeholder {
  color: var(--muted);
  opacity: 1;
}
.topbar-spacer {
  flex-grow: 1;
}
.menu-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--text);
  cursor: pointer;
  flex-shrink: 0;
  transition: border-color 0.2s ease;
}
.menu-btn:hover {
  border-color: var(--line-2);
}
.menu-btn .ico {
  width: 20px;
  height: 20px;
}
.btn-join {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 48px;
  padding: 0 24px 0 20px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-family: "Figtree", sans-serif;
  font-size: 15px;
  font-weight: 700;
  flex-shrink: 0;
  transition:
    box-shadow 0.2s ease,
    background 0.2s ease;
}
.btn-join .ico {
  width: 19px;
  height: 19px;
}
.btn-join:hover,
.btn-join:focus-visible {
  background: var(--accent-hi);
  box-shadow: var(--glow-btn);
}
.btn-join:focus-visible {
  outline: 2px solid var(--text);
  border-radius: 999px;
}

/* ---- the account: Sign in, or the player's name (Accounts v2 D3) ------------- */
/* Beside Join a room, and quieter than it: a hairline pill on the card ground, the
   shape the theme toggle already wears. Signed in, the same place holds the player's
   own name — cut short, never allowed to push the search away — with Your account
   and Sign out under it. While a signed-in browser waits for its pass, Sign in keeps
   its space but is not shown, so nothing jumps and nothing flashes the wrong word. */
.account {
  position: relative;
  display: flex;
  flex-shrink: 0;
}
.account [hidden] {
  display: none !important;
}
.account[data-state="asking"] .btn-signin {
  visibility: hidden;
}
.btn-signin,
.btn-account {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 48px;
  padding: 0 22px 0 18px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  color: var(--text);
  font-family: "Figtree", sans-serif;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  transition: border-color 0.2s ease;
}
.btn-signin .ico {
  width: 19px;
  height: 19px;
}
.btn-account {
  padding: 0 18px 0 6px;
  max-width: 240px;
}
.btn-signin:hover,
.btn-account:hover,
.btn-account[aria-expanded="true"] {
  border-color: var(--line-2);
}
.btn-signin:focus-visible,
.btn-account:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 2px;
}
/* The first letter of the chosen name, in the site's yellow: the one mark that says
   "this is you" on a phone, where the name itself has no room. */
.account-initial {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--point);
  color: #121212;
  font-size: 16px;
  font-weight: 800;
}
.account-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 200px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--card);
}
/* The name, over the menu, only where the chip has no room for it (a phone). */
.account-who {
  display: none;
  padding: 10px 14px 8px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 4px;
  color: var(--muted);
  font-size: 13.5px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-menu [role="menuitem"] {
  display: block;
  padding: 11px 14px;
  border-radius: 10px;
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  white-space: nowrap;
}
.account-menu [role="menuitem"]:hover,
.account-menu [role="menuitem"]:focus-visible {
  background: var(--sky);
  outline: none;
}

/* Your early access: the shelf's own cards under a yellow rule, the one place a
   restricted game is listed, and only for the person it was opened to. */
#early {
  display: flex;
  flex-direction: column;
}
.section-head .rule-dot.ea {
  background: var(--point);
}
.content {
  padding: 28px 32px 36px;
  display: flex;
  flex-direction: column;
  gap: 36px;
}

/* ---- the hero row: the capsule and the join card ---------------------------- */
.hero-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 20px;
  align-items: stretch;
}
/* THE JOIN CARD: the one thing this site does that a solo-game portal does not,
   so it sits above the fold beside the capsule and shares its purple edge glow
   (0049), fainter. */
.room-panel {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: var(--r-xl);
  background: var(--mint);
  border: 1px solid var(--feature-edge);
  box-shadow: var(--glow-feature);
  padding: 28px 24px 24px;
  display: flex;
  flex-direction: column;
  /* The row stretches this card to the capsule's height; centred, the spare
     height reads as room around the code rather than a hole under it. */
  justify-content: center;
  gap: 16px;
}
.room-kicker {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
}
.room-cells {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}
.room-cells span {
  position: relative;
  aspect-ratio: 1;
  border-radius: 14px;
  background: var(--card);
  border: 1px solid var(--line-2);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: "Figtree", sans-serif;
  font-size: 34px;
  font-weight: 700;
  line-height: 1;
  color: var(--text);
  /* A step below the card: the cells read as wells. */
  background: var(--bg-2);
}
.room-cells span.next {
  border: 2px solid var(--accent-edge);
}
.room-cells span.next::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 2px;
  height: 30px;
  background: var(--accent-edge);
  animation: caret 1.1s steps(1) infinite;
}
@keyframes caret {
  50% {
    opacity: 0;
  }
}
.room-panel .btn-lg {
  width: 100%;
}
.room-note {
  font-size: 13.5px;
  color: var(--muted);
  text-align: center;
}
/* Watermark, not information: aria-hidden in the markup, under 1.2:1 on
   purpose. */
.room-glyph.ico {
  position: absolute;
  z-index: -1;
  right: -18px;
  bottom: -26px;
  width: 132px;
  height: 132px;
  stroke-width: 1.5;
  color: var(--text);
  opacity: 0.06;
  pointer-events: none;
}

/* ---- section heads ------------------------------------------------------------ */
/* The shelves are siblings in a plain div, so the space between them has to
   come from here. */
#shelves {
  display: flex;
  flex-direction: column;
  gap: 36px;
}
.section {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.section-head {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  color: inherit;
}
.section-head h2 {
  font-size: 22px;
  font-weight: 700;
}
.section-head .rule-dot {
  height: 22px;
  width: 4px;
  border-radius: 999px;
  background: var(--accent-edge);
  flex-shrink: 0;
}
.section-head .ico {
  width: 22px;
  height: 22px;
  color: var(--text);
}
.section-head .chev {
  width: 18px;
  height: 18px;
  stroke-width: 2.5;
  color: var(--muted);
  transition:
    transform 0.2s ease,
    color 0.2s ease;
}
a.section-head:hover .chev {
  transform: translateX(3px);
  color: var(--text);
}
.section-note {
  margin-left: auto;
  font-size: 13.5px;
  color: var(--muted);
}

/* The drawer toggle. Hidden above the breakpoint, where the sidebar is always
   there; aria-expanded is the single source of truth and drives the icon. */
.menu-btn {
  display: none;
  z-index: 100;
}
.menu-btn .icon-close {
  display: none;
}
.menu-btn[aria-expanded="true"] .icon-open {
  display: none;
}
.menu-btn[aria-expanded="true"] .icon-close {
  display: block;
}
.nav-scrim {
  display: none;
}

/* =========================================================================
   NARROW
   The sidebar becomes a drawer. It is NOT parked off-canvas with a transform:
   a transformed fixed element still counts toward the document's scroll width,
   which was ~300px of horizontal scroll on a 375px phone. `display: none`
   cannot do that, and it takes the closed menu out of the tab order for free.
   ====================================================================== */
@media (max-width: 1080px) {
  .hero-row {
    grid-template-columns: minmax(0, 1fr);
  }
  /* Side by side under the capsule would be a 60px-wide code cell; the card
     keeps its own measure instead. */
  .room-panel {
    padding: 24px;
  }
  .room-cells {
    max-width: 360px;
    width: 100%;
    margin: 0 auto;
  }
  .room-panel .btn-lg {
    max-width: 360px;
    margin: 0 auto;
  }
}
@media (max-width: 720px) {
  .menu-btn {
    display: inline-flex;
  }
  .sidebar {
    display: none;
    position: fixed;
    inset: 0 auto 0 0;
    width: min(82vw, 300px);
    height: 100vh;
    /* The visible height on a phone, so the last list is not under the
       browser's own toolbar. */
    height: 100dvh;
    /* A hairline and no shadow (0044): the scrim already separates it. */
    border-right: 1px solid var(--line);
    padding-top: 84px;
  }
  .sidebar.open {
    display: flex;
    animation: side-in 0.22s ease both;
  }
  .side-studio {
    display: flex;
    flex-direction: row;
    gap: 18px;
  }
  @keyframes side-in {
    from {
      transform: translateX(-100%);
    }
    to {
      transform: none;
    }
  }
  .nav-scrim {
    display: none;
    position: fixed;
    inset: 0;
    /* Graphite, flat: no blur behind it. */
    background: rgba(18, 18, 18, 0.55);
    z-index: 90;
  }
  .nav-scrim.open {
    display: block;
    animation: scrim-in 0.22s ease both;
  }
  @keyframes scrim-in {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }
  .topbar {
    height: 68px;
    padding: 0 14px;
    gap: 8px;
  }
  .search {
    max-width: none;
    height: 44px;
    padding: 0 16px;
  }
  .menu-btn,
  .btn-join {
    width: 44px;
    height: 44px;
  }
  .btn-join {
    justify-content: center;
    padding: 0;
  }
  .btn-join span {
    display: none;
  }
  /* A phone keeps the glyph and the initial; the words are the controls' names. */
  .btn-signin,
  .btn-account {
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
  }
  .btn-signin span,
  .account-name {
    display: none;
  }
  .account-who {
    display: block;
    max-width: 240px;
  }
  .account-initial {
    width: 32px;
    height: 32px;
  }
  .content {
    padding: 18px 16px 28px;
    gap: 28px;
  }
  /* The band's negative margin has to track .content's padding, or it stops
     touching the column edges and reads as one more card. */
  .zone-band {
    margin: 0 -16px -28px;
    padding: 16px;
  }
  .site-footer {
    padding: 36px 16px 24px;
  }
  .room-cells span {
    font-size: 30px;
  }
  .section-note {
    flex-basis: 100%;
    margin-left: 14px;
  }
  .section-head {
    flex-wrap: wrap;
    row-gap: 0;
  }
  .debug-bar {
    flex-wrap: wrap;
    padding: 10px 10px 10px 14px;
  }
  .debug-text {
    flex-basis: calc(100% - 80px);
  }
  .footer-links a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}
@media (max-width: 420px) {
  .search {
    display: none;
  }
}

/* ---- the Party capsule (ADR 0044, glow 0049) ----------------------------- */
/* The hero of the page. Party is one product on this shelf — every
   shared-screen game lives inside it — so the capsule is the stage itself,
   graphite on both grounds, with the games it holds crossfading behind the
   wordmark (renderCapsule in catalog.js). The art pans, it never tilts; the
   card's only light is its purple edge glow. The height is set here and not by
   the art, and the thumbnail row reserves its own, so nothing moves when the
   first image lands; without JS the wordmark and the button still stand on
   bare graphite. */
.capsule {
  position: relative;
  isolation: isolate;
  container-type: inline-size;
  border-radius: var(--r-xl);
  border: 1px solid rgba(139, 92, 246, 0.36);
  background: #161616;
  color: #f4f4f2;
  padding: 40px 44px 36px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 28px;
  overflow: hidden;
  min-height: 460px;
  box-shadow: var(--glow-feature);
  transition:
    box-shadow 0.25s ease,
    border-color 0.25s ease;
}
.capsule:hover,
.capsule:focus-visible {
  border-color: rgba(139, 92, 246, 0.55);
  box-shadow: var(--glow-feature-hi);
}
/* The ring is the page's own ink rather than purple: a purple ring would
   melt into the capsule's purple edge, and this ring has to read on both grounds. */
.capsule:focus-visible {
  outline: 3px solid var(--text);
  outline-offset: 4px;
  border-radius: var(--r-xl);
}
.capsule-art {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.capsule-slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.08);
  transition: opacity 1.2s ease;
}
.capsule-slide.on {
  opacity: 1;
}
/* The game being played over its still (loopCapsule): invisible until the clip
   is actually playing, so a game without one — or a slow one — keeps its still
   and never flashes a black frame. Scaled like the slides so the edges line up;
   it does not pan, because the footage already moves. */
.capsule-clip {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.08);
  transition: opacity 1.2s ease;
  pointer-events: none;
}
.capsule-clip.on {
  opacity: 1;
}
/* The pan keeps running while a slide fades out (`leaving`), or the image
   would snap back to centre while still half visible. 7.2 s is one hold plus
   one fade — CAPSULE_HOLD_MS + CAPSULE_FADE_MS in catalog.js. */
.capsule-slide.on,
.capsule-slide.leaving {
  animation: capsule-pan 7.2s linear both;
}
.capsule-slide:nth-child(even).on,
.capsule-slide:nth-child(even).leaving {
  animation-name: capsule-pan-back;
}
.capsule.is-paused .capsule-slide {
  animation-play-state: paused;
}
@keyframes capsule-pan {
  from {
    transform: scale(1.08) translate3d(-2.5%, 0, 0);
  }
  to {
    transform: scale(1.08) translate3d(2.5%, 0, 0);
  }
}
@keyframes capsule-pan-back {
  from {
    transform: scale(1.08) translate3d(2.5%, 0, 0);
  }
  to {
    transform: scale(1.08) translate3d(-2.5%, 0, 0);
  }
}
/* The scrim, sized against the worst case — a pure-white pixel of art — not
   against today's screenshots. The text column is a fixed 560px, not a share
   of the card, because the copy does not get narrower when the card does:
   the kicker and the note keep 4.5:1 inside it, and what is right of it is
   left to the art. */
.capsule-art::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    /* The wordmark and kicker sit top-left, where a game's own headline text
       often is; this keeps that text a texture rather than a second title. */
    linear-gradient(
      180deg,
      rgba(18, 18, 18, 0.62) 0%,
      rgba(18, 18, 18, 0.3) 38%,
      rgba(18, 18, 18, 0) 60%
    ),
    linear-gradient(
      0deg,
      rgba(18, 18, 18, 0.85) 0%,
      rgba(18, 18, 18, 0.4) 30%,
      rgba(18, 18, 18, 0) 55%
    ),
    linear-gradient(
      90deg,
      rgba(18, 18, 18, 0.9) 0,
      rgba(18, 18, 18, 0.82) 560px,
      rgba(18, 18, 18, 0.34) 80%,
      rgba(18, 18, 18, 0.12) 100%
    );
}
.capsule-head,
.capsule-foot {
  position: relative;
  z-index: 1;
}
.capsule h1 {
  font-size: clamp(56px, 6.4vw, 104px);
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 0.92;
  color: #f4f4f2;
}
.capsule-raid {
  color: var(--point);
}
.capsule-kicker {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: #e2e2de;
  margin-top: 18px;
}
/* What Party holds, as a row of thumbnails, and which one is on screen:
   outlined in yellow, because yellow points, with the selected state's glow
   (0049). Hidden from assistive tech in the markup: it changes every six
   seconds, and the link's name should not. 44px reserved before JS fills it. */
.capsule-now {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 44px;
  margin-top: 24px;
}
.capsule-thumbs {
  display: flex;
  gap: 8px;
  flex: none;
}
.capsule-thumb,
.capsule-more {
  position: relative;
  display: grid;
  place-items: center;
  height: 44px;
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #f4f4f2;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.04em;
  transition:
    border-color 0.3s ease,
    box-shadow 0.3s ease;
}
.capsule-thumb {
  width: 70px;
  background: linear-gradient(135deg, var(--art-a, #2a3038), var(--art-b, #12161b));
}
.capsule-thumb img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.capsule-more {
  min-width: 44px;
  padding: 0 10px;
  font-size: 13px;
  background: rgba(18, 18, 18, 0.6);
}
.capsule-thumb.on,
.capsule-more.on {
  border: 2px solid var(--point);
  box-shadow: 0 0 16px -4px rgba(255, 194, 51, 0.6);
}
.capsule-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 14px;
  font-weight: 600;
  color: #f4f4f2;
}
.capsule-foot {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
}
.capsule .btn-primary {
  min-height: 60px;
  padding: 16px 36px 16px 30px;
  font-size: 18px;
  gap: 12px;
}
.capsule .btn-primary .ico {
  width: 22px;
  height: 22px;
}
/* The whole capsule is the link, so its hover and focus are the button's. */
.capsule:hover .btn-primary,
.capsule:focus-visible .btn-primary {
  background: var(--accent-hi);
  box-shadow: var(--glow-btn);
}
/* One line reserved, and the count replacing the fallback sentence is the same
   length give or take "5 games · ", so it moves nothing. */
.capsule-note {
  font-size: 15px;
  color: #e2e2de;
  line-height: 1.45;
  max-width: 64ch;
  min-height: 1.45em;
}
@media (max-width: 1080px) {
  .capsule {
    min-height: 420px;
  }
}
/* A narrow capsule — a phone, a tablet, or a laptop where the rail and the
   join card leave it under ~760px — carries its copy across the whole width,
   so the scrim goes even instead of a left-hand column. Asked of the capsule,
   not the viewport: with the rail beside it, the viewport's width says little
   about the card's. */
@container (max-width: 680px) {
  .capsule-art::after {
    background: linear-gradient(
      0deg,
      rgba(18, 18, 18, 0.9) 0%,
      rgba(18, 18, 18, 0.82) 50%,
      rgba(18, 18, 18, 0.84) 100%
    );
  }
  .capsule-note {
    min-height: 2.9em;
  }
  /* Four thumbnails and "+N" fill a narrow card, so the title takes its own
     line under them; both lines are reserved before JS fills either. */
  .capsule-now {
    flex-wrap: wrap;
    row-gap: 8px;
    min-height: 74px;
    align-content: flex-start;
  }
  .capsule-title {
    flex-basis: 100%;
  }
}
/* A phone: less padding, smaller thumbnails, and the button takes the full
   width. */
@media (max-width: 720px) {
  .capsule {
    min-height: 440px;
    padding: 28px 22px 24px;
    gap: 22px;
  }
  .capsule-kicker {
    font-size: 12px;
    letter-spacing: 0.18em;
  }
  .capsule-now {
    flex-wrap: wrap;
    row-gap: 8px;
    min-height: 66px;
    align-content: flex-start;
  }
  .capsule-thumbs {
    gap: 6px;
  }
  .capsule-thumb {
    width: 56px;
    height: 36px;
  }
  .capsule-more {
    height: 36px;
    min-width: 36px;
  }
  .capsule-title {
    flex-basis: 100%;
  }
  .capsule-foot {
    align-items: stretch;
  }
}
