/* The site's one stylesheet: edit it here (Đợt 2, 2026-09-28). It began as
   core/src/styles/tokens.css + base.css + code/css/php.css, concatenated; those
   are in _old/astra/ now. */

/* Design tokens. These eight colours come verbatim from the supplied design
   (.superpowers/sdd/design-index.html) and must not be retuned without it:
   they are the contract with that design, not a matter of taste. */
:root {
  --acc: #c8f751;
  --acc-ink: #0e0e10;
  --ink: #f4f3f5;
  --ground: #0e0e10;
  --panel: #18181c;
  --panel-2: #23232a;
  --line: #2b2b32;
  --dim: #9d9ca7;

  --radius: 12px;
  --radius-lg: 18px;

  /* A 4px scale. Spacing anywhere in the site is one of these. */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;

  --wrap: 1280px;
  --font: "Archivo", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* The whole site's stylesheet. One file, linked once, cached across every page
   (astro.config.mjs sets inlineStylesheets: 'never' for exactly this). */

/* Self-hosted, variable-weight Archivo (latin subset, weights 100-900 in a
   single 34,928-byte woff2), replacing the design's Google Fonts link. See
   code/assets/f/archivo-latin.woff2 and the brief for Task 1. */
@font-face {
  font-family: 'Archivo';
  src: url('/assets/f/archivo-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

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

/* THE `hidden` ATTRIBUTE HAS TO WIN, AND WITHOUT THIS IT DOES NOT.
   ------------------------------------------------------------------------
   `hidden` only hides anything because the browser's own stylesheet carries
   `[hidden] { display: none }`. Any author rule that sets `display` beats it
   -- author styles outrank the UA sheet whatever the specificity -- so a
   `.thing { display: flex }` silently un-hides every `<div class="thing"
   hidden>` on the site.

   Found in production, reported as "the account row shows Sign in AND Sign
   out at once": both halves of that row carry `hidden` and both were visible,
   because `.drawer-acct` sets `display: flex`. Ten classes were in the same
   position, including the mobile MENU button and the favourite heart -- both
   of which ship hidden precisely so a visitor with no JavaScript never sees a
   control that cannot work. The enhancement was shipping broken.

   One rule instead of ten fixes, because the eleventh is written next week by
   somebody who has never read this. `!important` is correct here and nowhere
   else: `hidden` means hidden, and no rule should be able to argue with it. */
[hidden] { display: none !important; }

html { color-scheme: dark; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.55;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--dim); text-decoration: none; }
a:hover { color: var(--acc); text-decoration: underline; }
/* Links in prose are underlined by default; links that are cards or nav are
   not. Relying on colour alone fails anyone who cannot see the difference. */
main p a, main li a { text-decoration: underline; text-underline-offset: 2px; }

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

/* Off-screen until focused, then the first thing on the page. Positioned
   rather than `display: none`, because a hidden element cannot be focused and
   a skip link that cannot be focused is decoration. */
.skip-link {
  position: fixed; top: -80px; inset-inline-start: 12px; z-index: 100;
  background: var(--acc); color: var(--acc-ink);
  padding: 12px 22px; border-radius: 0 0 14px 14px;
  font-weight: 800; font-size: 13px; letter-spacing: 0.02em;
  text-decoration: none;
  transition: top 120ms ease;
}
.skip-link:focus { top: 0; }
.skip-link:hover { text-decoration: none; }
/* The landing target itself is not a control, so it draws no ring. */
main:focus { outline: none; }

::selection { background: var(--acc); color: var(--acc-ink); }

/* Hides scrollbars on elements that scroll by design (e.g. a horizontal rail)
   without removing the ability to scroll. */
.noscroll { scrollbar-width: none; -ms-overflow-style: none; }
.noscroll::-webkit-scrollbar { width: 0; height: 0; display: none; }

button, a, input { -webkit-tap-highlight-color: transparent; touch-action: manipulation; }

img { max-width: 100%; height: auto; display: block; }

/* The horizontal gutter is the design's own clamp(18px, 3vw, 44px), not the
   4px scale's 16px -- it governs every page's horizontal rhythm, so it stays
   literal rather than rounded to the nearest --sN. */
.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 clamp(18px, 3vw, 44px); }

/* --- shell: rail, header, footer ------------------------------------------
   Every value below comes from .superpowers/sdd/design-index.html (the
   `.shell`/`.rail`/`.col` block and its two @media rules), ported from that
   file's 212 inline `style` attributes into one shared, cached stylesheet.
   See core/src/layouts/Base.astro and the Task 2 brief. */
.shell { min-height: 100vh; display: flex; align-items: stretch; background: var(--ground); color: var(--ink); font-family: var(--font); }

.rail {
  flex: 0 0 86px;
  border-inline-end: 1px solid var(--line);
  position: sticky; top: 0;
  align-self: flex-start;
  height: 100vh;
  display: flex; flex-direction: column;
  background: var(--ground);
  padding: var(--s3) 0;
  gap: var(--s2);
  z-index: 40;
}

/* The menu button, standing where the design put two decorative circles
   (Leaderboard, Profile) that had no route to point at.

   IT USED TO BE HIDDEN HERE, on the reasoning that the desktop rail was
   already the whole navigation. That stopped being true: the drawer has since
   gained the fifteen categories, the tag directory and the language picker,
   and none of them reached the rail -- so a desktop visitor had no way to
   change language at all, and had to go through /categories/ to reach a genre
   a phone could tap directly.
   The drawer is in the markup of every page either way. Hiding this button
   was not saving anything; it was locking a door that was already built. */
.rail-menu {
  flex: 0 0 auto;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  margin: 0 11px;
  padding: 10px 0;
  border: 0; border-radius: 18px;
  background: transparent; color: var(--dim);
  font: inherit; cursor: pointer;
}
.rail-menu span { font-size: 9px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; }
.rail-menu:hover, .rail-menu[aria-expanded='true'] { background: var(--panel); color: var(--ink); }

.rail-scroll { flex: 1 1 auto; overflow-y: auto; display: flex; flex-direction: column; gap: var(--s1); padding: 0 11px; }
.rail-item {
  flex: 0 0 auto;
  border: 0; border-radius: 18px;
  background: transparent; color: var(--dim);
  padding: 12px 6px 10px;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  min-width: 64px;
  text-decoration: none;
}
.rail-item:hover { background: var(--panel); color: var(--ink); text-decoration: none; }
.rail-item span { font-size: 10px; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; }
.rail-item[aria-current="page"] { background: var(--acc); color: var(--acc-ink); }
.rail-item[aria-current="page"]:hover { background: var(--acc); color: var(--acc-ink); }

.col { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }

.col > header {
  position: sticky; top: 0; z-index: 30;
  background: color-mix(in srgb, var(--ground) 90%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
  display: flex; align-items: stretch; flex-wrap: wrap;
}
.head-brand {
  display: flex; align-items: center;
  gap: 10px; /* the design's own value; not on the 4px scale */
  padding: 0 clamp(16px, 2.5vw, 28px);
  /* NO DIVIDER. A vertical rule sat here, boxing the brand off from the
     search field beside it -- so the header read as two panels with a seam
     down the middle rather than as one bar. The header's own bottom border
     already separates it from the page; a second line inside it was drawing
     a boundary that means nothing. */
  min-height: 72px;
  color: var(--ink); text-decoration: none;
}
.head-brand:hover { text-decoration: none; }
.head-brand .brand-mark { color: var(--acc); display: grid; place-items: center; }
.head-brand .brand-name { font-weight: 800; font-size: clamp(16px, 2vw, 21px); letter-spacing: -0.03em; }

.col > footer {
  border-top: 1px solid var(--line);
  margin-top: 20px; /* the design's own value; not on the 4px scale */
}
/* The footer, in two parts: a row that carries the brand, the site's own
   pages and the share buttons, and one line of prose under it.

   It used to be five stacked bands, each introduced by a tiny uppercase
   label -- brand, blurb, SHARE, SITE, copyright. Five headings for five short
   things, in the part of the page nobody came for. */
.foot-main { display: flex; flex-direction: column; gap: var(--s4); padding: clamp(24px, 3.5vw, 46px) clamp(18px, 3vw, 44px); }

.foot-top { display: flex; align-items: center; flex-wrap: wrap; gap: 14px clamp(16px, 3vw, 32px); }

.foot-brand { display: flex; align-items: center; gap: 11px; text-decoration: none; }
.foot-brand:hover { text-decoration: none; }
.foot-brand .brand-mark { color: var(--acc); display: grid; place-items: center; }
.foot-brand .brand-name { font-weight: 800; font-size: 22px; letter-spacing: -0.03em; color: var(--ink); }
.foot-brand svg { width: 28px; height: 28px; }

/* No label above these. A row of four words under the brand is self-evidently
   the site's own pages. */
.foot-links { display: flex; flex-wrap: wrap; gap: 2px 4px; min-width: 0; }
.foot-links a {
  display: inline-flex; align-items: center;
  min-height: 44px; padding: 0 10px;
  font-size: 15px; font-weight: 600; text-decoration: none;
}
.foot-links a:hover { color: var(--ink); }

.foot-blurb { margin: 0; max-width: 74ch; font-size: 13px; line-height: 1.65; color: var(--dim); }
/* Lives in .foot-bar now, beside the copyright, and inherits that row's type.
   The `margin-left` it used to carry was there to hold it off the end of a
   sentence -- a sentence it is no longer part of. */
.foot-credit { font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim); }
.foot-credit a { font-size: 11px; }

.foot-bar {
  border-top: 1px solid var(--line);
  display: flex; gap: var(--s4); flex-wrap: wrap; align-items: center;
  padding: var(--s4) clamp(18px, 3vw, 44px);
}
.foot-copy { font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim); margin-inline-end: auto; }
.foot-bar a { font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; text-decoration: none; }
.foot-bar a:hover { color: var(--ink); }

/* The rail collapses to a sticky horizontal bar and the search field drops
   to its own full-width row. */
@media (max-width: 780px) {
  .shell { flex-direction: column; }
  .rail {
    position: sticky; top: 0;
    align-self: stretch; width: 100%;
    height: auto;
    flex-direction: row; align-items: center;
    flex: 0 0 auto;
    border-inline-end: 0; border-bottom: 1px solid var(--line);
    padding: 8px 10px;
    background: color-mix(in srgb, var(--ground) 94%, transparent);
    backdrop-filter: blur(14px);
  }
  /* Spread, not packed. The four items used to sit hard left with the
     leftover width sitting empty before the divider -- which read as the bar
     being unfinished rather than as breathing room. flex-basis 0 makes the
     four share the row equally whatever their labels are, so CATEGORIES and
     NEW get the same column. */
  /* SIX EQUAL COLUMNS, COUNTED IN THE FLEX BASIS.
     `flex: 1 1 auto` sized this group by its CONTENT, so the four links took
     whatever they needed and the Search button beside them was measured at
     23x57 on a 320px screen -- half a tap target. `4` here is the number of
     items inside, so the group claims four sixths and Search and Menu take
     one each: six columns of the same width at every phone size. Add a link
     to the group and this number changes with it. */
  .rail-scroll { flex: 4 1 0; flex-direction: row; overflow-x: auto; overflow-y: hidden; padding: 0; justify-content: space-between; }
  .rail-item { flex: 1 1 0; min-width: 0; padding: 10px 2px 8px; }
  .rail-search { flex: 1 1 0; min-width: 0; }
  .rail-item span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .hero-art { min-height: 200px; /* design's own value */ }
  /* The game is nearly the whole screen on a phone, so the page owes it far
     less than a desktop's heading, bar and breadcrumb. */
  /* Breadcrumbs go on a phone. "Home / Categories / Puzzle" is 38px naming
     two destinations the bar at the top of the screen already carries, above
     a grid that is the reason for the page. They stay in the markup, so the
     crawler still reads the trail. */
  .crumbs { display: none; }
  /* .hero-desc is NOT hidden here any more -- see the end of this file. */
  main > .wrap { --player-chrome: 104px; }
}


/* The rail's buttons at the narrowest widths -- see the end of this file,
   where the rest of this breakpoint lives and why. */
@media (max-width: 420px) {
  .rail-item span { font-size: 9px; letter-spacing: 0.02em; }
}

/* --- headings ------------------------------------------------------------ */
h1 { font-size: clamp(26px, 4vw, 38px); line-height: 1.15; letter-spacing: -0.02em; margin: 0 0 var(--s3); }
h2 { font-size: clamp(19px, 2.4vw, 24px); letter-spacing: -0.01em; margin: var(--s6) 0 var(--s4); }
.lede { color: var(--dim); font-size: 17px; margin: 0 0 var(--s5); max-width: 62ch; }

/* --- home hero ------------------------------------------------------------
   Every value below comes from design-index.html's home hero (the
   `.hero-art` block and the text column beside it). See the Task 3 brief. */
.hero {
  /* The positioning context for the badge below. */
  position: relative;
  display: flex; flex-wrap: wrap;
  /* TOP, NOT CENTRE. The text is always shorter than the art, and centred
     it floated in the middle of the art's height: a band of nothing between
     the TRENDING GAME badge and the title, and a title whose top matched no
     edge. Aligned to the top it starts level with the picture, and the spare
     height falls below the description, above the slide dots. */
  align-items: flex-start;
  gap: clamp(20px, 3vw, 44px);
  padding: clamp(24px, 3.5vw, 52px) 0;
}
.hero-copy { flex: 1 1 320px; min-width: 0; display: flex; flex-direction: column; gap: 18px; /* design's own value */ }
/* THE BADGE SITS IN THE HERO'S TOP-RIGHT CORNER, OVER THE ARTWORK.
   It used to open the text column, which put it directly above the title --
   two stacked labels where the second is already the largest thing on the
   page, and a band of empty width beside it doing nothing.

   `right: 0` and a top matching the hero's own padding land it exactly on the
   art's top-right corner, because the art is the taller of the two columns
   and therefore defines the hero's top and right edges. No coordinate is
   copied from the art; move either column and this still lands on it.

   IT NEEDS ITS OWN BACKGROUND NOW. The old fill was 18% accent over the page,
   which only worked because the page behind it was always the same near-black.
   Over 13,734 different pieces of artwork it would be legible on some and
   invisible on others, so the chip carries its own dark backing and blurs what
   is behind it. */
/* --- the trending rail ---------------------------------------------------
   The swipe is CSS. `overflow-x: auto` plus `scroll-snap-type` is the
   browser's own gesture -- its momentum, its rubber-banding, its
   accessibility -- and public/j/hero.js only adds arrows for pointers, which
   have no gesture of their own.

   `overscroll-behavior-x: contain` so flicking past the last slide does not
   trigger the browser's back-navigation gesture, which on iOS would take
   somebody off the site for swiping one game too far. */
/* The rail's own header. `grid-head` gives it the row; this only takes the
   top margin off, because it is the first thing under the site header and
   the hero below already carries the page's opening space. */
/* Badge on the left, See all on the right, one line, and it does not move
   when the rail does -- it names the shelf, not the game currently on it. */
.hero-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin: var(--s4) 0 var(--s4);
}
.hero-head .see-all { margin-inline-start: auto; }

/* The rail had no bottom margin at all, so the dots ended on the exact pixel
   the next section's heading began -- measured at 0px between them. Nothing
   overlapped, which is why no guard saw it; there was simply no air, and the
   dots read as punctuation belonging to the heading below rather than as the
   position of the rail above. */
.hero-rail { position: relative; margin-bottom: var(--s6); }
.hero-track {
  display: flex;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;
}
.hero-track::-webkit-scrollbar { display: none; }
.hero-track > .hero {
  flex: 0 0 100%;
  scroll-snap-align: center;
  /* A flex item's default `min-width: auto` is its CONTENT width, and this
     one holds a 1024px picture -- eight of those make a page 8,000px wide
     instead of a rail that scrolls. */
  min-width: 0;
}

/* Pointer only. A finger has the gesture, and arrows over the artwork on a
   phone would cover the game they are advertising. */
.hero-nav { display: none; }
@media (min-width: 781px) {
  .hero-nav {
    position: absolute; top: 50%; transform: translateY(-50%); z-index: 3;
    width: 40px; height: 40px;
    display: grid; place-items: center;
    border: 1px solid var(--line); border-radius: 999px;
    background: color-mix(in srgb, var(--ground) 82%, transparent);
    backdrop-filter: blur(8px);
    color: var(--ink);
    cursor: pointer;
    transition: opacity .14s ease, border-color .14s ease;
  }
  .hero-nav:hover { border-color: var(--acc); }
  /* Kept in the layout rather than removed, so the rail does not reflow when
     somebody reaches either end. */
  .hero-nav:disabled { opacity: .3; cursor: default; }
  /* BOTH ARROWS LIVE OVER THE ARTWORK, NOT ONE OVER THE WORDS.
     Prev sat at the start edge, which on a desktop is inside the slide's TEXT
     column: measured at 1024, 1280 and 1440 it covered the description, and
     at 1280 the title as well -- a 40px disc sitting on "Dive into a
     mesmerizing...". The symmetry that put it there is a symmetry the layout
     does not have: this hero is text on one side and a picture on the other,
     and only one of those can take something on top of it.
     So they pair up over the picture. Logical properties, so in Arabic both
     move to the left, which is where that layout puts its artwork. */
  .hero-prev { inset-inline-end: 52px; }
  .hero-next { inset-inline-end: 4px; }
}

/* Static, at every width. It used to be absolutely positioned over the
   artwork, with a phone breakpoint putting it back in the flow -- two
   positions and a media query to keep in sync. Out here in its own row it
   needs neither. */
.hero-badge {
  display: inline-flex; align-items: center; gap: var(--s2);
  border-radius: 999px;
  background: rgba(14, 14, 16, .72);
  border: 1px solid color-mix(in srgb, var(--acc) 38%, transparent);
  backdrop-filter: blur(8px);
  color: var(--acc);
  padding: 7px 15px; /* design's own values */
  /* Matches See all beside it. That button is 44px because a finger needs 44;
     the badge is not a target and needs nothing, but the two now share a row,
     and a 33px chip next to a 44px button reads as a mistake. */
  min-height: 44px;
  font-size: 11px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase;
}
.hero-title { margin: 0; font-weight: 900; font-size: clamp(34px, 4.6vw, 64px); line-height: 0.96; letter-spacing: -0.04em; }
/* Shown while the hero is two columns, hidden once it stacks. The width is
   what changes the answer: beside a 360px picture this paragraph is what makes
   the column balance, and stacked on a 390px screen the same words are six
   lines of feed copy standing between the featured game's name and the grid.
   The breakpoint that decides it is at the end of this file, and it is the
   width where the hero ACTUALLY stacks -- measured, not assumed. */
.hero-desc { margin: 0; max-width: 46ch; font-size: clamp(15px, 1.2vw, 17px); line-height: 1.55; color: var(--dim); text-wrap: pretty; }
/* Four lines at most, whatever reaches it: every slide in the rail is as tall
   as the tallest, so one long paragraph empties the space under all the
   others. The phone rule at the end of this file still hides it. */
.hero-desc { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4; line-clamp: 4; overflow: hidden; }

/* A LEFT-TO-RIGHT PARAGRAPH INSIDE A RIGHT-TO-LEFT COLUMN.
   The description is the publisher's sentence, so it stays in the language
   they wrote it in -- English, nearly always -- while the page around it may
   be Arabic. A block with a measure is placed at its container's START, which
   in RTL is the right, so the narrow paragraph sat 127px right of the title
   above it and the two English texts began at different places. Dropping the
   measure puts both blocks on the same edge. The line then runs to about 70
   characters, which is the cap the rest of this site already uses for prose. */
[dir="rtl"] .hero-desc { max-width: none; }

.hero-art {
  position: relative;
  flex: 1 1 320px; min-width: 0;
  aspect-ratio: 16 / 10;
  min-height: 250px; /* design's own value */
  border-radius: 26px; /* design's own value */
  overflow: hidden;
  display: block;
  background: var(--panel);
}
.hero-art img { width: 100%; height: 100%; object-fit: cover; }
.hero-play {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 86px; height: 86px; /* design's own value */
  border-radius: 999px;
  background: var(--ink); color: var(--ground);
  display: grid; place-items: center;
  box-shadow: 0 12px 44px rgba(0, 0, 0, .55); /* design's own value */
  pointer-events: none;
}
/* PHYSICAL ON PURPOSE, and the only one left in this file besides a centring
   pair. A play triangle points the same way in Arabic as in English -- media
   transport controls are not mirrored by any platform -- so this nudge must
   not become margin-inline-start, which would move it the wrong way in RTL.
   test/rtl.test.mjs allows this line by name. */
.hero-play svg { margin-left: 5px; /* design's own value: optical centring of the triangle */ }

/* --- section header ------------------------------------------------------
   A row heading and one link onward. This was the design prototype's
   `.sortbar`: a pill bar whose three pills -- Popular, Newest, Categories --
   were the left rail's own links repeated, rendered as a full-width panel of
   low-contrast uppercase under a stat line that counted the catalogue at the
   visitor. Navigation said twice is navigation nobody trusts the first
   time. */
/* nowrap, and the TITLE is what gives way.
   Wrapping dropped "See all" onto its own line under the heading at 390px --
   measured, not guessed -- which reads as an orphaned button rather than as
   the section's one way onward. Letting the words break instead keeps the
   pair on one row at every width. min-width: 0 is what allows it: without it
   a flex item refuses to shrink below its longest line and overflows. */
.grid-head { display: flex; align-items: center; flex-wrap: nowrap; gap: 12px; margin-bottom: 20px; /* design's own value */ }
.grid-title { margin: 0 auto 0 0; min-width: 0; }

.see-all {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 44px;          /* measured at 35px */
  border: 1px solid var(--line); border-radius: 999px;
  padding: 8px 15px;
  color: var(--dim);
  font-size: 11px; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase;
  text-decoration: none;
  transition: background .14s ease, color .14s ease, border-color .14s ease;
}
.see-all:hover {
  background: var(--acc); color: var(--acc-ink); border-color: var(--acc);
  text-decoration: none;
}

/* AN ARROW THAT MEANS "ONWARD" HAS TO TURN ROUND IN ARABIC.
   Everything else on this row mirrors by itself: the flexbox reverses, the
   badge moves to the right and the link to the left, because the layout is
   written in logical properties. The drawing inside the link does not -- an
   SVG path is coordinates, and coordinates have no direction -- so in Arabic
   the button sat on the left of the row with its arrow pointing back at the
   badge it came from.

   The play triangle above is the counter-example and the reason this list is
   named rather than blanket: transport controls point the same way in every
   script, so mirroring every icon in the file would break that one. These
   three say "further along", which is a direction, and a direction mirrors. */
[dir="rtl"] .see-all svg,
[dir="rtl"] .rail-more svg,
[dir="rtl"] .hero-nav svg,
[dir="rtl"] .game-desc-more svg,
/* The sign-in glyph is an arrow going through a doorway, and "through" is a
   direction like any other: in Arabic you enter leftwards, so the arrow and
   the door swap with everything else on the row. Platforms mirror their own
   log-in and log-out icons for exactly this reason. */
[dir="rtl"] [data-auth-signin] svg { transform: scaleX(-1); }

/* --- game grid ----------------------------------------------------------- */
/* auto-fill, not auto-fit: a row with two games in it should leave the rest of
   the row empty rather than stretching two cards across the whole width.
   146px and the gap clamp are design-index.html's own values (its grid-
   auto-flow: dense is dropped with it: that only mattered for the design's
   wide-card variant, which this site does not build -- see the Task 3
   report). */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: clamp(12px, 1.5vw, 20px); padding: 0; margin: 0; list-style: none; }

/* THE DIRECTORY TILE -- /categories/ and /tags/.
   One colour, a large icon with nothing behind it, the name, the count.
   Categories were fifteen hues in tinted boxes and tags were a cloud of
   pills down the left half of the page; both now fill the width in the same
   grid, so the two directories read as one design. */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: clamp(10px, 1.2vw, 16px);
  margin: var(--s5) 0 0; padding: 0; list-style: none;
}
.tile {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--s1);
  height: 100%;
  padding: clamp(16px, 1.8vw, 22px);
  border-radius: var(--radius-lg);
  background: var(--panel);
  color: var(--ink);
  text-decoration: none;
  transition: background .12s ease, transform .12s ease;
}
.tile:hover, .tile:focus-visible {
  background: var(--panel-2);
  transform: translateY(-2px);
  text-decoration: none;
}
.tile-icon {
  width: 40px; height: 40px;
  margin-bottom: var(--s3);
  color: var(--acc);
  flex: none;
}
.tile-name { font-size: 17px; font-weight: 700; line-height: 1.25; letter-spacing: -0.01em; }
.tile-count { font-size: 13px; font-weight: 500; color: var(--dim); font-variant-numeric: tabular-nums; }
@media (max-width: 560px) {
  .tile-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tile-icon { width: 34px; height: 34px; }
  .tile-name { font-size: 15px; }
}

/* The game card (GameCard.astro).
   ------------------------------------------------------------------------
   A card is one square and nothing else: the picture, a scrim, and the title
   over it. It used to be a square plus a two-line caption block beneath --
   title and category -- and a rank number burned into the corner of the
   first 120.

   Three things were wrong with that. The numbers ranked games by a score the
   visitor cannot see and did not ask for, and "01" over the artwork is the
   one thing on a card that is not the game. The category line repeated, on
   every tile, what the page heading or the rail had already said. And the
   caption block made a card's height depend on how long its title was, so a
   grid row was as tall as its wordiest game.

   Putting the title on the picture fixes all three at once: every tile is
   exactly a square, the grid packs tighter, and more games fit above the
   fold on the phone where most of them are opened. The scrim is what keeps
   it readable -- a thumbnail is arbitrary artwork, and white text over a
   pale sky is not text. */
.card-game { display: block; transition: transform 180ms cubic-bezier(.2, .8, .3, 1); }
.card-game:hover, .card-game:focus-within { transform: translateY(-4px); /* design's own value */ }
.card-game a { display: block; color: inherit; text-decoration: none; }
/* The aspect ratio is reserved before the picture loads, so a grid of cards
   does not reflow as thumbnails arrive. */
.card-game .art { position: relative; aspect-ratio: 1 / 1; border-radius: var(--radius-lg); overflow: hidden; display: block; background: var(--panel-2); }
.card-game .shot { width: 100%; height: 100%; object-fit: cover; display: block; }
.card-game .scrim {
  position: absolute; left: 0; right: 0; bottom: 0; height: 70%;
  background: linear-gradient(to top, rgba(8, 8, 10, .93) 0%, rgba(8, 8, 10, .6) 40%, transparent 100%);
  pointer-events: none;
}
.card-game .info {
  position: absolute; left: 10px; right: 10px; bottom: 8px;
  display: flex; flex-direction: column; gap: 1px; min-width: 0;
}
.card-game .name {
  color: #fff;
  font-weight: 800; font-size: 13px; line-height: 1.22; letter-spacing: -0.015em;
  text-shadow: 0 1px 10px rgba(0, 0, 0, .6);
  /* Two lines, then an ellipsis. Truncating to one loses the half of a long
     title that tells two sequels apart. */
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* See GameGrid.astro's `oneCategory`. */
.grid.one-cat .card-game .cat { display: none; }
.card-game .cat {
  /* Not var(--dim): that is tuned against the page background, and this sits
     on arbitrary artwork behind a scrim. */
  color: rgba(255, 255, 255, .64);
  font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
  text-shadow: 0 1px 8px rgba(0, 0, 0, .6);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A game its provider says does not play on a phone (GameCard.astro's
   data-desktop): off every grid on a touch screen, where its card would be a
   tap into a game that cannot be played. `hover: none` keeps it for a laptop
   whose screen also takes touch -- its primary pointer is still the mouse. */
@media (hover: none) and (pointer: coarse) {
  .card-game[data-desktop] { display: none; }
}

/* --- player ---------------------------------------------------------------
   THE BOX IS THE SHAPE OF THE GAME. `--ratio` is written onto .player by
   g/[slug].astro from that game's own width and height; see the long note
   there for the measurement. It was a hard-coded 16/10 (1.600) against a
   catalogue that is 4:3 (1.333) and 3:4 (0.750) and nothing else, so every
   landscape game sat in a box 20% wider than its canvas and GamePix filled
   the difference with colour down both sides.

   A custom property substitutes as tokens, so one declaration serves both
   jobs: `aspect-ratio: var(--ratio)` reads `800 / 600`, and inside the
   calc() below the same tokens divide a length.

   --player-chrome is what the viewport owes the rest of the page. Without a
   height cap a 3:4 game on a desktop would be 1,600px tall and somebody
   would have to scroll to see the bottom half of a game. Capping by height
   and letting width follow keeps the canvas undistorted either way. */
/* THE PLAYER AND ITS CONTROLS ARE SIZED BY ONE NUMBER, DECLARED ONCE.
   --player-chrome used to live on .player, where the row of buttons beneath
   it could not read it -- so .game-actions spanned the whole column while the
   player was capped by viewport height and centred. Measured at 1440 the
   share button sat 137px past the player's right edge, and at 1920, 296px:
   controls floating outside the thing they control.

   --stage-max is the cap itself, computed once here. It uses 4/3 rather than
   var(--shape) on purpose: --shape becomes the game's own ratio only while it
   is PLAYING, and a playing game is fullscreen with no buttons under it. The
   splash, which is what the buttons sit below, is always 4/3. */
main > .wrap {
  --player-chrome: 210px;
  --stage-max: calc((100vh - var(--player-chrome)) * 4 / 3);
  --stage-max: calc((100dvh - var(--player-chrome)) * 4 / 3);
}

.player {
  /* THE CORNER CONTROLS' CONTAINING BLOCK. .player-fs and .player-exit are
     absolutely positioned, and without this the nearest positioned ancestor
     was <body>: in fullscreen the player is its own box and nobody noticed,
     but back on the page the Fullscreen button sat 10px from the top of the
     PAGE, inside the header, and a player who left fullscreen could not find
     the way back in. Measured 2026-09-24 at 1456px: the button's offsetParent
     was BODY, its top 10, the player's top 89. */
  position: relative;
  display: grid; place-items: center;
  background: #000;
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  overflow: hidden;
  /* The BOX carries the cap, not its children. With the cap on the child the
     box stayed page-width around a correctly-shaped game: measured at
     1440x900, a 600x800 game sat 517px wide inside 1192px of black. Centred
     by the auto margins, so a portrait game is a portrait box in the middle
     of the page rather than a strip in a letterbox.
     vh first, dvh second: a browser without dvh keeps a working cap. */
  /* THE SPLASH IS OURS, SO IT KEEPS OUR SHAPE. --shape is 4/3 until a game
     is actually running: a 600x800 game was giving the poster a 3:4 box
     1,080px tall on a phone -- a full screen of blurred artwork and one
     button -- while the 800x600 game next to it got a neat landscape card.
     The splash has no canvas to match, so it stops pretending to. On Play,
     --shape becomes the game's own ratio and the box takes the shape the
     canvas actually needs. */
  --shape: 4 / 3;
  width: 100%;
  max-width: calc((100vh - var(--player-chrome)) * var(--shape));
  max-width: calc((100dvh - var(--player-chrome)) * var(--shape));
  margin: var(--s4) auto 0;
}
.player.is-playing { --shape: var(--ratio, 4 / 3); }
.player iframe {
  display: block; border: 0;
  width: 100%;
  aspect-ratio: var(--ratio, 4 / 3);
}
/* In fullscreen the whole viewport is the budget -- native, or the pinned
   overlay used where element fullscreen does not exist.

   EVERY SELECTOR BELOW STANDS ALONE, and that is not style. A browser drops
   the whole rule when any selector in a comma-separated list is one it cannot
   parse. `:fullscreen` unprefixed landed in Safari 16.4; grouped with
   `.is-immersive`, it silently deleted the fallback overlay on exactly the
   browsers the fallback exists for. */
.player.is-immersive { border: 0; border-radius: 0; margin: 0; max-width: none; --shape: var(--ratio, 4 / 3); }
.player:fullscreen { border: 0; border-radius: 0; margin: 0; max-width: none; --shape: var(--ratio, 4 / 3); }
.player:-webkit-full-screen { border: 0; border-radius: 0; margin: 0; max-width: none; --shape: var(--ratio, 4 / 3); }

.player.is-immersive {
  position: fixed; inset: 0; z-index: 60;
  width: 100vw; height: 100vh; height: 100dvh;
}
.player.is-immersive iframe { width: 100%; height: 100%; max-width: none; aspect-ratio: auto; }
.player:fullscreen iframe { width: 100%; height: 100%; max-width: none; aspect-ratio: auto; }
.player:-webkit-full-screen iframe { width: 100%; height: 100%; max-width: none; aspect-ratio: auto; }

/* --- the poster ----------------------------------------------------------
   Deliberately the shape of GamePix's own splash -- blurred art behind, the
   square icon, the title, one button -- so pressing Play does not visibly
   change venue. See the long note in g/[slug].astro for why the frame is not
   loaded until then. */
.poster {
  position: relative;
  display: block; width: 100%;
  aspect-ratio: 4 / 3;
  background: var(--panel-2);
  overflow: hidden;
}
.poster-bg {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  /* Scaled past the edges because a blur samples beyond them and would
     otherwise leave a pale rim all the way round. */
  transform: scale(1.18);
  filter: blur(26px) saturate(1.25) brightness(.55);
}
.poster-card {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px; padding: 16px;
}
.poster-icon {
  /* The upper bound is for a desktop, where the box is 920px wide and a
     148px icon read as a stamp in a field. It is the only picture of the
     game on that screen until somebody presses Play. */
  width: clamp(88px, 22%, 196px); height: auto;
  border-radius: 22px;
  box-shadow: 0 14px 44px rgba(0, 0, 0, .55);
}
.poster-name {
  margin: 0;
  color: #fff; font-weight: 800; font-size: clamp(19px, 2.8vw, 34px); line-height: 1.2;
  letter-spacing: -0.02em; text-align: center;
  text-shadow: 0 2px 14px rgba(0, 0, 0, .6);
  max-width: 90%;
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.poster-cta {
  display: inline-flex; align-items: center; gap: 9px;
  border: 0; border-radius: 999px;
  background: var(--acc); color: var(--acc-ink);
  padding: 13px 26px;
  font: inherit; font-weight: 800; font-size: 14px; letter-spacing: 0.02em;
  cursor: pointer;
}
/* The whole poster is the hit area. The button stays a button -- one control,
   one accessible name -- and simply covers the box it sits in. */
.poster-cta::after { content: ''; position: absolute; inset: 0; cursor: pointer; }
.poster-cta:hover { filter: brightness(1.1); }
.poster-cta:active { filter: brightness(.9); }
.poster-cta:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
/* The poster is gone the moment the frame exists; it is not a loading state
   to return to. */
.player.is-playing .poster { display: none; }

/* BOTTOM right: where a player looks for fullscreen -- the corner every
   video player and game portal puts it in -- and away from the top edge,
   where games put their own pause and settings buttons. */
.player-fs {
  position: absolute; bottom: 10px; inset-inline-end: 10px; z-index: 2;
  width: 40px; height: 40px;
  display: none; place-items: center;
  border: 0; border-radius: 999px;
  background: rgba(8, 8, 10, .62); color: #fff;
  backdrop-filter: blur(6px);
  cursor: pointer;
}
/* Top LEFT, and a chevron rather than an X: it leaves the game, it does not
   dismiss a dialog. */
/* Small, dark, and readable on either background.
   ------------------------------------------------------------------------
   It was a near-white pill carrying a chevron AND the site's mark, 40px tall
   and about 70 wide. Over a game's own loading screen -- which is often plain
   white -- that is the largest and brightest thing on the display, and the
   mark inside it was doubling the width to say something the visitor already
   knows: whose site they are on.

   Dark translucent with a hairline of light instead. The fill keeps it
   legible over a pale loading screen, the border keeps it legible over a dark
   one, and neither depends on knowing what the game is about to draw.

   36px rather than the 44 used elsewhere on the site: WCAG 2.5.8 asks 24, the
   44 is this site's own floor for controls in a page, and a control floating
   over somebody's game is the one place where being small is part of the
   job. */
.player-exit {
  position: absolute; top: 10px; inset-inline-start: 10px; z-index: 2;
  /* A pill, not a circle: it holds a chevron AND the mark. Height stays 36px
     -- the size that was settled on -- and only the width follows the second
     glyph. The COLOURS are the ones from the dark rework and are the only
     thing about this button that changed from the original. */
  min-height: 36px; padding: 0 11px; gap: 7px;
  display: none; align-items: center; justify-content: center;
  /* DARK AGAIN, AND THIS TIME WITH A RIM.
     It went light to match GamePix's own pill, and it was then the brightest
     thing on the screen over every game -- a white tile in the corner of a
     site that is dark everywhere else. Dark near-opaque now, like the rest of
     the site's chrome. What the old dark version lacked was an edge: over a
     dark loading screen it disappeared. The light hairline and the shadow give
     it one on dark, and the fill carries it on pale.
     Near-opaque for the reason the light one was: 55% of anything lets the
     blur through and turns the chevron to mush. */
  border: 1px solid rgba(255, 255, 255, .22); border-radius: 12px;
  background: rgba(14, 14, 16, .92); color: #fff;
  box-shadow: 0 2px 14px rgba(0, 0, 0, .55);
  cursor: pointer;
}

/* The favourite control lives in .game-actions under the player now: a
   labelled button somebody can read, rather than a glyph over artwork. */
.player-fs:hover { background: rgba(8, 8, 10, .85); }
.player-exit:hover { background: #1c1c20; border-color: rgba(255, 255, 255, .4); }
/* Only once a game is running: until then the poster's own button is the way
   in, and it goes fullscreen by itself. */
.player.is-playing .player-fs { display: grid; }
.player.is-immersive .player-fs { display: none; }
.player:fullscreen .player-fs { display: none; }
.player:-webkit-full-screen .player-fs { display: none; }
/* In the pinned overlay, and in native fullscreen too: a desktop browser's
   own way out is Esc and a hint that fades. Where the browser draws a close
   button of its own over this corner -- iPadOS Safari -- game.js never asks
   for native fullscreen (drawsOwnClose), so the two are never stacked. */
.player.is-immersive .player-exit { display: flex; }
.player:fullscreen .player-exit { display: flex; }
.player:-webkit-full-screen .player-exit { display: flex; }

/* The related rail's header: the same row the home page uses -- a heading and
   one link onward -- rather than a bare sentence. "More adventure games" told
   somebody what they were looking at and gave them nowhere to go; the whole
   category is one tap away now. */
.rail-head { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin: var(--s5) 0 var(--s4); }
/* The home page's "Continue playing" shelf ends in a grid and the next
   section opens with a heading -- and nothing sat between them: measured at
   0px on a phone, the last row of cards touching "Play free games online". */
.continue { margin-bottom: clamp(32px, 4vw, 48px); }
.rail-head h2 { margin: 0 auto 0 0; font-size: clamp(18px, 2.2vw, 22px); }
/* The "See all N games" button under a game page's related games: the
   whole width on a phone, where it is the next thing a thumb reaches after
   the last card; a centred pill on a desktop. */
.rail-more {
  display: flex; align-items: center; justify-content: center; gap: var(--s2);
  width: 100%; max-width: 420px; min-height: 48px;
  margin: var(--s5) auto 0; padding: 0 var(--s5);
  border-radius: 999px;
  background: var(--panel-2); color: var(--ink);
  font-size: 15px; font-weight: 700;
  text-decoration: none;
  transition: background .12s ease, color .12s ease;
}
.rail-more:hover, .rail-more:focus-visible {
  background: var(--acc); color: var(--acc-ink); text-decoration: none;
}

/* The actions under the player: keep it, send it to somebody. */
.game-actions {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  padding: var(--s4) 0 0;
  /* Same cap and same centring as the player above, from the same variable.
     Verified at 390, 1024, 1440 and 1920: the row's right edge and the
     player's now agree exactly at all four. */
  max-width: var(--stage-max);
  margin-inline: auto;
}
/* The best result and where the progress lives. Under the actions row, on
   the same cap as the player, and quiet: it is a fact about the reader's own
   play, not a headline, and it only exists once they have played. */
.progress-note {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px;
  max-width: var(--stage-max);
  margin: var(--s3) auto 0;
  color: var(--dim); font-size: 13.5px;
}
.progress-stat { color: var(--ink); font-weight: 800; }
/* g/[slug].astro: a desktop-only game, opened on a touch screen anyway. */
.desk-note { display: none; }
@media (hover: none) and (pointer: coarse) {
  .desk-note {
    display: block;
    max-width: var(--stage-max);
    margin: var(--s3) auto 0;
    color: var(--ink); font-size: 16px; font-weight: 600; line-height: 1.45;
  }
}
.progress-stat + .progress-stat::before { content: '·'; margin-inline-end: 14px; color: var(--dim); font-weight: 400; }

.game-action {
  display: inline-flex; align-items: center; gap: 9px;
  min-height: 44px; padding: 0 18px;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--panel-2); color: var(--ink);
  font: inherit; font-weight: 800; font-size: 12px;
  letter-spacing: 0.06em; text-transform: uppercase;
  cursor: pointer;
}
.game-action:hover { border-color: var(--acc); }
.game-action.is-on { background: var(--acc); color: var(--acc-ink); border-color: var(--acc); }

/* The two thumbs and their verdict, kept together so they read as one control
   rather than three things that happen to be adjacent. */
.vote-group { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* NO CHIP, JUST THE ICON -- AND THE TARGET STAYS 44px.
   These were filled circles with an 18px glyph inside, which made a row of
   five dark discs under a piece of game art: chrome competing with the thing
   the page is for. Dropping the fill and the border leaves the icon alone on
   the page, and the 24px glyph is readable at a glance in a way the 18px one
   inside a disc was not.
   The BUTTON is still 44x44 -- the box is invisible, not absent, so a thumb
   still has the target WCAG 2.5.8 asks for. ui-sweep confirms it: "under
   44px: none" with the fill gone. */
.vote-btn,
.game-share-btn,
.game-desc > summary {
  padding: 0; width: 44px; justify-content: center;
  /* Not a circle. There is no fill left to see it on, but the focus ring
     follows the radius, so a keyboard still gets a rounded square rather
     than a lozenge around a square glyph. */
  border-radius: 12px;
  background: none;
  border-color: transparent;
  color: var(--dim);
}
.vote-btn svg,
.game-share-btn svg,
.game-desc > summary svg { width: 24px; height: 24px; }
.vote-btn:hover,
.game-share-btn:hover,
.game-desc > summary:hover { color: var(--ink); border-color: transparent; }

/* "On" is now the icon's own colour, since there is no fill left to invert. */
.vote-btn.is-on { background: none; border-color: transparent; color: var(--acc); }
/* A dislike that lights up in the brand's accent would be congratulating
   somebody for disliking it. */
.vote-btn[data-vote-down].is-on { background: none; color: var(--ink); border-color: transparent; }
/* A number, not a sentence. "No ratings yet" and "87% liked it · 142 votes"
   are both too wide to share a line with six controls on a 390px phone; the
   full wording lives in the title attribute, where somebody who wants it can
   find it. */
.vote-rating { color: var(--dim); font-size: 12.5px; white-space: nowrap; font-weight: 700; }

.game-action.is-on svg { fill: currentColor; }

/* --- share ---------------------------------------------------------------
   One treatment for both kits -- the footer's and the game page's. They were
   diverging: the game row was grey and tight while the footer stayed six
   brand colours spread across the width, which read as two different sites.

   MONOCHROME, AND `!important` IS THE ONLY WAY TO GET IT. The widget writes
   each network's brand colour as an INLINE style on the anchor -- Facebook
   arrives as `style="background-color: rgb(8, 102, 255)"` -- and an inline
   declaration outranks every class rule in this file. An important one
   outranks it back.

   TIGHT, AND TWO STYLESHEETS WERE BOTH ADDING SPACE. The widget puts
   `gap: 8px` on the kit and this file used to add `margin: 10px` to every
   button to hold a 44px target. Together they turned six 40px buttons into
   about 380px of footer. The padding inside the button is the target now, so
   neither is wanted. */
/* HARD RIGHT, VIA AUTO MARGIN, NOT VIA A GAP.
   Brand, then the site's own pages, then sharing -- and the share row pinned
   to the far edge so the three read as two groups rather than as three things
   adrift in a row. Without this the kit simply followed the links at whatever
   distance the row's gap happened to be, which is why it looked placed by
   accident. On a phone the row wraps and each line lays out on its own, so
   the margin costs nothing there. */
.foot-share { display: flex; flex-wrap: wrap; align-items: center; min-width: 0; margin-inline-start: auto; gap: 4px !important; }

/* THE GAME PAGE HAS ONE SHARE BUTTON, NOT A KIT. Four networks under a game,
   beside a heart and two votes, made a row of six controls on a phone -- and
   three of them were our guess at where somebody wanted to send a link. The
   button is ours (.game-share-btn, styled as every other control in the row)
   and this kit is only the machinery behind it: one trigger, off-screen, for
   the browsers with no share sheet of their own. See public/j/share.js.

   Off-screen rather than display:none because the widget measures its own
   buttons as it builds them, and a box that is not laid out measures zero. */
/* `margin-left: auto` and nothing else. Keeping and rating are things you do
   TO a game, sharing is a thing you do WITH it, and the gap between them is
   that distinction. A wrapped flex row lays each line out on its own, so this
   pushes the button right whether it shares a line with the other controls or
   drops to one of its own -- a media query pinning it left below 480px was
   here briefly and was simply wrong, since on a 430px phone the row always
   wraps and the rule fired on exactly the screens it was meant to spare. */
/* The auto margin lives on the FIRST of the two, not on share. In a flex
   row it absorbs the free space before the item it sits on, so putting it
   here carries the description button AND the share button to the right edge
   as one pair. On share alone it left the description stranded with the
   votes, which is where it read as a fourth opinion button rather than as
   what it is. */
.game-desc { margin-inline-start: auto; }
.game-share-kit {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.foot-share .ats_btn {
  margin: 0 !important;
  background-color: var(--panel-2) !important;
  color: var(--dim) !important;
  border-radius: 8px !important;
  /* 24px, MEASURED RATHER THAN ASSUMED.
     `ats_kit_size_24` sizes the icon's CONTENT box to 24px -- the glyph inside
     it measures 19px -- and this file was then adding 8px of padding on every
     side. 24 + 16 = a 40px chip, six of them, and that is what read as too
     big. No padding, so the chip IS the 24px the class name promises.
     The phone keeps a thumb-sized target; see the media query at the end. */
  padding: 0 !important;
  box-sizing: content-box !important;
  transition: background-color .14s ease, color .14s ease;
}
.foot-share .ats_btn:hover {
  background-color: var(--acc) !important;
  color: var(--acc-ink) !important;
}

/* --- the share sheet the "+" opens ---------------------------------------
   The widget ships a light theme and a dark one, and nobody had told it which
   site it was on: the sheet opened white, with near-black text, over a
   near-black page. Base.astro now sets ats_config.theme before the script
   loads, so its own dark styles apply; these rules only close the gap between
   "a dark theme" and THIS dark -- the same panel, line and ink tokens every
   other surface here uses. */
.ats_overlay { background: rgba(8, 8, 10, 0.62) !important; backdrop-filter: blur(2px); }
.ats_menu {
  background: var(--panel) !important;
  color: var(--ink) !important;
  border: 1px solid var(--line) !important;
  border-bottom: 0 !important;
}
.ats_menu_head, .ats_menu_bar { border-color: var(--line) !important; }
.ats_menu_title { color: var(--ink) !important; }
.ats_menu_count, .ats_menu_label { color: var(--dim) !important; }
.ats_menu_search {
  background: var(--panel-2) !important;
  color: var(--ink) !important;
  border: 1px solid var(--line) !important;
}
.ats_menu_close { color: var(--dim) !important; }
.ats_menu_close:hover { color: var(--ink) !important; }
.ats_menu_item:hover { background: var(--panel-2) !important; }
/* The icons in the SHEET keep their colours, unlike the row. In the row they
   are chrome competing with game artwork; here they are the content, each one
   labelled, and colour is what makes sixty-nine of them scannable. */


.tag { display: inline-block; background: var(--panel); border: 1px solid var(--line); border-radius: 999px; padding: 2px var(--s3); font-size: 13px; text-decoration: none; color: var(--dim); }
.tag:hover { color: var(--ink); border-color: var(--acc); }

/* Everything written about the game, below the game and below the related
   rail. Ruled off so it reads as reference rather than as something standing
   between a visitor and the iframe. */
.about { margin-top: var(--s6); padding-top: var(--s5); border-top: 1px solid var(--line); }
/* Folded by default once game.js confirms there is something to unfold. The
   mask fades the last lines into the page rather than guillotining a
   sentence, which is what tells somebody there is more without a label
   saying so. */
.about-body.is-clamped {
  max-height: 340px; overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
}
.about-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px;          /* measured at 41px: three short of a thumb */
  margin-top: var(--s4);
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--panel-2); color: var(--ink);
  padding: 11px 24px;
  font: inherit; font-weight: 800; font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase;
  cursor: pointer;
  transition: background .14s ease, color .14s ease, border-color .14s ease;
}
.about-toggle:hover { background: var(--acc); color: var(--acc-ink); border-color: var(--acc); }
.about h2 { margin-top: 0; }

/* --- the description button under the player ---------------------------
   A <details> styled as one more control in the actions row. The panel is
   absolutely positioned so opening it does not push the row -- and therefore
   the whole page -- downward under the reader's finger. */
.game-desc { position: relative; }
.game-desc > summary {
  /* list-style on the element and ::-webkit-details-marker for Safari, which
     ignores the first one. Without both, a disclosure triangle appears inside
     a round icon button. */
  list-style: none;
  cursor: pointer;
}
.game-desc > summary::-webkit-details-marker { display: none; }
.game-desc > summary::marker { content: ''; }
/* Open is the accent on the glyph, matching how the vote buttons now show
   their state -- there is no fill to invert on either. */
.game-desc[open] > summary { background: none; color: var(--acc); border-color: transparent; }

.game-desc-body {
  position: absolute; z-index: 30;
  /* RIGHT, not left. The button moved to the right end of the row when the
     auto margin moved onto .game-desc, and a panel still anchored `left: 0`
     opened off the side of the page -- measured overflowing at every width
     from 640 to 1600. It hangs from the edge the button is now on. */
  top: calc(100% + 8px); inset-inline-end: 0;
  /* Wide enough to read, never wider than the column. 46ch is the measure the
     hero paragraph uses, for the same reason: prose stops being readable past
     it whatever the container allows. */
  width: max(260px, min(46ch, 88vw));
  padding: 14px 16px;
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--panel);
  box-shadow: 0 18px 40px rgba(0, 0, 0, .45);
}
.game-desc-body p { margin: 0; color: var(--dim); font-size: 14px; line-height: 1.6; text-wrap: pretty; }
.game-desc-more {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 10px;
  color: var(--acc); font-size: 12px; font-weight: 800;
  letter-spacing: 0.04em; text-transform: uppercase; text-decoration: none;
}
.game-desc-more:hover { text-decoration: underline; }

/* On a phone the row is near the left edge, so the panel is pinned to the
   viewport rather than to the button: anchored left it would run off the
   screen the moment the button is not the first control in the row. */
@media (max-width: 560px) {
  .game-desc { position: static; }
  .game-desc-body { left: 0; right: 0; width: auto; }
  .game-actions { position: relative; }
}
.about .lede { font-size: 15px; }

/* The head of a listing page: the name and the one fact about the order,
   on one line. */
.list-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 14px; margin: var(--s4) 0 var(--s4); }
.list-head h1 { margin: 0; font-size: clamp(22px, 2.8vw, 32px); }

/* /favorites/ and /recent/: remove one game, or all of them after a question.
   The x sits in the thumbnail's BOTTOM corner, beside the title rather than
   over the artwork's top, where it covered the part of the picture that
   identifies the game. A 44px target around a 30px dark disc so it reads on
   any artwork. It is a sibling of the card's link, so the card needs to be
   the positioning box -- and the caption stops short of it, or a long title
   wraps underneath it. */
[data-library-grid] .card-game { position: relative; }
[data-library-grid] .card-game .info { inset-inline-end: 46px; }
.card-remove {
  position: absolute; bottom: 2px; inset-inline-end: 2px; z-index: 2;
  width: 44px; height: 44px;
  display: grid; place-items: center;
  padding: 0; border: 0; cursor: pointer;
  color: #fff;
  background: radial-gradient(circle, rgb(14 14 16 / 78%) 15px, transparent 16px);
  transition: color .12s ease, background .12s ease;
}
.card-remove:hover, .card-remove:focus-visible {
  color: var(--acc-ink);
  background: radial-gradient(circle, var(--acc) 15px, transparent 16px);
  outline: none;
}
/* The question that replaces Clear. The red is the only one on the site,
   kept for the one button that deletes. */
.clear-ask { display: inline-flex; align-items: center; flex-wrap: wrap; gap: var(--s2); align-self: center; }
.clear-ask[hidden] { display: none; }
.clear-q { color: var(--ink); font-size: 14px; font-weight: 600; margin-inline-end: var(--s1); }
.clear-yes { color: #fff; background: #d64545; border-color: #d64545; }
.clear-yes:hover { background: #e25656; border-color: #e25656; color: #fff; }
.list-meta { margin: 0; color: var(--dim); font-size: 13.5px; }
/* Two or three lines under the heading. Capped in width because a 400-char
   sentence run across a 1,200px desktop is one line nobody's eye can track
   back from. */
.list-intro { margin: calc(var(--s4) * -1) 0 var(--s5); max-width: 68ch; color: var(--dim); font-size: 14.5px; line-height: 1.6; text-wrap: pretty; }

/* A footnote under a grid: where a number came from, said once and quietly.
   /trending/ borrows GamePix's ranking rather than measuring this site's own
   players, and that is worth stating -- but above the games it would be a
   banner between somebody and the thing they came for, so it goes below them
   at the smallest size on the page. */
.list-note { margin: var(--s5) 0 0; max-width: 68ch; color: var(--dim); font-size: 13px; line-height: 1.6; text-wrap: pretty; }

/* --- the rail's Search button, and the sheet it opens -------------------
   Desktop has the header's field, so neither of these belongs there. Both are
   `display: none` here and switched on in the phone block at the end of this
   file; while the button still carries its `hidden` attribute the global
   [hidden] rule beats the media query, so it stays off until search.js has
   run and there is something for it to open. */
.rail-search { display: none; }
.search-sheet { display: none; }

/* --- a curated collection on /specials/ --------------------------------
   The heading and its reason travel together, above the grid, because the
   reason is the only thing distinguishing this block from /popular/ with a
   filter on. Separate them and the page becomes four unexplained grids. */
.special { margin: var(--s6) 0 0; }
.special-head { margin: 0 0 var(--s4); }
.special-head h2 { margin: 0 0 6px; font-size: clamp(19px, 2.4vw, 26px); }
.special-blurb { margin: 0; max-width: 62ch; color: var(--dim); font-size: 14.5px; line-height: 1.6; text-wrap: pretty; }
.list-note a { color: var(--acc); }

/* --- breadcrumb ---------------------------------------------------------- */
.crumbs { font-size: 14px; color: var(--dim); padding: var(--s4) 0 0; }
.crumbs ol { list-style: none; display: flex; flex-wrap: wrap; gap: var(--s2); margin: 0; padding: 0; }
.crumbs li + li::before { content: "/"; margin-inline-end: var(--s2); opacity: .5; }
.crumbs a { text-decoration: none; }
.crumbs a:hover { color: var(--ink); }

main { padding-bottom: var(--s6); }

/* Body copy on the site's own text pages -- /about/, /privacy/, /terms/, 404
   -- where a paragraph is a direct child of the content column.
   ------------------------------------------------------------------------
   Measured on /about/ at 1440px: every paragraph ran the full 1,194px column,
   about 170 characters a line. Typographic practice puts the comfortable
   maximum near 75; past roughly 90 the eye starts losing the return sweep and
   re-reads the line it just finished. `.lede` on those same pages was already
   capped at 62ch, so the page showed a readable intro above unreadable body
   copy and looked, correctly, like nobody had checked.

   The selector is deliberately narrow -- DIRECT children of .wrap. Everything
   else on the site already caps itself where it should (.prose at 68ch,
   .hero-desc at 46ch, .list-intro at 68ch), and a blanket `main p` would also
   catch the hero and the card captions, which are not body copy and are not
   measured in characters. */
main > .wrap > p,
main > .wrap > ul:not(.grid):not(.tile-grid),
main > .wrap > ol { max-width: 68ch; }
/* :not(.grid) is load-bearing, not tidiness. The game grid and the category
   grid are <ul class="grid"> and they ARE direct children of .wrap, so the
   first version of this rule capped them at 68ch: measured, the grid on /new/
   went from 1,194px to 623px and from six cards a row to three, with half the
   page empty. Every guard passed -- the markup is valid, the links resolve,
   the SEO is intact. It was only visible by measuring the rendered grid.
   It happened again to .tile-grid (/categories/, /tags/): three tiles on
   the left half of the page. Any grid that is a direct child joins this list. */

/* One rule, not one per component. `!important` on the universal selector
   beats every transition in the file, including ones added later by somebody
   who never reads this block -- which is the only way a motion preference
   stays honoured as a stylesheet grows. A per-component copy of it was here
   for the drawer and did nothing this did not already do. */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* --- prose and fact tables on a game page ------------------------------- */
.prose { max-width: 68ch; }
.prose h2 { font-size: 20px; margin: var(--s5) 0 var(--s3); }
.prose p { margin: 0 0 var(--s4); }
.prose h3 { font-size: 17px; margin: var(--s5) 0 var(--s2); }
.prose ul, .prose ol { margin: 0 0 var(--s4); padding-inline-start: 1.4em; }
.prose li { margin: 0 0 var(--s2); }
.prose li::marker { color: var(--acc); }
.prose a { color: var(--acc); }

.facts, .faq { max-width: 68ch; margin: 0 0 var(--s5); }
.facts dt, .faq dt { color: var(--dim); font-size: 13px; margin-top: var(--s3); }
.facts dd, .faq dd { margin: 0; }
.faq dt { color: var(--ink); font-weight: 650; font-size: 15px; }
.faq dd { color: var(--dim); margin-bottom: var(--s3); }

/* Load more.
   ------------------------------------------------------------------------
   Borrows the sortbar pill's shape and type so the button reads as part of
   the same family, but fills rather than outlines: this one does something
   to the page rather than navigating away from it, and the design uses the
   accent fill for exactly that distinction (see .see-all on hover).

   It renders hidden and public/j/more.js reveals it. A visitor with no
   JavaScript sees the 120 games the page already carries and no button --
   a complete page rather than a control that cannot work. */
.more { display: flex; justify-content: center; margin: clamp(20px, 3vw, 32px) 0 8px; }
.more-button {
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--panel-2); color: var(--ink);
  padding: 13px 28px;
  font: inherit; font-weight: 800; font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase;
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease, border-color 160ms ease;
}
.more-button:hover:not(:disabled) { background: var(--acc); color: var(--acc-ink); border-color: var(--acc); }
.more-button:focus-visible { outline: 2px solid var(--acc); outline-offset: 3px; }
/* Disabled only while a fetch is in flight. The label says what is happening,
   so the state does not rely on colour alone. */
.more-button:disabled { opacity: 0.55; cursor: default; }

/* The order of a shelf, at the end of its title row (web_helper.php
   shelf_sort(); the owner, 2026-09-29 -- the Popular | New pill that stood
   above the grid read as clutter). A 44px round button shows the icon of
   the order on screen, the flame of Popular or the sparkle of New, and opens
   the two choices: a small menu under it here, a sheet from the bottom on a
   phone (just below). [hidden] is restated because an author's display rule
   beats the browser's. */
.list-head .sort { margin-inline-start: auto; align-self: center; }
.sort { position: relative; display: flex; }
.sort[hidden], .sort-menu[hidden], .sort-backdrop[hidden], .sort-now[hidden] { display: none; }
/* A bare icon and caret (the owner: the first, bordered pill was too big and
   made the title row taller). The 44px target stays, reaching 10px into the
   margins above and below instead of pushing the intro down. */
.sort-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 3px;
  min-height: 44px; min-width: 44px; padding: 0 8px;
  margin-block: -10px;
  border: 0; border-radius: 999px;
  background: none; color: var(--ink);
  cursor: pointer;
}
.sort-btn:hover, .sort-btn[aria-expanded="true"] { background: var(--panel); }
.sort-btn:focus-visible, .sort-opt:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.sort-now, .sort-btn svg { display: block; }
.sort-caret { color: var(--dim); }
.sort-menu {
  position: absolute; inset-inline-end: 0; top: calc(100% + 6px); z-index: 40;
  min-width: 200px; padding: 6px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.55);
}
.sort-title { display: none; }
.sort-opt {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  width: 100%; min-height: 48px; padding: 0 14px;
  border: 0; border-radius: 10px;
  background: none; color: var(--ink);
  font: inherit; font-size: 15px; font-weight: 700; text-align: start;
  cursor: pointer;
}
.sort-opt:hover { background: color-mix(in srgb, var(--ink) 7%, transparent); }
.sort-opt[aria-checked="true"] { color: var(--acc); }
.sort-opt[aria-checked="false"] .sort-check { visibility: hidden; }
@media (max-width: 780px) {
  .sort-backdrop { position: fixed; inset: 0; z-index: 90; background: rgba(0, 0, 0, 0.6); }
  .sort-menu {
    position: fixed; left: 0; right: 0; top: auto; bottom: 0; z-index: 91;
    min-width: 0; padding: 10px 12px calc(22px + env(safe-area-inset-bottom));
    border-width: 1px 0 0; border-radius: 18px 18px 0 0; box-shadow: none;
  }
  .sort-menu::before { content: ""; display: block; width: 40px; height: 4px; margin: 0 auto 14px; border-radius: 2px; background: var(--line); }
  .sort-title { display: block; margin: 0 14px 6px; font-weight: 800; font-size: 18px; color: var(--ink); }
  .sort-opt { min-height: 52px; font-size: 16px; }
}

/* --- mobile menu drawer ---------------------------------------------------
   Measured before this existed: on a 390px phone the shell stacked three rows
   above the first game -- the nav bar, then the brand, then a search field
   that does not work yet -- for about 230px of chrome on a site whose product
   is a grid of thumbnails. Now it is one row of about 62px.

   WHAT WENT AND WHAT STAYED, because the difference is the whole design.
   The four links stayed in the bar: they are the site's spine and they were
   already one tap away. What went was everything that was not navigation --
   the two inert circles standing in for a leaderboard and a profile this site
   does not have, the brand repeated under a bar that already contains Home,
   and a search box whose placeholder says "coming soon". The button occupies
   the circles' place, and the drawer it opens carries what the bar never had
   room for: every category, and the site pages otherwise buried in the
   footer.

   State lives on .shell as data-nav, set by public/j/nav.js. Absent it, none
   of the rules keyed on it apply -- so with no JavaScript the drawer is not a
   panel stuck off-screen, it is simply not there, and the bar of four links
   is the navigation, working. */
.nav-backdrop {
  position: fixed; inset: 0; z-index: 45;
  background: rgba(8, 8, 10, 0.62);
  backdrop-filter: blur(2px);
}

.drawer {
  position: fixed; top: 0; bottom: 0; inset-inline-start: 0; z-index: 50;
  /* Full width on a phone. A 320px panel with 70px of dimmed page beside it
     is a panel apologising for being open; at 22 rows it also had to scroll
     in a column narrower than it needed. The cap keeps it a panel rather
     than a whole tablet screen. */
  /* Full width on a phone, always. The cap exists for tablets, where a panel
     spanning 1,024px would be a page rather than a menu -- and it is applied
     only there, because on a 430pt phone a 400px cap left a strip of the page
     showing beside it, which reads as a drawer that failed to open. */
  width: 100%;
  height: 100vh; height: 100dvh;
  display: flex; flex-direction: column;
  background: var(--panel);
  border-inline-end: 1px solid var(--line);
  /* Off-screen AND inert: a translated panel is still focusable, so a
     keyboard tab would walk into a drawer nobody opened. */
  transform: translateX(-100%);
  visibility: hidden;
  transition: transform 220ms cubic-bezier(.2, .8, .3, 1), visibility 0s linear 220ms;
}
.shell[data-nav='open'] .drawer {
  transform: translateX(0);
  visibility: visible;
  transition: transform 220ms cubic-bezier(.2, .8, .3, 1), visibility 0s;
}

/* It only holds focus so the keyboard lands inside; it is not itself a
   control, so it does not draw a ring. */
.drawer:focus { outline: none; }
.drawer-head {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 10px;
  padding: 14px 12px 14px 16px;
  border-bottom: 1px solid var(--line);
}
/* Both of these are the first things a thumb meets after opening the menu,
   and both measured under 44px: the brand 145x26, the close button 40x40. */
.drawer-brand { display: flex; align-items: center; gap: 10px; min-height: 44px; color: var(--ink); text-decoration: none; margin-inline-end: auto; }
.drawer-brand:hover { text-decoration: none; }
.drawer-brand .brand-mark { display: grid; place-items: center; }
.drawer-brand .brand-name { font-weight: 800; font-size: 17px; letter-spacing: -0.03em; }
.drawer-close {
  flex: none; width: 44px; height: 44px;
  display: grid; place-items: center;
  border: 0; border-radius: 12px;
  background: var(--panel-2); color: var(--dim);
  cursor: pointer;
}
.drawer-close:hover { color: var(--ink); }

/* The account row, at the END of the scrolling list, under Language. It used
   to be pinned between the head and the links, where it never scrolled away.
   Ruled off above now: it closes the list rather than opening it. */
.drawer-account { margin-top: var(--s5); padding: var(--s4) 11px 0; border-top: 1px solid var(--line); }
.drawer-acct { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; min-width: 0; }
/* THE AVATAR IS SOMEBODY ELSE'S PICTURE, SO THE FRAME DOES THE WORK.
   auth.date generates it and we never see it before it loads: it can be
   square, it can be transparent, it can be a pixel-art face on a coloured
   tile. Three things make any of those look deliberate rather than pasted in:

   `object-fit: cover` -- without it a square SVG in a round box is stretched
   or letterboxed, and the corners of the art get cut mid-face, which is what
   was happening.

   A background, because a transparent SVG over a dark panel is a hole.

   A ring with a gap, drawn as two box-shadows: the first is the page colour
   and the second the accent, so the accent never touches the picture and does
   not tint whatever colour the avatar happens to be. That gap is the whole
   difference between "an avatar" and "an image that ended up here". */
.drawer-avatar,
.rail-avatar {
  flex: none;
  display: block;
  width: 40px; height: 40px;
  border-radius: 999px;
  background: var(--panel-2);
  object-fit: cover;
  box-shadow:
    0 0 0 2px var(--panel),
    0 0 0 3px color-mix(in srgb, var(--acc) 50%, transparent);
}
.rail-avatar { width: 30px; height: 30px; }
.drawer-acct-name {
  font-weight: 700; font-size: 14px; color: var(--ink);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  margin-inline-end: auto;
}
.drawer-acct-link {
  border: 0; background: transparent; color: var(--dim);
  font: inherit; font-size: 11px; font-weight: 800;
  letter-spacing: 0.08em; text-transform: uppercase;
  padding: 10px 6px; min-height: 44px; display: inline-flex; align-items: center;
  cursor: pointer; text-decoration: none;
}
.drawer-acct-link:hover { color: var(--ink); text-decoration: none; }
.drawer-signin {
  display: inline-flex; align-items: center; gap: 9px;
  min-height: 44px; padding: 0 20px;
  border: 0; border-radius: 999px;
  background: var(--acc); color: var(--acc-ink);
  font: inherit; font-weight: 800; font-size: 13px;
  cursor: pointer;
}
.drawer-signin:hover { filter: brightness(1.08); }
.drawer-acct-note { font-size: 12px; color: var(--dim); min-width: 0; }

.drawer-body { flex: 1 1 auto; overflow-y: auto; padding: 10px 12px 28px; overscroll-behavior: contain; }
/* THE DRAWER IS READ AT ARM'S LENGTH, ONE-HANDED. It is the only navigation
   a phone gets, so its type is sized for that rather than inherited from the
   desktop sidebar it grew out of: the links were 14px, which is a size for a
   dense column beside content, not for the full width of a screen. */
.drawer-title {
  display: block;
  padding: 18px 10px 8px;
  font-size: 11px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--dim);
}
.drawer-title:first-child { padding-top: 6px; }
.drawer-link {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 12px; border-radius: 12px;
  color: var(--ink); font-size: 15.5px; font-weight: 650;
  text-decoration: none;
}
.drawer-link:hover { background: var(--panel-2); color: var(--ink); text-decoration: none; }
.drawer-link[aria-current='page'] { background: var(--acc); color: var(--acc-ink); }
.drawer-icon {
  /* No tile behind the glyph. It sat in a 34px grey square, and with the
     icons solid the square was a second shape around the first -- a box of
     boxes down the whole drawer. The glyph is larger instead (26px, in the
     same 34px column so the names still line up). */
  flex: none; width: 34px; height: 34px;
  display: grid; place-items: center;
  /* The category's own colour when the markup supplies one, the house accent
     when it does not -- a nav item with no category still looks like the rest
     of the rail. */
  color: var(--cat, var(--acc));
}
.drawer-icon svg { width: 26px; height: 26px; }
.drawer-link[aria-current='page'] .drawer-icon { color: var(--acc-ink); }
/* THE COUNTS, AS A COLUMN RATHER THAN A RAGGED EDGE.
   They were plain grey numerals pushed right, so "2,528" and "486" ended at
   the same place and began at different ones -- five ragged left edges down
   the list, each one reading as a stray number rather than as the same fact
   about each row.

   A fixed minimum width plus tabular figures lines the digits up (the font's
   default proportional figures make a 1 narrower than a 0, so even equal
   digit counts drift), and the chip says these belong to the row they sit on
   rather than floating beside it. */
.drawer-count {
  margin-inline-start: auto; flex: none;
  min-width: 52px; padding: 3px 9px;
  border-radius: 999px;
  background: var(--panel-2); color: var(--dim);
  font-size: 12px; font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
/* The chip must not vanish into the surface that arrives under it. */
.drawer-link:hover .drawer-count { background: var(--ground); color: var(--ink); }
.drawer-link[aria-current='page'] .drawer-count { background: rgba(14, 14, 16, .16); color: var(--acc-ink); }

@media (max-width: 780px) {
  /* The whole header row goes. Brand, search and game count: an identity the
     bar below already links to, a control that does nothing yet, and a number
     nobody came for. The brand is at the top of the drawer and in the
     footer. */
  .col > header { display: none; }


  .rail { padding: 6px 8px; gap: 6px; }
  .rail-menu {
    flex: 1 1 0; min-width: 0;
    display: flex; flex-direction: column; align-items: center; gap: 5px;
    margin-inline-start: 4px; padding: 8px 10px;
    border: 0; border-inline-start: 1px solid var(--line); border-radius: 0;
    background: transparent; color: var(--dim);
    font: inherit; cursor: pointer;
  }
  .rail-menu span { font-size: 10px; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; }
  .rail-menu:hover, .rail-menu[aria-expanded='true'] { color: var(--ink); }
}

/* Above a phone it is a panel, not the whole screen -- at any width from a
   tablet up, including a desktop, where the button that opens it is now
   visible too. */
@media (min-width: 641px) {
  .drawer { max-width: 400px; }
}

/* This used to be `@media (min-width: 781px) { .drawer, .nav-backdrop
   { display: none } }`, which was correct while the rail carried everything
   the drawer did. It does not: the categories, the tag directory and the
   language picker live only in here, so hiding it above 780px was hiding the
   only copy of them from every desktop visitor. */


/* --- the phone grid ------------------------------------------------------
   Two across on a phone. Not three: at three the tile is about 110px on a
   390px screen, and the title -- which sits on the picture -- has to drop to
   9px and wrap to two lines before it fits. Two tiles give the artwork room
   to be recognised, which is the only thing a card is for.

   Two FIXED columns rather than a smaller auto-fill minimum, because
   auto-fill answers "how many fit" and the answer wanted here is "two".

   520px, NOT 640. It was 640, and that number was chosen while looking at a
   390px phone -- where two is right. Measured across the range afterwards,
   640 was the one width it was wrong at: two cards of 295px, so a 640px
   screen showed TWO games under the hero while 720 showed three of 218px.
   The same rule that keeps art recognisable on a phone was making it
   enormous on a small tablet, and the step from 640 to 720 went 2 -> 3 with
   a 77px jump in tile size.

   Below 520 nothing changes: at 390, 480 and 560 auto-fill also answers
   "two", so the fixed rule and the flexible one now agree everywhere they
   overlap and differ only at the width where two was wrong.

   This block is LAST in the file on purpose: a media query adds no
   specificity, so while it sat above `.grid` the plain rule further down won
   and the breakpoint did nothing at all. */
@media (max-width: 520px) {
  .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 11px; }
  .card-game .info { left: 9px; right: 9px; bottom: 7px; }
}


/* THE SHARE CHIPS GROW BACK ON A PHONE.
   24px is right on a desktop, where the pointer is a pixel. It is below the
   44px that a thumb needs, and the footer is not the place to make somebody
   aim. Desktop gets the small chip it was asked for; the phone gets a target
   it can hit. */
/* 780, not 640. The chips were sized back up at the width the GRID changes,
   which is a layout number and has nothing to do with what is pointing at the
   screen. Measured at 768px -- an iPad held upright, the most common tablet
   width there is -- all six were 24x24 against the 44px a finger needs. The
   right boundary is the one the rest of this file already uses for "a finger
   is the pointer here". */
@media (max-width: 780px) {
  /* A BIGGER GLYPH, AND IT TAKES THREE RULES TO GET ONE.
     The widget ships two sizes, 16 and 24, and 24 was already the larger --
     but `ats_kit_size_24` draws a 19px SVG in a 24px box, so the thing on
     screen was 19px. Sizing the <span> did nothing: measured, the height
     took and the width stayed 24, and no rule anywhere sets that width.
     Sizing the SVG works, but the BUTTON then stayed 40px because the widget
     fixes .ats_btn at 24px too, and the glyph simply overflowed it.
     So all three: the svg, the span that holds it, and the box around both.
     28 + 2x8 = the 44px target, verified rather than assumed. */
  .foot-share .ats_btn .ats_icon svg { width: 28px !important; height: 28px !important; }
  .foot-share .ats_btn .ats_icon { width: auto !important; height: auto !important; display: flex !important; }
  .foot-share .ats_btn { width: 28px !important; height: 28px !important; padding: 8px !important; border-radius: 12px !important; }
  /* 12px, not 2. The 2 came from a round where the row was ~380px wide and
     the fix was to pull it in; the chips were 40px with the widget's own 8px
     gap on top. They are 44px here and the row is centred in its own full
     width, so 2px left them touching -- six controls reading as one bar.
     Measured: 6x44 + 5x12 = 324px inside 354px, so they separate and still
     sit on one line at 390px. */
  .foot-share { gap: 14px !important; }
}

/* THE HERO DESCRIPTION GOES WHEN THE HERO STACKS -- NOT BEFORE.
   This was `max-width: 780px`, sitting in the phone block with a comment about
   a 390px screen. Measured, the hero's two columns (`flex: 1 1 320px` each)
   stay side by side down to a 680px container and stack at 670 -- which is a
   viewport of roughly 710px once the page's own padding is taken off.

   So between about 710px and 780px the hero was still two columns with the
   description already gone: a picture on the right, a title on the left, and
   a column of empty black underneath it where the paragraph used to be. That
   band is an ordinary desktop window with devtools open, which is exactly
   where it was found.

   700px errs on the side of showing it. A stacked hero WITH a paragraph is
   merely taller; a two-column hero without one has a hole in it. */
@media (max-width: 480px) {
  /* ON A PHONE THE BUTTON BECOMES ITS ARROW, AND THE HEADING KEEPS THE ROW.
     The row holds a heading and one link onward, and below 480px there is not
     enough width for both to be spelled out. Three arrangements were measured
     before this one:

       - letting the row wrap put the button alone on a line under the
         heading, where it reads as an orphan rather than as the way onward;
       - letting the heading wrap inside the row broke it into three cramped
         lines at 390px;
       - clipping the heading to one line worked in English and failed
         everywhere else. Measured across all twenty languages: the button
         label alone took 188px of the 354 available in Polish ("Zobacz
         wszystkie"), 166 in Dutch, 161 in Filipino -- and nine languages were
         left showing under 57% of their own heading. "Играйте в бес..." is
         not a heading, it is the start of one.

     So the words that can be dropped are dropped. An arrow beside a section
     heading already means "more of this", the link keeps its 44px target, and
     its text stays in the accessibility tree -- clipped, not display:none --
     so it is still announced as "See all". The heading gets the whole row
     back, and ends in an ellipsis only in the languages that still need it.

     Scoped to the three headers that HAVE an arrow: /favorites/ and /recent/
     use .see-all for a Clear button, which is a word and nothing else, and
     would be clipped to an empty circle by a blanket rule. */
  .grid-head .see-all,
  .rail-head .see-all,
  .hero-head .see-all {
    width: 44px; min-width: 44px; padding: 0; gap: 0; justify-content: center;
    /* Filled, not outlined. With the words gone, a 1px ring round an arrow
       read as an empty circle -- a placeholder, not a button. */
    background: var(--panel-2); border-color: transparent; color: var(--ink);
  }
  .grid-head { margin-bottom: var(--s5); }
  .grid-head .see-all > span,
  .rail-head .see-all > span,
  .hero-head .see-all > span {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; white-space: nowrap; clip-path: inset(50%);
  }

  /* TWO PIXELS OFF THE HEADING, WHICH BUYS FIVE LANGUAGES A WHOLE ONE.
     At 26px the row fitted eleven languages and cut five of the rest by one
     character each -- "Бесплатные онлайн-игр..." is a heading that looks
     broken rather than abbreviated, and one character is a poor thing to lose
     it over. At 24px, which is an ordinary size for a heading on a phone,
     seventeen fit whole; the three that still did not were long for the usual
     reason and lost their verb like the others. */
  .grid-title { font-size: 24px; }

  /* One row, and the heading ends in an ellipsis if it must. */
  .grid-head, .rail-head { flex-wrap: nowrap; }
  .grid-title,
  .rail-head h2 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}

@media (max-width: 700px) {
  .hero-desc { display: none; }

  /* THE STACKED HERO'S VERTICAL RHYTHM, FROM ONE SOURCE EACH.
     Three separate things were spacing this column -- the slide's own padding,
     the flex gap, and the margins on the head and the dots -- and they added
     up to 48px above the title and 44px below the artwork while the intrinsic
     gaps underneath were 8px and 8px. Nobody chose 48 or 44; they were what
     three unrelated numbers happened to sum to.
     Measured with every source zeroed first, then one value added back per
     gap: 16 / 16 / 20 / 16, and 32 to the next section. Every number is a step
     on the 4px scale, which is what "even" means here. Desktop keeps its own
     padding: there the hero is two columns and none of these gaps exist. */
  .hero { padding: 0; gap: var(--s1); }
  .hero-head { margin: var(--s4) 0 var(--s2); }
  /* The dots' margin is NOT set here. .hero-dots is declared further down this
     file, and a media query adds no specificity -- the later rule wins and this
     one would do nothing, which is how the gap measured 20px while this block
     claimed 16. It is set once, at the declaration. */
}

/* --- search ---------------------------------------------------------------
   Two fields in every page's markup, one script, one downloaded index. The
   header carries one and the drawer carries the other; which is on screen is
   a width question, so CSS answers it rather than JavaScript. */

/* Read by a screen reader, never drawn. The icon beside each field is
   decorative and carries aria-hidden, so without this the input would have no
   accessible name at all. */
.vh {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.search { position: relative; min-width: 0; }

/* The desktop field, in the band between the brand and the game count -- the
   width that was sitting empty. It goes with the header below 780px, where
   the drawer's field takes over. */
.head-search {
  flex: 1 1 260px; max-width: 460px;
  /* Centred in the bar rather than stretched by it: the header is
     `align-items: stretch`, which made the form 72px tall around a 44px
     field. */
  align-self: center;
}

/* The phone's, first thing in the drawer and full width. */
.drawer-search { margin: 4px 2px 10px; }

.search-field {
  display: flex; align-items: center; gap: 9px;
  padding: 0 14px;
  /* 44px, the same target every other control here holds. A search box is the
     one control somebody uses while walking. */
  min-height: 44px;
  border: 1px solid var(--line);
  /* A BOX, NOT A PILL. Every button on this site is a 999px capsule, and the
     field was wearing the same shape -- so the one control you type into
     looked like the several you press. 12px keeps it in the family and still
     says "this is a field". */
  border-radius: 12px;
  background: var(--panel);
  transition: border-color .14s ease, background-color .14s ease;
}
.search-field:focus-within { border-color: var(--acc); background: var(--panel-2); }
.search-icon { flex: none; display: grid; place-items: center; color: var(--dim); }
.search-field:focus-within .search-icon { color: var(--acc); }

.search-field input {
  flex: 1 1 auto; min-width: 0;
  border: 0; background: none; outline: none;
  color: var(--ink); font: inherit; font-size: 14px;
  /* 16px on a phone, and not for looks: Safari zooms the whole page when a
     focused input's text is under 16px, and the page never zooms back. The
     drawer rule below raises it. */
}
.search-field input::placeholder { color: var(--dim); }
/* The browser's own clear button is a grey X that does not match anything
   here, and Escape already clears the field. */
.search-field input::-webkit-search-cancel-button { display: none; }

/* --- the dropdown ------------------------------------------------------ */
.search-results {
  position: absolute; z-index: 40;
  top: calc(100% + 6px); left: 0; right: 0;
  max-height: min(60vh, 380px); overflow-y: auto; overscroll-behavior: contain;
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--panel);
  box-shadow: 0 18px 40px rgba(0, 0, 0, .45);
  padding: 6px;
}
.search-list { list-style: none; margin: 0; padding: 0; }
.search-opt > a {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 10px;
  border-radius: 10px;
  color: var(--ink); font-size: 14px; font-weight: 600;
  text-decoration: none;
}
/* THE PICTURE IS THE POINT OF A SUGGESTION LIST.
   Nine games in this catalogue are called some arrangement of "Bubble
   Shooter" and the titles alone do not tell them apart. The art does, at a
   glance, which is the whole reason a dropdown beats a page of results.
   40px because it is a thumbnail, not a card -- and `object-fit: cover`
   because the source is square but a few are not. */
.search-thumb {
  flex: none;
  width: 40px; height: 40px;
  border-radius: 8px;
  object-fit: cover;
  background: var(--panel-2);
}
/* One line. A wrapped title moves every row below it as you arrow down. */
.search-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* One class for hover and for the keyboard, so arrowing down looks the same
   as pointing at it -- two different highlights for one selection is two
   things that can disagree about which row is chosen. */
.search-opt.is-active > a,
.search-opt > a:hover { background: var(--acc); color: var(--acc-ink); text-decoration: none; }
.search-empty { margin: 0; padding: 12px; color: var(--dim); font-size: 13.5px; }

/* --- the /search/ page -------------------------------------------------- */
.search-page-form { max-width: 560px; margin: 0 0 var(--s5); }
.search-page-form .search-field { min-height: 52px; padding: 0 18px; }
.search-page-form .search-field input { font-size: 16px; }

.search-fallback { margin: var(--s6) 0 0; }
.search-fallback h2 { font-size: 15px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--dim); margin: 0 0 var(--s3); }
.tag-row { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; max-width: none; }
.tag-count { color: var(--dim); font-size: 11px; font-variant-numeric: tabular-nums; margin-inline-start: 4px; }

/* 16px in the drawer's field, because Safari zooms the page when a focused
   input is smaller and never zooms back -- and the drawer is the phone's only
   navigation, so it is exactly where that happens. */
@media (max-width: 780px) {
  .search-field input { font-size: 16px; }
}

/* THE PHONE'S SEARCH, AS A SECOND ROW OF THE RAIL.
   `flex-wrap` on the rail and `flex: 0 0 100%` on the sheet: open, it wraps
   to its own full-width line under the five buttons; closed, it is display:
   none and takes no space at all. No overlay, no absolute positioning, and
   nothing covering the first two games while somebody types.

   The rail is already `position: sticky; top: 0` here, so the sheet stays on
   screen while the results scroll under it. */
@media (max-width: 780px) {
  .rail { flex-wrap: wrap; }
  .rail-search { display: flex; }
  .search-sheet { display: block; flex: 0 0 100%; padding: 2px 8px 8px; }
  /* The dropdown belongs to the sheet's own width, not the form's line box. */
  .sheet-search .search-results { top: calc(100% + 4px); }
}

/* TARGETS THAT WERE READABLE BUT NOT HITTABLE.
   Three controls measured under 44px on every touch width: the tag pills
   (26px), the "Read more" link in the description panel (19px) and the footer
   brand (26px). All three are destinations, not links inside a sentence, so
   the inline exception in WCAG 2.5.8 does not cover them.

   min-height with centred content rather than more padding: padding would
   move the text and change the rhythm of the rows they sit in, and the point
   is a bigger target, not a bigger pill. */
@media (max-width: 780px) {
  .tag {
    display: inline-flex; align-items: center;
    min-height: 44px; padding: 0 14px;
  }
  .game-desc-more { min-height: 44px; }
  .foot-brand { min-height: 44px; }
}

/* THE LAST TWO PIXELS, AND WHY THEY ARE DOWN HERE.
   Six equal columns left each rail button at 41px on a 320px screen, against
   the 44 a finger needs. The width is spent on padding and gaps rather than on
   the buttons, so this takes it back.

   IT WAS WRITTEN IN THE 420px BLOCK FIRST AND DID ALMOST NOTHING -- 41px
   became 42. A media query adds no specificity, and there is a SECOND
   `max-width: 780px` block further down this file that sets
   `.rail { padding: 6px 8px; gap: 6px }`. Later wins, so the wider rule beat
   the narrower one and the measurement barely moved.

   That is the same trap that once kept phones on six grid columns, and it is
   written up in wiki/entities/viewports-kiem-giao-dien.md. Layout breakpoints
   belong at the end of this file. This one now is. */
@media (max-width: 420px) {
  .rail { padding: 6px 4px; gap: 2px; }
  .rail-menu { margin-inline-start: 0; padding: 8px 4px; }

  /* SIX MORE PIXELS FOR THE LABEL, BOUGHT FROM THE GAPS AROUND IT.
     Measured at 360px across all twenty languages: the label had 67px, and
     five languages did not fit in it -- Dutch "Categorieën" by 4px, Turkish
     "Kategoriler" by 3, Russian "Популярные" by 2. Each was being clipped to
     an ellipsis, which is tidy and still wrong: a navigation label that reads
     "CATEGORIEË..." is a word the reader has to finish themselves.

     The three pixels of gap and the four of padding were doing nothing a
     reader could see -- the items are already separated by their own width --
     so they go to the text. 67px becomes 73px, and the three fit. The two
     that were 18px over are fixed in the dictionaries instead, because no
     amount of gap saves a two-word label in a one-word slot. */
  .rail-scroll { gap: 0; }
  .rail-item { padding-inline: 0; }
  .rail-item span { letter-spacing: 0; }
}

/* THE FOOTER CENTRES ITSELF ON A PHONE.
   On a desktop .foot-top is one row -- brand hard left, links beside it,
   share pinned right by `margin-left: auto` -- and each part has a reason to
   be where it is. On a phone that row wraps into three stacked lines, and
   `margin-left: auto` on a wrapped line pushes the share row to the right
   edge on a line of its own. The result was a left brand, left links and a
   right-ish share row: three different alignments down one narrow column.

   Centred, the three read as one block. The brand is 20px with a 26px mark
   and the links 16px: it was 15px and 13px, a sign-off so small it read as
   fine print, and the owner asked for it larger. */
@media (max-width: 780px) {
  .foot-top { justify-content: center; text-align: center; }
  .foot-brand { justify-content: center; }
  .foot-brand .brand-name { font-size: 20px; }
  .foot-brand svg { width: 26px; height: 26px; }
  .foot-links a { font-size: 16px; }

  /* Full width so `justify-content` has room to centre within, rather than
     centring a shrink-wrapped box that is already at one edge. */
  .foot-links { flex: 0 0 100%; justify-content: center; }
  .foot-share { flex: 0 0 100%; justify-content: center; margin-inline-start: 0; }

  .foot-blurb { text-align: center; margin-left: auto; margin-right: auto; }

  /* The bar underneath follows, or the page ends on two centred lines above
     two left-aligned ones. */
  .foot-bar { justify-content: center; text-align: center; }
  .foot-copy { margin-inline-end: 0; }
}

/* WIDE SCREENS GET THE WIDTH. MEASURED AT 1920:
     wrap 1280px, 258px dead on the left and 274px on the right,
     six cards of 182px, and 977px of empty header between the search
     field and the game count.
   The container was capped at 1280px, which is right for PROSE and wrong for
   a wall of pictures -- and the prose is capped separately anyway, by
   `main > .wrap > p { max-width: 68ch }`, so raising this cannot widen a
   paragraph.

   190px as the grid minimum, chosen by trying three: 168 gives eight cards of
   172px, 200 gives six of 235px, and 190 gives SEVEN of 199px -- the most
   games on screen without the art getting small. auto-fill only accepts whole
   columns, so these are steps rather than a curve, and the middle step is the
   one worth having.

   Starts at 1500px so 1440 -- the commonest desktop here -- is untouched.
   Verified at 1440 (unchanged), 1600 (fills, six cards of 215px) and 1920
   (seven of 199px, dead space 532px down to 212px).

   Last in the file, like every other layout breakpoint, because a media query
   adds no specificity and a later plain rule would beat it.

   The header bar is NOT capped: its background and bottom line run the full
   width, and padding puts what is in it over the centred 1600px column. It
   was capped at 1600px too, but without the column's centring, so past
   ~1700px logo, search and icons sat at the left while the games moved to the
   middle -- an empty strip over the grid's right half (the owner's desktop,
   2026-09-29). */
@media (min-width: 1500px) {
  main > .wrap { max-width: 1600px; }
  .col > header { padding-inline: max(0px, calc((100% - 1600px) / 2)); }
  .head-search { max-width: 640px; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
}

/* --- the rail's desktop-only half ----------------------------------------
   Trending, Specials, Recently played, Favourites and the account block are
   on the rail for a pointer and reached through MENU on a phone, where the
   rail is a six-column horizontal bar with no room for eleven things.

   Written as a `display: none` inside the phone breakpoint rather than a
   `display: flex` inside a desktop one, so the default -- what a browser with
   no media query support would render -- is everything visible rather than a
   navigation with four items. */
.rail-account { padding: 0 11px 10px; }
.rail-acct { width: 100%; font: inherit; border: 0; background: transparent; cursor: pointer; }

@media (max-width: 780px) {
  .rail-desk { display: none !important; }
}

/* --- the written pages: about, privacy, terms -----------------------------

   These three were the only pages on the site still rendering as a bare
   column of tags. Every other page earns its shape from the catalogue -- a
   grid of art, a player, a rail -- and these have nothing but sentences, so
   the shape has to come from the typography and the panels instead.

   Nothing new is invented here. The panel is the card's background, border
   and radius; the rhythm is the same 4px scale; the one accent is the same
   --acc already on the Play button. A second visual language for three pages
   would be a worse outcome than the bare column. */

.page-head {
  padding: var(--s3) 0 var(--s5);
  margin: 0 0 var(--s5);
  border-bottom: 1px solid var(--line);
}
.page-head h1 { margin: 0 0 var(--s3); }
.page-head .lede { margin: 0; font-size: 17px; max-width: 58ch; }

/* Stretch, not start. On the privacy page the two panels are a pair of equal
   parties, and two panels of visibly different height say the opposite of
   that before a word is read. */
.doc-grid { display: grid; gap: var(--s4); align-items: stretch; }

.doc-panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: clamp(18px, 2.6vw, 28px);
}
/* The panel supplies the outer spacing, so the first and last child must not
   add their own -- otherwise the text sits low and leaves a gap at the foot. */
.doc-panel > :first-child { margin-top: 0; }
.doc-panel > :last-child { margin-bottom: 0; }
.doc-panel h2 { font-size: 19px; margin: 0 0 var(--s3); }
.doc-panel p { margin: 0 0 var(--s4); max-width: 64ch; }

/* The one mark of colour. A rule rather than a bullet: a dot beside a heading
   reads as a list item that lost its list, while a short bar reads as a
   heading's underline and stops competing with the words. */
.doc-panel h2::after {
  content: '';
  display: block;
  width: 26px;
  height: 2px;
  margin-top: var(--s2);
  border-radius: 2px;
  background: var(--acc);
}

/* Facts pulled out of the prose, where a number is easier to find than to
   read past. */
.doc-facts { display: flex; flex-wrap: wrap; gap: var(--s4); margin: var(--s4) 0 0; padding: 0; }
.doc-fact { list-style: none; }
.doc-fact dt,
.doc-fact .k {
  font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--dim); margin: 0 0 var(--s1);
}
.doc-fact dd,
.doc-fact .v { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -0.01em; }

/* Two panels side by side only where two panels are genuinely a pair -- the
   privacy page, whose entire point is that the two parties are separate. One
   column everywhere else, and one column on a phone, where side-by-side prose
   is two narrow gutters of text. */
@media (min-width: 900px) {
  .doc-grid.is-pair { grid-template-columns: 1fr 1fr; }
}

/* --- the trending rail's swipe indicator ---------------------------------

   One dot per slide, not a fixed three: a count that disagrees with the number
   of slides tells the reader something untrue about how much is there. */
.hero-dots {
  display: flex;
  justify-content: center;
  gap: var(--s2);
  /* 8px, plus the 8px the artwork's own box already leaves: 16 to the dots,
     matching the 16 above the title. */
  margin-top: var(--s2);
  /* Reported, never operated -- see the note beside the markup. */
  pointer-events: none;
}
.hero-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ink);
  opacity: 0.26;
  transition: opacity 160ms ease, transform 160ms ease;
}
.hero-dot.is-on { opacity: 1; transform: scale(1.2); }

@media (prefers-reduced-motion: reduce) {
  .hero-dot { transition: none; }
}

/* --- the account menu in the rail ----------------------------------------

   The rail is 86px wide and its overflow is visible, so this opens sideways
   into the page rather than down the rail. Anchored to the BOTTOM of the
   button and growing upward, because the account block sits at the foot of a
   full-height rail and a menu growing down would leave the viewport. */
.acct { position: relative; display: flex; justify-content: center; }

.acct-btn {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  cursor: pointer;
  transition: background 120ms ease;
}
.acct-btn:hover,
.acct-btn[aria-expanded='true'] { background: var(--panel); }
.acct-btn .rail-avatar { width: 34px; height: 34px; }

.acct-pop {
  position: absolute;
  bottom: 0;
  inset-inline-start: calc(100% + 14px);
  min-width: 194px;
  padding: var(--s2);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: 0 18px 40px rgb(0 0 0 / 55%);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Who you are, not a thing to press -- so it does not look like the first
   item of the menu. */
.acct-who {
  margin: 0;
  padding: var(--s2) var(--s3) var(--s3);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--s2);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.acct-link {
  display: flex;
  align-items: center;
  min-height: 40px;
  padding: 0 var(--s3);
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--dim);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  text-align: start;
  text-decoration: none;
  cursor: pointer;
  width: 100%;
}
.acct-link:hover,
.acct-link:focus-visible { background: var(--panel-2); color: var(--ink); text-decoration: none; }

/* The one destructive item, set apart by a rule rather than by a red that is
   not in this palette. */
.acct-out { margin-top: var(--s2); border-top: 1px solid var(--line); border-radius: 0 0 var(--radius) var(--radius); }

/* --- the language picker in the drawer -----------------------------------

   A button that shows the current language, and a list of twenty that opens
   under it. Both rows are 52px so a finger has the same target everywhere, and
   the flag is a fixed 24x24 box so the names start on one straight column --
   a flag that sized itself to its own artwork would leave the column ragged.

   That box is SQUARE, and the <svg> inside it must carry viewBox="0 0 2 2" to
   match. It carried "0 0 3 2" for a while, left over from before the flags
   were redrawn for a square: preserveAspectRatio then fitted a 3:2 drawing
   into a 24x24 box as 24x16 and centred it, so every round flag sat in the
   circle with 4px of panel showing above and below it. Nothing overflowed and
   nothing was misaligned, which is why no guard and no sweep saw it -- the
   artwork was simply smaller than the hole it was cut from. */
.langs { position: relative; padding: 0 11px; }

.lang-btn,
.lang-opt {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  min-height: 52px;
  padding: 0 var(--s3);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  text-align: start;
  cursor: pointer;
}

.lang-btn {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.lang-btn:hover { background: var(--panel-2); }
.lang-caret { margin-inline-start: auto; color: var(--dim); flex: none; }

.lang-list {
  position: absolute;
  inset-inline: 11px;
  bottom: calc(100% + 6px);
  z-index: 3;
  margin: 0;
  padding: var(--s2);
  list-style: none;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: 0 -18px 40px rgb(0 0 0 / 55%);
  /* Twenty rows do not fit a phone drawer, so the list scrolls inside itself
     rather than pushing the page around. */
  max-height: 60vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.lang-opt { border-radius: var(--radius); }
.lang-opt:hover,
.lang-opt:focus-visible { background: var(--panel-2); outline: none; }
.lang-opt[aria-selected='true'] { background: var(--panel-2); color: var(--acc); }

/* The fixed box the names line up against. */
/* Round, and square underneath. The flags in src/lib/flags.mjs are composed
   for a 1:1 box rather than cropped into one, so the circle cuts only the
   corners -- nothing of a hoist-side emblem is lost. */
.lang-flag {
  flex: none;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  overflow: hidden;
  /* A hairline, so a white flag on a dark panel still reads as a flag rather
     than as a hole in the row. */
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 20%);
}
.lang-flag svg { display: block; width: 100%; height: 100%; }
.lang-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The header's language picker: a "translate" glyph, the same size and colour as the
   three icons beside it. It was the chosen flag for a while -- a picture in
   a row of icons, and the loudest thing in the bar. The flags are still in
   the list it opens. */
.langs-head {
  display: flex; align-items: center;
  padding: 0;
}
.langs-head .lang-btn {
  width: 44px; min-height: 44px; height: 44px;
  padding: 0;
  justify-content: center;
  background: none; border: 0;
  color: var(--dim);
}
.langs-head .lang-btn:hover,
.langs-head .lang-btn[aria-expanded='true'] { color: var(--ink); background: var(--panel); }
/* Recent and Favourites, before the globe. The group takes the bar's
   hard-right margin so the four icons sit together at its end. */
.head-links {
  margin-inline-start: auto;
  display: flex; align-items: center; gap: var(--s1);
  padding-inline-end: var(--s1);
}
.head-link {
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border-radius: var(--radius);
  color: var(--dim);
}
.head-link:hover { color: var(--ink); background: var(--panel); text-decoration: none; }
.head-link[aria-current='page'] { color: var(--acc); }
/* Solid, and larger than the rail's 20px strokes: a filled glyph carries
   more ink per pixel, and these four are the whole of the corner. */
.head-icon-svg { width: 26px; height: 26px; display: block; }
/* The last icon of the corner, and the one that holds the bar's end gap. */
.head-menu {
  background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
  align-self: center;
  margin-inline: var(--s1) clamp(8px, 1.6vw, 16px);
}
.head-menu[hidden] { display: none; }
.langs-head .lang-list {
  inset-inline: auto 0;
  bottom: auto;
  top: calc(100% + 8px);
  width: 240px;
  box-shadow: 0 18px 40px rgb(0 0 0 / 55%);
}

/* --- the category palette ------------------------------------------------
   Fifteen categories, fifteen hues, one rule each.

   THESE WERE STYLE ATTRIBUTES AND A TEST STOPPED THE DEPLOY OVER IT, rightly:
   the colour is the same fifteen values on every page of the site, so sending
   them inline re-sends them 13,800 times instead of once inside a stylesheet
   that is cached after the first page. The markup now carries data-cat, which
   is the category's slug -- a fact about the tile, not a decoration.

   The values are the same ones in src/lib/category-icons.mjs, which stays the
   source: it is what categoryIcon() and the tests read. Two files holding one
   truth is a drift waiting to happen, so test/category-colours.test.mjs reads
   both and fails if they disagree. */
[data-cat="shooter"] { --cat: #f87171; }
[data-cat="racing"] { --cat: #fb8a5c; }
[data-cat="action"] { --cat: #fbb03c; }
[data-cat="casual"] { --cat: #f0c945; }
[data-cat="animals"] { --cat: #d9d24a; }
[data-cat="sports"] { --cat: #86d95c; }
[data-cat="adventure"] { --cat: #3fd39a; }
[data-cat="two-player"] { --cat: #2ecfc0; }
[data-cat="arcade"] { --cat: #38c6e8; }
[data-cat="learning"] { --cat: #4aa8f0; }
[data-cat="strategy"] { --cat: #6f8ff5; }
[data-cat="simulation"] { --cat: #8f82f2; }
[data-cat="puzzle"] { --cat: #b073ee; }
[data-cat="horror"] { --cat: #cd6ee0; }
[data-cat="girls"] { --cat: #f06ab4; }

.head-icon { display: inline-grid; place-items: center; vertical-align: -4px; margin-inline-end: var(--s2); color: var(--acc); }

/* --- app-like navigation -------------------------------------------------
   public/j/app.js draws .nav-progress across the top of the page on every
   link followed; the speculation rules in Base.astro have usually fetched
   the page before the tap. */
.nav-progress {
  position: fixed; inset: 0 0 auto 0; height: 3px; z-index: 1000;
  background: var(--acc);
  box-shadow: 0 0 10px var(--acc);
  transform: scaleX(0); transform-origin: 0 50%;
  pointer-events: none;
}
[dir="rtl"] .nav-progress { transform-origin: 100% 50%; }

/* A cross-fade between pages instead of a blank frame. Chrome, Edge and
   Safari 18.2+ do it for a same-origin navigation with this one rule;
   everything else navigates exactly as before. The rail and the header are
   the same on every page, so they are named and hold still while the
   content changes under them. */
@view-transition { navigation: auto; }
::view-transition-old(root),
::view-transition-new(root) { animation-duration: 160ms; }
.rail { view-transition-name: site-rail; }
.col > header { view-transition-name: site-header; }
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* The drawer's Install app row is a <button> dressed as the links around it. */
.drawer-install { width: 100%; border: 0; background: none; font: inherit; font-size: 15.5px; font-weight: 650; text-align: start; cursor: pointer; }
.drawer-install[hidden], .drawer-install-ios[hidden] { display: none; }
.drawer-install-ios { margin: var(--s2) 12px 0; color: var(--dim); font-size: 13.5px; line-height: 1.5; }

/* /offline/, the page the service worker answers with when there is no
   network. */
.offline { display: flex; flex-direction: column; align-items: center; text-align: center; padding: clamp(48px, 12vh, 120px) 0; }
.offline-icon { color: var(--dim); margin-bottom: var(--s4); }
.offline .lede { margin-inline: auto; }
.offline-retry { max-width: 240px; border: 0; cursor: pointer; font-family: inherit; }

/* Rules the PHP site adds to site.css (tokens.css + base.css, copied from
   core/src/styles). The language picker's options are links now
   (wiki/decisions/2026-09-27-da-ngon-ngu-theo-duong-dan.md). */
a.lang-opt { text-decoration: none; }
a.lang-opt[aria-current='true'] { background: var(--panel-2); color: var(--acc); }

/* The desktop rail in another language (the owner, 2026-09-28: "rất lộn xộn
   khi text dài 2 line"). The rail is 86px wide, so "TRANG CHỦ" or "TUYỂN
   CHỌN" wraps -- and wrapped, it hung from the left under a centred icon.
   Each line is centred now, the two lines kept even, and a single long word
   ("KATEGORIEN", "ПОПУЛЯРНЫЕ") is hyphenated in the page's language -- or,
   with no dictionary for it, broken -- instead of spilling out of the column.
   English labels fit one line and are left exactly as they were. */
@media (min-width: 781px) {
  html:not([lang="en"]) .rail-item { padding-inline: 4px; }
  html:not([lang="en"]) .rail-item span,
  html:not([lang="en"]) .rail-menu span {
    max-width: 100%;
    text-align: center;
    line-height: 1.2;
    letter-spacing: 0.02em;
    text-wrap: balance;
    hyphens: auto;
    overflow-wrap: anywhere;
  }
}

/* The desktop rail on a short window (the owner, 2026-09-28: "bị đè lên
   nhau"). .rail-scroll scrolls with its scrollbar hidden, so when the window
   is shorter than the list, the item at its edge was cut mid-word right
   above the account block -- "NHÃN" read as a row of dashes -- and the items
   below it were hidden with no sign they existed. Its two ends now fade (the
   padding keeps the first and last item clear of the fade when nothing is
   hidden), so a cut item fades out and says "there is more"; on a short
   window the items also sit closer, so fewer of them are hidden at all. */
@media (min-width: 781px) {
  .rail-scroll {
    padding: 8px 11px 20px;
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 8px, #000 calc(100% - 20px), transparent 100%);
            mask-image: linear-gradient(to bottom, transparent 0, #000 8px, #000 calc(100% - 20px), transparent 100%);
  }
}
@media (min-width: 781px) and (max-height: 860px) {
  .rail-scroll { gap: 2px; }
  .rail-item { padding-block: 8px 6px; gap: 4px; }
  .rail-menu { padding-block: 6px; }
}
