
/* ---------------------------------------------------------------------------
   Faces

   Self-hosted from /fonts — Cloudflare static assets, see wrangler.jsonc —
   rather than a font CDN. Two reasons: the CSP below is default-src 'self'
   with no font-src, so a third-party origin would be blocked outright; and a
   cross-origin round trip before first paint would undercut the immutable
   caching everything else here gets.

   Two families since the 2026-09-09 redesign, where there were three. The
   client's design sets every word on the site in Archivo and every heading in
   Archivo Black, so Figtree and Space Grotesk have nothing left to do. Their
   files are still in public/fonts and their @font-face blocks are gone; an
   unreferenced face is never fetched, so what actually changed for a visitor
   is that the page now costs two latin files instead of three — about 55KB
   against 77KB — and that headings arrive in a face drawn to be heavy rather
   than in Archivo pushed to 800.

   Archivo Black is a separate family rather than a weight: the variable
   Archivo axis stops at 900 and its heaviest cut is still a text weight. It
   ships one weight, 400, and asking it for 700 gets you a synthesised bold —
   so nothing here ever sets a weight on --font-display.

   Both are OFL-licensed. Each ships latin and latin-ext; the unicode-range
   split means latin-ext is fetched only when a page actually contains one of
   its characters, so the usual cost is the two latin files. Both are
   preloaded in Layout.tsx.

   These files are served immutable, so replacing one means renaming it —
   otherwise browsers hold the old copy for a year.
   --------------------------------------------------------------------------- */
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 100 900;
  font-stretch: 100%;
  font-display: swap;
  src: url("/fonts/archivo-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 100 900;
  font-stretch: 100%;
  font-display: swap;
  src: url("/fonts/archivo-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Archivo Black";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/archivo-black-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Archivo Black";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/archivo-black-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---------------------------------------------------------------------------
   Tokens
   --------------------------------------------------------------------------- */
/* The values below came off the client's design of 2026-09-09, which redrew
   the whole site. Two of theirs are not used as drawn and it is worth saying
   which before anybody "restores" them:

     #8A8175, their faintest grey, is 3.77:1 on the new paper and 3.35:1 on
     the panel. It carries struck-through prices, the legal line and card
     meta — all of it body text, all of it under AA. --ink-muted is the same
     hue at a lightness that clears 4.5:1 everywhere, and the ramp is three
     greys rather than four because a fourth would have landed 0.4 of a ratio
     from its neighbour and read as the same colour.

     #4F7A28 is 4.42:1 on the panel. It is kept exactly as drawn for FILLS
     (--accent-2, which only ever draws lines and meters and needs 3:1), and
     darkened by one step on the same hue for TYPE (--ok-ink, which needs
     4.5:1). Those were already two tokens here for this reason.

   Everything else is theirs verbatim. Re-derive the sums below if any ground
   moves; the darkest ground a semantic ink sits on is --panel, not --paper. */
:root {
  /* Three grounds, and which one a thing sits on is what its contrast is
     measured against.

       --paper    the body. Near-white, where it used to be a warm grey.
       --panel    the alternate band: the footer, a paper section, the
                  phone menu, the sort disclosure. This is what --paper used
                  to do, and it is a new token rather than a re-pointed one
                  because the body and the alternate band are now genuinely
                  two colours instead of one.
       --surface  cards and inputs — the things that have to lift off the
                  ground they sit on. Unchanged. NOT the masthead, which is
                  --paper as of 2026-09-09: white on #fffdf9 is 1.01:1, so a
                  header in it read as a lighter strip across the top of the
                  page with no line to explain why, and its own rule is what
                  separates it from the page anyway. */
  --paper: #fffdf9;
  --panel: #f4efe5;
  --surface: #ffffff;
  /* The ground behind a photograph while it loads, and behind the tinted
     placeholder where there is no photograph at all. */
  --photo-ground: #efe9de;
  --ink: #17140f;
  /* Long prose — the blurbs, the FAQ answers, anything that runs to a
     paragraph. Between --ink and --ink-muted, and new: the design sets
     running text a step lighter than headings and a step darker than meta,
     which this site had no token for. 8.62:1 on the panel. */
  --ink-soft: #4a4237;
  /* Meta, and the faintest type on the site. See the note above about why
     there is no fourth grey below this one. 5.17:1 on the panel. */
  --ink-muted: #6b6357;
  --rule: #e7e1d6;
  --rule-strong: #17140f;
  /* Two accents, and between them they carry every non-neutral pixel here.
     Both obey the same rule: they are fills and lines, never glyphs. Anything
     that wants to read as accent-coloured *type* takes --ink or --ink-muted
     and earns its emphasis from weight, an underline, or the neutral ramp.

       --accent    orange, and actionable — the primary button, the Shop chip
                   in the masthead, the tile hover, the focus ring, hover
                   underlines.
       --accent-2  olive, and informational — the complexity meter and the
                   accent event note. It marks what describes rather than what
                   invites, which leaves orange unambiguously the thing to do.

     Text sitting ON either fill takes whichever of #fff or --ink clears the
     4.5:1 AA needs. Which one that is falls out of the fill's own lightness
     rather than taste, and the two land on opposite answers:

       on --accent      #fff 5.24:1 ✓    --ink 3.51:1 ✗
       on --accent-2    #fff 5.06:1 ✓    --ink 3.63:1 ✗

     Both carry white now, and that is a change worth noticing: the olive used
     to be light enough that ink was the readable side of it and white was
     not. The design's green is darker, so the pair agree for the first time
     and every label on either fill is white. Both are set explicitly at every
     use rather than inherited, and re-deriving this pair is the first thing
     to do if either value moves.

     One thing these tokens do not reach: public/logo.png is a raster and
     carries its own brighter #fd6807. It has never tracked --accent. */
  --accent: #b0501b;
  --accent-2: #4f7a28;

  /* The Tuesday nights band. This was its own dimmer orange — #a8501a, the
     accent with the saturation taken out of it — because at full strength the
     old #b34d00 competed with the buttons on the page when it ran full-bleed.

     The 2026-09-09 design draws that band at the accent itself, and the two
     values are now within a hair of each other anyway (#b0501b against
     #a8501a): the new accent already IS the dimmer orange, because it moved
     towards it rather than away. So the token stays, pointing at the accent,
     rather than being deleted and taking .band and its two button states
     through a rewrite for a change nobody would see. White on it is 5.24:1.

     Fills only, like the other two. Nothing sets type in it. */
  --accent-muted: #b0501b;

  /* Four semantic inks, and an admission: the rule above says the accents are
     fills and lines and never glyphs, and these are glyphs. That was the right
     rule for an editorial page and the wrong one for a shop — a reduced price
     that does not look reduced is a reduction nobody notices, and the client
     said so in as many words. So the palette gains exactly four colours, each
     with one meaning, used on type and nowhere else:

       --sale-ink   a price that has come down, and the chip that says by how
       --ok-ink     good news — in stock, free delivery reached
       --warn-ink   true but awkward — nearly out, a few left
       --info-ink   neither — a pre-order, something on its way

     Every one of them is a *second* signal, never the only one: the sale price
     is also struck through, the stock line also says the number, the pre-order
     is also a word. Somebody who cannot separate the hues loses nothing.

     Contrast against --panel (#f4efe5), which is the DARKEST ground any of
     them sits on and therefore the worst case for dark type. (The note here
     used to say --paper was the worst case because it was the lightest; that
     had it backwards, and was harmless only because --paper was in fact the
     darkest ground at the time. It no longer is.)

       --sale-ink   6.43:1 ✓   --ok-ink     4.79:1 ✓
       --warn-ink   5.68:1 ✓   --info-ink   7.56:1 ✓

     All four clear AA for body text, which is the bar because all four *are*
     body text. Re-check them if --panel, --paper or --surface moves.

     Two of these moved with the redesign and two did not. The sale red is the
     design's own #A81C1C. The green is its #4F7A28 darkened one step on the
     same hue (91°) — as drawn it is 4.42:1 here, and it is the *type* half of
     a pair whose fill half keeps the original value; see --accent-2. Amber
     and blue stayed: the design names no colour for either, and both still
     clear AA against the new grounds. */
  --sale-ink: #a81c1c;
  --ok-ink: #4b7426;
  --warn-ink: #8a5000;
  --info-ink: #1f4e79;

  /* Two faces from one family, which is the design's whole type system.

     --font-display  Archivo Black. Headings, the hero, the wordmark, and the
                     display numerals that are styled like headings (prices,
                     dates, stat values). It ships ONE weight — never set
                     font-weight on it; see the note in the faces block.
     --font-body     Archivo. Everything else: prose, nav, buttons, the cart
                     table, meta. Its weight axis does the work that three
                     families used to.
     --font-eyebrow  Archivo again, kept as a token rather than deleted. The
                     eyebrow is a role, not a face: what makes it read as one
                     is that it is small, uppercase and letterspaced, and
                     those rules all live on .eyebrow already. Keeping the
                     name means the day somebody wants a third face back,
                     there is one place to put it.

     This was Archivo / Figtree / Space Grotesk until 2026-09-09. Pairing a
     grotesque with a geometric was what kept 14px nav type distinct from
     14px headings; the design answers that with weight and case instead,
     which is why the eyebrow rules matter more than they did.

     The fallbacks are the old system stack, so a failed font request degrades
     to what the site shipped with rather than to Times. */
  --font-system: "Segoe UI", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  --font-display: "Archivo Black", "Archivo", var(--font-system);
  --font-body: "Archivo", var(--font-system);
  --font-eyebrow: "Archivo", var(--font-system);

  --step-0: 1rem;
  --step-1: clamp(1.125rem, 0.35vw + 1.05rem, 1.3rem);
  --step-2: clamp(1.5rem, 0.9vw + 1.3rem, 2rem);
  --step-3: clamp(2rem, 2vw + 1.5rem, 3rem);
  /* Only .hero__headline uses step-4 — the home page's h1, and nothing else
     on the site is set this large. It was loud enough to be the whole page;
     smaller lets the shelf below it share the first screen. */
  --step-4: clamp(2.75rem, 5.5vw + 0.25rem, 5.5rem);

  /* Tracking belongs to the size, not the element. Headings here run from a
     112px hero down to 17px card titles — a 6.6x range — and a single value
     across all of it cannot be right at both ends: what reads as confident on
     the hero reads as cramped on a card title, where the letters are already
     only a few pixels apart.

     So each step carries its own. The numbers tighten as the type grows, which
     is what optical sizing does by hand. --track-3 is the -0.028em every
     heading used to share; it turned out to suit roughly the page-title size
     and nothing else. */
  --track-1: -0.012em;
  --track-2: -0.02em;
  --track-3: -0.028em;
  /* Eased with step-4: at ~74px the hero sits nearer the page-title size than
     it did at ~94px, so it wants slightly less of a pull. */
  --track-4: -0.032em;

  /* Near-full-bleed, but not flush: a thin gutter keeps text off the screen
     edge. ~26px on a 1280px laptop, ~20px on a phone. */
  --gutter: clamp(1.25rem, 2vw, 2.25rem);
  --shell: 1760px;
}

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

/* The browser's own rule for the hidden attribute sets display to none, and
   any class that sets display outranks it — several here do. That is not a
   nicety. The cart badge is marked hidden when the basket is empty and drew
   an empty orange disc regardless; the shelf's Go button is marked hidden by
   the script the moment sorting works without it, and stayed on the page.
   An attribute that says "not rendered" has to win. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Archivo Black ships one weight. Asked for a heavier one, a browser
     smears the glyphs sideways and calls it bold — which on a face that is
     already black reads as a rendering fault rather than as emphasis. This
     refuses the fake and renders the real 400 instead.

     It costs the body face nothing: Archivo is variable across 100-900, so
     every weight this site asks for there genuinely exists and none of them
     was ever being synthesised. */
  font-synthesis-weight: none;
}

img, svg { display: block; max-width: 100%; height: auto; }
input, button, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }

a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover { text-decoration-color: var(--accent); }

/* The label rules further down are class-based, so the handful of headings
   that carry .eyebrow or .site-footer__heading still win over this. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  /* 400 is not a lightening — it is the only weight Archivo Black has, and
     the browser's own bold on a heading would otherwise be synthesised.
     This declaration is what stops that, so it has to stay even though it
     looks like it is saying nothing. */
  font-weight: 400;
  /* The small end of the ramp, because most headings on the site are step-1 or
     smaller. The three that run larger raise it where they are defined. */
  letter-spacing: var(--track-1);
  line-height: 1.03;
  text-wrap: balance;
}
p { text-wrap: pretty; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 1px;
}

/* ---------------------------------------------------------------------------
   Utilities
   --------------------------------------------------------------------------- */
.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Available to screen readers, invisible on screen. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  top: 0; left: 0;
  z-index: 100;
  padding: 0.75rem 1.25rem;
  background: var(--ink);
  color: #fff;
  font-weight: 700;
  text-decoration: none;
  transform: translateY(-110%);
}
.skip-link:focus { transform: translateY(0); }

.eyebrow {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
/* Full-strength ink, for an eyebrow that is heading a box of its own instead
   of introducing a headline below it. (There was an .eyebrow--muted here for
   the reverse case, back when the base was orange. It has no call sites and
   would now just restate the base, so it is gone.) */
.eyebrow--ink { color: var(--ink); }

/* .eyebrow is the named one, but nine rules across the site share its
   treatment — small, uppercase, 0.10-0.14em tracking — and they only read as
   one system if they share the face too. Listed together here rather than set
   nine times, so the set is obvious and stays complete.

   Each keeps its own size, weight and colour where it is defined; the only
   thing this rule owns is the family. */
.eyebrow,
.wordmark,
.stats__label,
.tile__count,
.event__month,
.cart-table th,
.site-footer__heading {
  font-family: var(--font-eyebrow);
}

.lede { font-size: var(--step-1); color: var(--ink-muted); max-width: 46ch; }

/* WCAG 2.2 target size (2.5.8) asks for 24px of height on standalone links.
   Small type here means several would land at 17–22px, so pad them out. Links
   inside prose are exempt — they are constrained by the line-height around
   them — and are deliberately not listed. */
.wordmark,
.masthead__utils a,
.section__more,
.game-card__title a,
.event__title a,
.link-card__title a,
.breadcrumbs a,
.site-footer__list a {
  display: inline-block;
  padding-block: 0.28rem;
}

.surface { background: var(--surface); }

/* ---------------------------------------------------------------------------
   Buttons
   --------------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.8rem 1.4rem;
  border: 1px solid var(--ink);
  background: transparent;
  color: var(--ink);
  font-weight: 700;
  font-size: 0.9375rem;
  letter-spacing: -0.01em;
  text-decoration: none;
  transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}
.btn:hover { background: var(--ink); color: #fff; }

/* White on --accent is 5.3:1 — see the pairing table in the tokens above. This
   used to be the one place that shipped white on the brighter orange at 3.6:1
   and accepted the miss; darkening the token is what closed it. */
.btn--primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn--primary:hover { background: var(--ink); border-color: var(--ink); }

/* Sits on the accent band. At rest it is a white chip, so its label is ink;
   the hover drops the fill and leaves the label straight on the orange, where
   white is the side of the pairing that clears AA. */
.btn--onaccent { background: #fff; border-color: #fff; color: var(--ink); }
.btn--onaccent:hover { background: transparent; border-color: #fff; color: #fff; }

.btn--small { padding: 0.4rem 1rem; font-size: 0.8125rem; }

/* A button whose whole label is a mark. The sides match the ends, so an 18px
   glyph sits in the middle of a square rather than at the centre of a short
   wide box drawn for two or three words — .btn--small's 1rem of side padding
   is what makes an icon-only button read as a button somebody forgot to write
   the label into.

   It has to come after .btn--small: both are one class, so source order is the
   whole of what decides which padding wins.

   The mark is a block because an inline svg sits on the text baseline and
   carries the line box's descender space under it, which is half a line of
   nothing inside a box that is meant to be square. There is no flex gap to
   worry about: the name beside it is .visually-hidden, which is out of flow
   and therefore not a flex item at all. */
.btn--icon { padding: 0.45rem; }
.btn__mark { display: block; }

/* The primary commerce action. Deliberately outweighs the secondary button
   next to it so the main thing to do on the page is unambiguous. */
.btn--large { padding: 1.05rem 2rem; font-size: 1.0625rem; }

/* ---------------------------------------------------------------------------
   Header (client design, 2026-09-09)

   Sticky, and one row: the wordmark, the nav centred, and Search and Cart at
   the right. On a phone the nav collapses into a disclosure behind a hamburger
   and the four sections move to the bar at the foot of the window — see
   .tabbar below.

   It carried a full-width search box on a second row until 2026-09-09, when
   the client asked for it to go. Search is the utility beside the cart now,
   and it is a link to /search, which has a box of its own at the top of it.
   The header is 56-odd pixels rather than 119, which is most of what a sticky
   header costs a phone.

   Everything here works with nothing running: every control here is a link.
   There is no phone menu since 2026-09-10 — see the note in the 60rem block
   below.
   --------------------------------------------------------------------------- */
.masthead {
  /* The same ground as the page under it, at the client's word on 2026-09-09.
     It was --surface, which against the new near-white paper is 1.01:1 — not a
     band, just a strip that reads as slightly whiter than everything below it
     for no reason a visitor can see. The rule along its foot is what separates
     the header from the page, as it is for .hero-band and .section. */
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  /* Sticky is the design's, and it earns its keep on a long shelf: the search
     box and the cart are the two things somebody reaches for mid-scroll. */
  position: sticky;
  top: 0;
  z-index: 40;
}
/* A grid rather than a wrapping flex row, and the reason is the centre column.

   The nav sits in the middle of the header at the client's word (2026-09-09),
   and centred has to mean centred on the page rather than centred in whatever
   space the wordmark and the utilities leave over. A flex row can only do the
   second: an auto margin either side of the nav splits the free space, so the
   nav lands halfway between two flanks that are not the same width, which here
   is some 40px off true and reads as a near miss. Two equal fr columns either
   side of an auto one is the whole fix, and it needs no wrapper element and no
   stretched wordmark to hold a column open. */
.masthead__inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-areas: "brand nav utils";
  align-items: center;
  gap: 0.625rem clamp(0.75rem, 2vw, 1.75rem);
  min-height: 56px;
  padding-block: 0.625rem;
}
.wordmark { grid-area: brand; }
.masthead__nav { grid-area: nav; }
.masthead__utils { grid-area: utils; justify-self: end; }

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  margin-inline-end: auto;
  font-weight: 700;
  font-size: 0.8125rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}
/* Placeholder lockup, cropped from a logo mockup — a raster at a fixed ratio,
   so width stays auto and the img carries width/height for the aspect ratio.
   Swap for an SVG when the real artwork lands and this rule still holds. */
.wordmark__logo {
  width: auto;
  height: 34px;
  flex: none;
}

.masthead__list {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.4vw, 2.25rem);
  list-style: none;
  padding: 0;
}
.masthead__link {
  font-size: 0.90625rem;
  font-weight: 500;
  text-decoration: none;
  padding-block: 0.35rem;
  border-bottom: 2px solid transparent;
}
.masthead__link:hover { border-bottom-color: var(--accent); }
/* The section you are in, at the client's word on 2026-09-09: the word itself
   in the accent, under a softened accent rule. It was an --ink underline under
   ink type, which is the same mark the phone's bottom bar and the phone's
   drawer had already stopped making — both of those have marked the current
   section in the accent since they were drawn. This is the third of the three
   agreeing rather than a new idea.

   Two things worth knowing.

   Accent type is a deliberate exception to the rule in the tokens above, and
   the second one on the site: the semantic inks took the first in August, and
   the masthead's own utilities already colour on hover. It clears the bar the
   rule exists to protect — 5.16:1 on --paper, which is the only ground this
   header is ever drawn on — and it is re-derived here rather than assumed,
   because it moves the day --accent or --paper moves.

   And the underline stays, softened rather than dropped, because it is the
   half of the mark that is not a colour. A current section said in orange and
   nothing else is a state carried by hue alone; the rule under the word is
   what somebody who cannot separate the two still sees. 45% of the accent puts
   it a clear step under both the word above it and a hovered link's full-
   strength rule, which is what keeps "you are here" and "you are pointing at
   this" apart. */
.masthead__link[aria-current="page"] {
  color: var(--accent);
  border-bottom-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

/* Every link here is a word among words. Shop was drawn apart from them — a
   filled orange chip first, then a hairline box — and both read as an
   advertisement sitting in the navigation rather than as navigation. The
   mark on the section you are in is the only mark the masthead makes, and it
   means the same thing on all four. */
.masthead__utils {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  font-size: 0.875rem;
  font-weight: 600;
}

/* Search and Cart: a mark, then the word.

   The cart was a bordered chip until 2026-09-09, when the client asked for the
   box to go. It is the same argument the masthead already made about Shop on
   2026-08-26 — a link drawn apart from the links beside it reads as an
   advertisement in the navigation — and it only survived that pass because the
   cart was the last thing in the header with nothing beside it. Now there are
   two of them, and a box around one of a pair says the pair are different
   kinds of thing.

   Selected through .masthead__utils because the utilities block up in Utilities
   sets display: inline-block on .masthead__utils a for the 24px tap target, and
   at two class names it outweighs a bare .masthead__util. That is the same
   specificity trap the badge rule further down carries a note about. */
.masthead__utils .masthead__util {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  text-decoration: none;
}
.masthead__utils .masthead__util:hover { color: var(--accent); }
/* The mark is the size of the word beside it and never larger: it is a second
   way of saying what the word says, not the thing being said. */
.masthead__util-mark { flex: none; }

@media (max-width: 60rem) {
  /* The row is two things: the wordmark and the utilities. It was three until
     the client took the phone's menu off on 2026-09-10 — the burger held this
     same navigation, which the tab bar at the foot of the screen now carries.

     What went with it: .masthead__menu, .masthead__burger and the drawer's
     own block, plus the position: relative that was here only so the drawer
     could be absolutely positioned against this row, and a redundant
     position: sticky on .masthead, which is sticky at every width anyway.

     The wordmark's own auto end margin is still what holds the utilities at
     the right, so neither column needs a fraction. */
  .masthead__inner {
    grid-template-columns: auto 1fr;
    grid-template-areas: "brand utils";
  }
  .masthead__nav { display: none; }
  .wordmark { margin-inline-end: auto; }
  .wordmark__logo { height: 28px; }
}

/* ---------------------------------------------------------------------------
   The bar across the foot of a phone

   Four links, from the client's design. Nothing here needs the script; the
   badge carries the same data-cart-count hook the masthead's does, so the one
   paintBadge fills both.

   The body element carries padding for its height further down, so the last line of the
   footer is never underneath it.
   --------------------------------------------------------------------------- */
.tabbar { display: none; }
.tabbar__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}
.tabbar__link {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.1875rem;
  padding-block: 0.5rem;
  min-height: 3.25rem;
  color: var(--ink-muted);
  text-decoration: none;
  border-top: 2px solid transparent;
  margin-top: -1px;
}
.tabbar__link[aria-current="page"] { color: var(--accent); border-top-color: var(--accent); }
.tabbar__icon { position: relative; display: block; }
.tabbar__count {
  position: absolute;
  top: -0.4375rem;
  inset-inline-end: -0.6875rem;
  font-size: 0.6875rem;
  font-weight: 700;
  color: var(--accent);
}
.tabbar__label { font-size: 0.6875rem; font-weight: 600; }

@media (max-width: 60rem) {
  .tabbar {
    display: block;
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: 50;
    background: var(--surface);
    border-top: 1px solid var(--rule);
    /* The home indicator on a phone without a hardware button sits over the
       bottom of the window. Without this the labels are under it. */
    padding-bottom: env(safe-area-inset-bottom);
  }
  /* The bar is fixed, so it covers whatever the page ends with. This is the
     height of it plus the same inset again. */
  body { padding-bottom: calc(3.25rem + env(safe-area-inset-bottom)); }
}

/* ---------------------------------------------------------------------------
   Hero (client design, 2026-09-09, revised)

   An even split: the shop's standing sentence and its two calls to action on
   the left, a card of featured titles on the right.

   The card is a horizontal scroll-snap row of whole panels, one title each —
   each panel carries its own photograph AND its own words, so the two travel
   together. That is the difference between this and the canvas it came from,
   which changes the contents of one card: client state, and with the script
   gone one panel that can never change.

   Built this way it is swipeable with nothing running: swipe the row, or
   press a dot, and the browser scrolls it. The script adds the automatic
   advance and takes over marking the dots. See the header of pages/home.tsx
   and wireHeroSwitch in site/script.ts.

   There are no arrows here. They were the one script-owned control on the
   hero this replaces and existed because three panels across the full width
   wanted them; two panels in a half-width card do not, and each dot already
   names a panel, so with nothing running the dots and the swipe are the whole
   control.
   --------------------------------------------------------------------------- */
.hero-band {
  /* The same ground as everything below it — see the note on .section. The
     rule under it is what separates the hero from the shelf, and it is the
     only line the design draws there. */
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  /* Named because the rule down the middle of the hero has to reach past it:
     see the note on .hero__aside. */
  --hero-pad: clamp(1.5rem, 3vw, 2.5rem);
  /* Top only, at the client's word on 2026-09-09: the band ends at the foot of
     the photograph. A strip of paper under a picture that is already the end of
     the band is a gap with nothing in it, and at both widths the last thing in
     here is that picture. The words still get their air at the top, and the
     card's own contents keep theirs — see .hero__aside. */
  padding-block: var(--hero-pad) 0;
}

.hero {
  display: grid;
  /* Stacked is the narrow default and the split is the enhancement, so a
     phone gets the words and then the card with no rules to undo. */
  gap: clamp(1.25rem, 4vw, 2rem);
}

@media (min-width: 60rem) {
  .hero {
    /* The even split the client asked for. The gap is zero because the rule
       down the middle is what separates the halves, and a gap either side of
       a border reads as two lots of space around one hairline; the padding on
       .hero__intro below is what holds the words off it. */
    grid-template-columns: 1fr 1fr;
    gap: 0;
    align-items: stretch;
    /* The band's vertical padding, moved into the two columns below, so that
       the rule down the middle is the aside's own border for the whole height
       of the band rather than a border with two drawn segments bolted onto its
       ends. It has to be one box: a 1px border at a fractional device-pixel
       ratio is snapped down to whole device pixels — measured at dpr 1.5 the
       border computes to 0.667px — while a pseudo-element's width: 1px is not,
       so the two extensions rendered half again as wide as the line they were
       continuing. That is what the client saw. Nothing moves: what the columns
       take back as padding is exactly what this gives up. Top only, matching
       the band's own padding, which is top only for the reason written there. */
    margin-block: calc(var(--hero-pad) * -1) 0;
  }
}

/* ---------------------------------------------------------------------------
   The left half: what the shop is
   --------------------------------------------------------------------------- */
.hero__intro {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: clamp(0.75rem, 1.5vw, 1.125rem);
  min-width: 0;
}
@media (min-width: 60rem) {
  .hero__intro {
    padding-right: clamp(2rem, 5vw, 4.5rem);
    /* The band's padding, taken back — see the note on .hero above. */
    padding-block: var(--hero-pad);
  }
}

.hero__kicker {
  font-family: var(--font-eyebrow);
  font-size: 0.71875rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
}

/* The one thing on the site set at step-4, and the page's h1 again: this
   design's headline is the shop's own standing sentence rather than the name
   of whichever title happens to lead the shelf this week. */
.hero__headline {
  font-size: var(--step-4);
  letter-spacing: var(--track-4);
  /* 1.05, not the 0.95 this was drawn at, and the figure is measured rather
     than taste. Archivo Black's ink runs 0.941em from the top of an ascender
     to the foot of a descender — 54px up and 16px down at the 74.4px this sets
     at 1280 — so a 0.95 line left 0.68px between the g of one line and the b
     of the next. Touching, and it read as a clipped descender: the client
     reported the g of "Bringing" as cut off, and they were looking at the line
     below it. 1.05 leaves 0.109em, which is 8px here and 3.9px at the 36px a
     phone sets, and both are real daylight. Nothing clips it — there is no
     overflow: hidden anywhere above this — so anything tighter than the ink
     is a collision rather than a crop, and the fix is the leading. */
  line-height: 1.05;
  /* The measure, not the column. Half of a 1760px shell at this size is nine
     or ten words to a line, which is a line nobody can find the start of
     twice running. */
  max-width: 12ch;
}

.hero__lede {
  font-size: 1.0625rem;
  line-height: 1.55;
  color: var(--ink-soft);
  max-width: 44ch;
}

/* Shared with /about, /checkout and /events, which render a row of buttons
   under a heading and have done since long before this hero existed. */
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.625rem; }
@media (min-width: 60rem) {
  .hero__actions { margin-top: 0.5rem; }
}

@media (max-width: 46rem) {
  /* A phone is the whole reason these three rules exist, and the measurements
     are from 375px.

     The headline is drawn for the front of a desktop page. At its floor it is
     44px, which is four lines and most of a screen before the lede has said
     anything — and the card of games, which is the half of the hero somebody
     came to look at, starts below the fold. Down a step it is 36px and the
     first game is on the first screen.

     The buttons are the same argument as 2026-08-27, when the approved page
     had this exact problem: two .btn--large at 262px and 292px cannot share a
     335px line, so they stack into two full-width slabs and cost another
     120px. The fill is what marks the primary one, not the padding. */
  .hero__headline { font-size: clamp(2.25rem, 9.5vw, 2.75rem); }
  .hero__lede { font-size: 1rem; }
  .hero__intro .btn--large { padding: 0.8rem 0.95rem; font-size: 0.9375rem; }

  /* And then, at the client's word on 2026-09-10, the whole column goes and a
     phone opens on the featured card. The three rules above are what is left
     of a year of shrinking this to fit; the answer the client reached is that
     it should not be on a phone at all, and the measurement agrees — even
     shrunk it was 340px of the first 504, so the games began below the fold on
     anything shorter than an iPhone 12.

     Two things it is careful about, and neither is optional.

     The h1 stays. A page with no h1 has no outline and nothing for a search
     result to key on, and this is the site's front page; it is hidden the way
     /shop's is, which is the idiom this site already had for exactly this.
     The declarations are .visually-hidden's, written out because the class
     cannot be applied at one width only.

     And it fires on .hero--featured alone, so the empty-shelf hero — which is
     this column and nothing else — is untouched. See the note in home.tsx.

     The card's own top rule goes with the column: it is there to separate the
     card from the words above it, and a hairline under the masthead with
     nothing above it reads as a line somebody forgot to remove. The band's
     top padding stays, so the card still opens a gutter below the header. */
  .hero--featured .hero__intro > :not(.hero__headline) { display: none; }
  .hero--featured .hero__headline {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .hero--featured { gap: 0; }
  .hero--featured .hero__aside { border-top: 0; }
}

/* ---------------------------------------------------------------------------
   The right half: the featured card
   --------------------------------------------------------------------------- */
.hero__aside {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* Stacked, the card spans the window and keeps its photograph edge to edge,
     the way the shelf's own cards do. */
  margin-inline: calc(var(--gutter) * -1);
  border-top: 1px solid var(--rule);
}
@media (min-width: 60rem) {
  .hero__aside {
    margin-inline: 0;
    border-top: 0;
    /* The one line the design draws through the hero, and it runs the whole
       height of the band at the client's word on 2026-09-09 — the border on
       the content alone started 34px below the header's rule and stopped 35px
       above the shelf's, which reads as a line somebody forgot to finish.

       It reaches by the aside being as tall as the band and holding the band's
       padding itself, so the rule is this one border and nothing else. It was
       two pseudo-elements drawn across the padding until the client reported
       the ends looking thicker than the middle, which they were: see the note
       on .hero. The card's own contents do not move — the padding they lost
       above them is the padding they take back here.

       Only the top of it, at the client's word later the same day: the card
       should end at the foot of the photograph rather than leaving a strip of
       paper between the picture and the shelf's rule. The box still spans the
       whole band, so the border is untouched; what goes is 38px of nothing,
       and the picture takes it. The words in the column beside this one keep
       their own padding on both sides. */
    padding-block: var(--hero-pad) 0;
    border-left: 1px solid var(--rule);
  }
}

/* The track and the dots, and nothing else. It is the box the dots are
   positioned against, and it exists because .hero__aside cannot be: that
   carries the band's vertical padding so the rule down the middle runs the
   whole height of the band, and an absolutely positioned box is placed against
   the padding box — so the dots measured their 6px from 38px below the
   photograph and sat on the paper underneath it. Nothing else changes: the
   stage is the track's own size. */
.hero__stage {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.hero__track {
  list-style: none;
  padding: 0;
  margin: 0;
  /* Auto-columns at 100% rather than a flex row: it needs no per-slide basis
     and the snap points fall exactly on the panel edges. */
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
  /* Fills the card, so a panel's photograph reaches the foot of the hero
     rather than stopping wherever its own ratio ran out. */
  flex: 1;
  min-height: 0;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* NO scroll-behavior: smooth here, and that is the whole reason the dots
     work. This track carried it until now, and with it the browser does not
     scroll the track for a fragment navigation at all: pressing a dot set the
     hash, moved the page vertically, and left the panel exactly where it was.
     Measured — auto scrolls it to 682, smooth scrolls it to 0 — and it was
     true of the hero this replaces too, so the control this site documents as
     "the whole of it with no script" has never actually moved the carousel.

     Nothing is lost. The automatic advance still animates, because the script
     asks for it by name in scrollTo, and an explicit behaviour beats the
     property. What changes is that a press jumps rather than glides, which is
     also what a reduced-motion setting wants — hence no override below any
     more; the script gates its own timer on that setting already. */
  /* The bar would sit across the bottom of the photograph. The dots are the
     control; the scrollbar is only how it happens to be built. */
  scrollbar-width: none;
}
.hero__track::-webkit-scrollbar { display: none; }

.hero__slide {
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  /* min-width on a grid item that is itself a grid column: without it one long
     unbroken title widens the column past 100% and the snap points drift off
     the panel edges. */
  min-width: 0;
  /* Load-bearing, and it does not look it. Every button in a panel carries a
     .visually-hidden span naming the game, and that class is position:
     absolute — so its containing block is the nearest positioned ancestor.
     Without this that was .hero__aside, OUTSIDE the scroller, and a 1px span
     belonging to the panel you cannot see sat at its static position ~680px
     past the right edge of the window and could not be clipped by a scroller
     it did not belong to. The page scrolled sideways by most of a panel's
     width, on every screen, with nothing visible out there to explain it.
     A panel is the containing block for its own contents. */
  position: relative;
  /* A dot is an anchor, so pressing one scrolls the page to the panel as well
     as scrolling the track to it — and the masthead is sticky, so without
     this the panel lands underneath it and the game's own name is the part
     that goes. 5rem clears the 57px the header measures now that the search
     box has gone off its second row, with room to spare so the panel lands
     clear of the rule rather than against it. Same idiom as .game-card in the
     admin. */
  scroll-margin-block-start: 5rem;
}

.hero__aside-head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  /* Named because the price and the Add button reach past it — see the pull on
     .hero__line's last child. */
  --head-pad: var(--gutter);
  padding: clamp(1rem, 2vw, 1.5rem) var(--head-pad);
}
@media (min-width: 60rem) {
  .hero__aside-head { --head-pad: clamp(1.5rem, 3vw, 2.5rem); }
}

.hero__title {
  font-size: var(--step-2);
  letter-spacing: var(--track-2);
  line-height: 1.05;
}
.hero__title a { text-decoration: none; }
.hero__title a:hover { text-decoration: underline; text-decoration-color: var(--accent); }

.hero__meta { font-size: 0.9375rem; color: var(--ink-muted); }

.hero__stock { font-size: 0.875rem; font-weight: 600; }
.hero__stock--ok { color: var(--ok-ink); }
.hero__stock--warn { color: var(--warn-ink); }
.hero__stock--info { color: var(--info-ink); }
/* An empty shelf. Red rather than the muted grey it was, at the client's word
   on 2026-09-09 — see shelfState in site/data.ts. */
.hero__stock--danger { color: var(--sale-ink); }

/* The card's two split lines, at the client's word on 2026-09-09: the title
   with the price ranged right against it, and the meta with the Add button
   ranged right against that. The price still sits above the button, which is
   what they asked for first; what changed is that the pair no longer has a
   block of its own under the words, because the right-hand half of those two
   lines was empty and is exactly the shape of what went in it.

   The words are the flexible half and the control is not: a title long enough
   to wrap does so inside its own item rather than squeezing a price, which is
   what min-width: 0 on one side and flex: none on the other say. Past that
   they wrap, and the control lands ranged right on a line of its own — the
   arrangement this replaced, which is the right thing to fall back to.

   Both are divs in the markup and not ps, because a form closes an open
   paragraph and the parser does it silently: trap 11 in CLAUDE.md is this
   exact markup. */
.hero__line {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.5rem 0.75rem;
  width: 100%;
}
.hero__line > :first-child { flex: 1 1 auto; min-width: 0; }
.hero__line > :last-child { flex: 0 0 auto; margin-left: auto; }

@media (min-width: 60rem) {
  /* The price and the Add button reach the end of the card, at the client's
     word on 2026-09-09. They were ranged right against the head's own padding,
     which left them 38px short of the edge the photograph below them ends on —
     near enough to read as a miss rather than as an inset. Pulled out by
     exactly that padding they line up with the picture, and with the shell's
     right edge above it.

     Narrow widths are deliberately left alone: there the card is full-bleed to
     the window, so the same pull would put a button hard against the screen
     edge with nothing to hold it off. */
  .hero__line > :last-child { margin-right: calc(var(--head-pad) * -1); }
}

/* What the game is and what the shelf says, as one item on the buy row rather
   than a row and a paragraph under it. The row is as tall as the button, so a
   stock line outside it began 35px below the description — the button's own
   height, spent on nothing. The two lines together come out at about what the
   button measures, so the row is balanced and the only gap left is the one
   between two lines of type. */
.hero__facts {
  display: flex;
  flex-direction: column;
  /* No gap, and the leading comes down with it. A 4px gap between these two
     read as twelve, because most of the space between two lines of type is
     inside their own line boxes and not between them: the meta carries 8.25px
     of leading at 15/23.25 and the stock 7.7px at 14/21.7, so half of each
     lands in the gap before a gap is even set. Measuring the boxes says 4 and
     the eye says 12, which is why the first attempt at this did not look like
     it had done anything.

     1.3 is tight for running prose and right for two labels that are one
     thought — what the game is, and whether we have it.

     The figure is the client's, twice: 4px after seeing it at nothing, then
     6px. Zero was too far — with the leading already down it put the two lines
     4.3px apart and they read as one block — so the gap is back and the
     tightened leading stays. At 6px it measures about 10px of daylight against
     the 12 it started at, which is the same space arrived at deliberately
     rather than by default. */
  gap: 0.375rem;
  min-width: 0;
}
.hero__facts > * { line-height: 1.3; }
/* Baseline on the title's row, so the price sits on the same line the title is
   written on rather than in the middle of a box half again as tall as itself.

   The button's row starts its two items together instead, at the client's word
   on 2026-09-09. Centred, a 44px button against a 22px line of meta hangs 11px
   above it and 11px below, so the description appeared to begin half a line
   down from the button beside it — two things on one row that did not look
   like they started at the same place. Not baseline: that would align the word
   inside the button with the meta, which drops the button's own box well below
   the line and costs the row its height twice over. */
.hero__line { align-items: baseline; }
.hero__line--buy { align-items: flex-start; }

.hero__price {
  font-family: var(--font-display);
  font-size: 1.5rem;
  letter-spacing: var(--track-2);
}

/* ---------------------------------------------------------------------------
   The card on a phone: the same shape, smaller

   At the client's word on 2026-09-10. This block used to say the opposite —
   that below 46rem the buy row wrapping was "the intended fallback" — and the
   client is right that it reads as a mistake rather than as a fallback: the
   button landed alone on a line under the stock line, ranged right with a
   whole line's width of nothing beside it, and the head ran to 103px to hold
   a row whose tallest item is a 51px button.

   It was only ever a width problem, and the measurement says exactly how much:
   at 375px the row has 335px and wants 386 — a 251px line of meta, a 123px
   button, and the 12px between them. Everything here is that 51px, taken off
   the four things that can give it up rather than off any one of them, which
   is what keeps the result looking like the wide card instead of a squeezed
   one. Measured after: 322 of 343, so a longer meta than either panel carries
   today still holds the line.

   The title row is untouched. It fits at this width and always has. */
@media (max-width: 46rem) {
  /* 4px back from each side. The card is full-bleed here, so this padding is
     the only thing holding the words off the screen edge and it does not go
     below a phone's ordinary gutter. */
  .hero__aside-head { --head-pad: 1rem; }
  .hero__line { gap: 0.375rem 0.625rem; }
  .hero__meta,
  .hero__stock { font-size: 0.8125rem; }
  /* The two facts a phone drops are hidden in the shared block further down,
     with the shop card's — see .meta-part. Nothing hero-specific is left to
     say here. */
  /* The same button set smaller, not .btn--small: that is a different control
     with its own floor under a coarse pointer, and this one still has to clear
     44px for a thumb. At this padding it measures 44 exactly, so the row is
     the button's height and the card's head loses the 52px the wrap cost. */
  .hero__line--buy .btn { font-size: 0.875rem; padding: 0.65rem 0.85rem; }
}

/* The photograph takes whatever the card has left. The basis stays auto so
   the ratio is its floor — no panel ever draws a picture shorter than 16/10 —
   and the grow is what carries it to the foot of a hero whose height is set
   by the headline in the column beside it. Both panels share one track, so
   they end on the same line and the shorter one simply crops a little more. */
.hero__aside .photo { flex: 1 0 auto; min-height: 0; }

/* The empty state: a shop part-way through being set up has nothing listed at
   all, which is exactly where this one is today. The left column still
   stands, so the page still opens with its headline and both its buttons. */
.hero__empty { padding-block: clamp(1.5rem, 5vw, 3rem); }

/* ---------------------------------------------------------------------------
   The dots

   On the photograph, bottom right, opposite the credit chip — where the
   approved design put them, and where they cost the picture no height. There
   is no panel behind them: a drop-shadow on the row does that job, because it
   follows the shape of each mark and a box does not.
   --------------------------------------------------------------------------- */
.hero__dots {
  position: absolute;
  right: 0.375rem;
  bottom: 0.375rem;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  filter: drop-shadow(0 1px 2px rgba(20, 17, 15, 0.6));
}
.hero__dot {
  display: block;
  /* The mark is 10px square and a 10px tap target is not one. The padding is
     the target; the mark inside it is what you see. It matters more here than
     it looks: with no arrows, these and the swipe are the whole control.
     22x26 measured. */
  padding: 0.5rem 0.375rem;
  text-decoration: none;
}
.hero__dot-mark {
  display: block;
  width: 0.625rem;
  height: 0.625rem;
  /* White rather than --rule, because these sit on a photograph now. The
     drop-shadow on the row above is what keeps them off a pale one. */
  background: #fff;
  transition: width 200ms ease, background-color 200ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .hero__dot-mark { transition: none; }
}

/* Which mark is filled has two answers and the switcher says which is in
   force. With the script live it is aria-current, moved as the track scrolls.
   Without it, :target — and the first panel when nothing is targeted yet,
   which is the state every visit starts in.

   The :target arms are written out per panel rather than generated, because
   CSS has no way to say "the nth dot for the nth panel". HERO_SLIDES in
   pages/home.tsx is 2; a third panel needs a third line here. */
.hero__dot[aria-current="true"] .hero__dot-mark,
.hero__aside:not([data-hero-live]):not(:has(:target)) .hero__dots li:first-child .hero__dot-mark,
.hero__aside:not([data-hero-live]):has(#hero-1:target) [data-hero-dot="0"] .hero__dot-mark,
.hero__aside:not([data-hero-live]):has(#hero-2:target) [data-hero-dot="1"] .hero__dot-mark {
  width: 1.75rem;
  background: var(--accent);
}

/* ---------------------------------------------------------------------------
   The answers at the foot of the home page

   Three claims, each under its own heavy rule. The rule is 2px and ink rather
   than a hairline, because these are the last thing on the page before the
   footer and a hairline there reads as the footer starting early.
   --------------------------------------------------------------------------- */
.faqs {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16.25rem), 1fr));
  gap: clamp(1.125rem, 3vw, 2rem);
}
.faq { border-top: 2px solid var(--ink); padding-top: 0.75rem; }
.faq__q { font-size: 1rem; }
.faq__a {
  margin-top: 0.4375rem;
  font-size: 0.90625rem;
  line-height: 1.55;
  color: var(--ink-soft);
}

/* The /faq page's variant, from 2026-09-09.

   The grid above is auto-fit at a 16.25rem minimum, which was drawn for the
   home page's three short claims and lays a longer set out in columns: with
   nine questions the answer to the second sits underneath the answer to the
   first, and the reading order is something the eye has to work out rather
   than something the page states. So the list is one column here, and the
   width is spent on putting each answer beside its own question instead.

   The rule stays 2px ink, which the base block earns on the home page by being
   the last thing before the footer. Here it is doing a second job as well: it
   is the only thing separating one question from the next, and a hairline
   between two blocks of running prose is not enough of a break to scan by. */
.faqs--list { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
@media (min-width: 60rem) {
  .faqs--list .faq {
    display: grid;
    grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
    gap: clamp(1.5rem, 4vw, 3rem);
    align-items: start;
  }
  /* The answer sits on the question's own first line rather than a line under
     it. Its margin is what holds it off the question when the two are stacked
     and is a shove downwards once they are side by side. */
  .faqs--list .faq__a { margin-top: 0; }
}

/* ---------------------------------------------------------------------------
   The facts strip

   Three facts at the foot of the home page — how you pay, what delivery costs,
   how long it takes — and a fourth cell that is the way to /faq. It replaced
   the three answers that sat here until 2026-09-09, and then the single line
   that replaced those; see the note on the Facts component in pages/home.tsx
   for why the front page says anything here at all.

   It draws the stats strip above rather than a layout of its own, at the
   client's word. That block's own comment says the home page used to carry
   those four figures, so this is a house idiom coming back to the page it left
   rather than a new one, and nothing here restyles it: only the ground, the
   rule and the fourth cell's link belong to this block.

   It is --paper deliberately, where a closing strip would ordinarily take
   --panel: the footer directly below it is --panel, and two panel grounds with
   nothing between them read as one very tall footer that happens to start with
   some figures. Its top rule is what separates it from the orange band above,
   exactly as .section and .hero-band are separated from what precedes them.
   --------------------------------------------------------------------------- */
.facts { background: var(--paper); border-top: 1px solid var(--rule); }
/* The whole cell is the link, so the target is a quarter of the strip rather
   than two words inside it. Stretching the anchor over the cell's own padding
   is what does that: without it the cell is padded and the link is not, and
   the press only counts within the words.

   The accent on --paper is 5.16:1, the sum the masthead's current-section mark
   is derived from. The label under it stays --ink-muted, so the pair reads as
   one cell of the strip that happens to be pressable rather than as a button
   somebody dropped in the row. */
.facts .stats__item--cta { padding: 0; display: grid; }
.facts .stats__item--cta a {
  /* Grid rather than block so the anchor stretches to the cell's own height.
     Its content is shorter than the three cells beside it — the value is a
     step down — so a block anchor stopped 17px above the bottom of a row it
     was supposed to be the whole of. */
  padding: 1.75rem 1.25rem;
  color: var(--accent);
  text-decoration: none;
  align-content: start;
}
.facts .stats__item--cta a:hover .stats__value { text-decoration: underline; text-underline-offset: 0.2em; }
/* A step down from the figures beside it, because it is language rather than a
   figure: at --step-2 "Go to FAQs" wraps to two lines in a cell drawn for
   "2–3", and a strip whose last cell is twice the height of the other three is
   a strip with something wrong in it.

   The line-height is what keeps the row's second line straight. The label under
   a smaller value starts higher — measured 17px higher than the three beside
   it — so the box is held at the height the others' type makes: --step-2 is
   the font-size they take and 1.55 is the line-height this sheet gives them,
   so the product is their box, at every width the clamp passes through. The
   figure has to move if either does. */
.facts .stats__item--cta .stats__value {
  font-size: var(--step-1);
  line-height: calc(var(--step-2) * 1.55);
}


/* ---------------------------------------------------------------------------
   Posters are shown whole

   A photograph of a table can be cropped to a shape and lose nothing that
   matters; a poster cannot. It is somebody's artwork with words on it, and
   a cover crop cuts the words off, so wherever an event's own poster is
   rendered, the whole of it is visible and the box takes the letterboxing.

   The tint underneath goes with it: those radial washes exist to stand in
   for a missing photograph, and behind a contained one they turn the mount
   into a coloured smear.
   --------------------------------------------------------------------------- */
.event__poster .photo {
  background-image: none;
  background-color: var(--photo-ground);
}
.event__poster .photo img { object-fit: contain; }



/* ---------------------------------------------------------------------------
   Photo frame + placeholder
   --------------------------------------------------------------------------- */
.photo {
  position: relative;
  aspect-ratio: var(--photo-ratio, 4 / 3);
  overflow: hidden;
  /* The ground a photograph loads against, and what a title with none shows
     under the washes below. It was a raw #d8d2c9; it is the design's own
     image ground now, and a token because the poster rule and the product
     stage both need to name the same colour. */
  background: var(--photo-ground);
  /* Two soft washes so the placeholder reads as a photo slot, not a bug.
     --tint is set per card; see photoTint() in components/Photo.tsx. */
  background-image:
    radial-gradient(120% 90% at 18% 12%, color-mix(in srgb, var(--tint, #b08d6a) 55%, #fff) 0%, transparent 62%),
    radial-gradient(100% 80% at 88% 92%, color-mix(in srgb, var(--tint, #b08d6a) 70%, #2a2320) 0%, transparent 70%);
}
/* object-position is the crop. Every box here is cover, so something is
   always being cut off; --focal says which part must survive it, and is
   written per photograph from the media library. Unset means the centre,
   which is what cover does unaided. */
.photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focal, 50% 50%);
}

.photo__credit {
  position: absolute;
  left: 0; bottom: 0;
  padding: 0.25rem 0.5rem;
  background: rgba(20, 17, 15, 0.78);
  color: #fff;
  font-size: 0.5625rem;
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* ---------------------------------------------------------------------------
   Stats band

   /about renders four figures about the shop; the home page renders three
   facts and a link to /faq in the same cells, which is the .facts block below.
   The home page used to carry these same four figures above the shelf, and the
   .stats section wrapper went with them — /about sets its own top rule inline
   and .facts sets one on its own section.

   So nothing here may assume a cell holds a number, or that the four are
   alike: the strip's last cell on the home page is an anchor at a smaller
   size, and .facts is where that is said.
   --------------------------------------------------------------------------- */
.stats__list {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  list-style: none;
  padding: 0;
}
.stats__item { padding: 1.75rem 1.25rem; }
.stats__item + .stats__item { border-left: 1px solid var(--rule); }
.stats__value { font-family: var(--font-display); font-size: var(--step-2); font-weight: 400; letter-spacing: var(--track-2); display: block; }
.stats__label {
  display: block;
  margin-top: 0.4rem;
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
@media (max-width: 720px) {
  .stats__list { grid-template-columns: repeat(2, 1fr); }
  .stats__item:nth-child(odd) { border-left: 0; }
  .stats__item:nth-child(n + 3) { border-top: 1px solid var(--rule); }
}

/* ---------------------------------------------------------------------------
   Sections
   --------------------------------------------------------------------------- */
/* The page is one continuous ground, which is what the 2026-09-09 design
   draws: everything sits on --paper and the only tinted band is the footer.

   This was --surface, and it had to be while --paper was a warm grey — white
   sections on grey paper were the whole vertical rhythm of the site. With
   paper at #fffdf9 that pairing is #ffffff on #fffdf9, a contrast of 1.01,
   which is not a band so much as a rounding error. So the rhythm now comes
   from the alternate below rather than from every section being lighter than
   the page it is on.

   It cuts the other way for anything that sits IN a section and wants to be
   lifted off it — .link-card, .tile, .checkout__summary — all of which are
   --surface and were previously white on white. Those read for the first
   time. */
.section { background: var(--paper); padding-block: clamp(2.5rem, 5vw, 3.5rem); }
.section--paper { background: var(--panel); }
/* A section that opens with its own content rather than a heading — the shelf,
   whose heading is the one-line toolbar — needs the air above the heading back
   more than it needs the space. */
.section--tight { padding-top: clamp(1.25rem, 2vw, 1.75rem); }
.section + .section { border-top: 1px solid var(--rule); }

.section__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  margin-bottom: 1.75rem;
}
.section__title { font-size: var(--step-2); letter-spacing: var(--track-2); }
.section__more { font-size: 0.8125rem; font-weight: 700; color: var(--ink); text-decoration: none; }
.section__more:hover { text-decoration: underline; text-decoration-color: var(--accent); }

/* ---------------------------------------------------------------------------
   Game grid
   --------------------------------------------------------------------------- */
/* Fixed column counts rather than auto-fill: auto-fill would open a fifth,
   empty track on wide screens and leave a four-card row looking short.

   No dividers between the cards and no box around them. Every card leads with
   a photo, so the images already line up into a grid on their own, and a rule
   drawn alongside them only restates it — in the densest part of the page,
   where eight cards meant twelve hairlines. Gap does the separating, and the
   one boxed thing left inside a card is the Add button, which is the thing
   worth boxing.

   The grid keeps its top rule. That one belongs to the section rhythm rather
   than to the cards, and .events draws the identical line. */

/* ---------------------------------------------------------------------------
   Browse by kind

   A wrapping row of chip links above the filter panel. Links rather than a
   second form: choosing a kind is the commonest way to cut the shelf, and a
   link is one tap where a select-and-apply is three.
   --------------------------------------------------------------------------- */
.cat-nav { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 1rem; }
.cat-nav__link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.45rem 0.8rem;
  border: 1px solid var(--rule-strong);
  background: var(--surface);
  font-size: 0.8125rem;
  font-weight: 600;
  text-decoration: none;
  min-height: 36px;
}
.cat-nav__link:hover { border-color: var(--accent); text-decoration: none; }
.cat-nav__link[aria-current="page"] { background: var(--ink); border-color: var(--ink); color: #fff; }
.cat-nav__count { font-size: 0.6875rem; color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.cat-nav__link[aria-current="page"] .cat-nav__count { color: #cfc9c1; }

/* ---------------------------------------------------------------------------
   The shelf and its rail

   Filters down the left, games on the right. The rail is a fixed 15rem rather
   than a fraction: it holds labels and selects whose width is set by the
   longest word in them, and letting it grow with the viewport only ever made
   the selects wider than anything they contain.

   Below 60rem the two become one column and the rail goes *after* the shelf.
   Somebody who came to look at games sees games; the jump link at the top
   takes them to the filters, and the filters take them back with the anchor
   the form already submits to. Reversing the visual order like this is only
   honest because the jump link makes the rail reachable in reading order too.
   --------------------------------------------------------------------------- */
.shop-layout {
  display: grid;
  grid-template-columns: 15rem minmax(0, 1fr);
  gap: 2rem;
  align-items: start;
}
.shop-rail { min-width: 0; }
.shop-results { min-width: 0; }

/* Only ever visible in the single-column layout — see the media query. It
   belongs to /search now: the shelf's own jump link lives in its toolbar
   (.shop-toolbar__jump), and this one had been left behind when it moved
   there. The name lost its page, because the two are the same control. */
.filter-jump { display: none; }

/* Search puts its filter in a band rather than a rail: on that page the
   controls are the operation rather than a way of trimming a shelf, and a
   rail 860px tall cannot be taken in at once where a line of six selects
   can. This is a grid only so that the three parts can be re-ordered on a
   phone — see the media query below. */
.search-layout { display: grid; }
.search-results { min-width: 0; }

@media (min-width: 60.001rem) {
  /* The rail follows the shelf down a long page instead of scrolling out of
     reach at the top of it. Capped and still scrollable, so a tall rail on a
     short window can be got at — but the bar itself is not drawn.

     It is a panel of six controls, not a document, and a bar down the side of
     it reads as a second page inside the page. Wheel, trackpad, touch and the
     keyboard all still reach the bottom of it; only the bar goes. This also
     retires scrollbar-gutter: stable, which was here because the panel lost
     15px the moment applying a filter made it tall enough to scroll and every
     control in it twitched narrower. With no bar there is no lane to hold and
     nothing to twitch. */
  .shop-rail .filters {
    position: sticky;
    top: 1.5rem;
    max-height: calc(100vh - 3rem);
    overflow-y: auto;
    scrollbar-width: none;
  }
  .shop-rail .filters::-webkit-scrollbar { display: none; }
}

@media (max-width: 60rem) {
  .shop-layout { grid-template-columns: minmax(0, 1fr); gap: 1.25rem; }
  .filter-jump {
    display: inline-block;
    justify-self: start;
    font-family: var(--font-eyebrow);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    padding: 0.6rem 0.9rem;
    border: 1px solid var(--rule-strong);
    background: var(--surface);
    text-decoration: none;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    order: 1;
  }
  .shop-results { order: 2; }
  .shop-rail { order: 3; }
  .shop-rail .filters { margin-bottom: 0; scroll-margin-top: 1rem; }

  /* Search takes the same three-part order — link, results, controls — for
     the same reason the shelf does: stacked, the band is 700px of selects
     standing between somebody and what they asked for. */
  .search-layout { gap: 1.25rem; }
  .search-results { order: 2; }
  .search-layout .filters { order: 3; margin-bottom: 0; scroll-margin-top: 1rem; }
}

/* The rail flavour of the filter form: one control per row, headed sections,
   and the kinds as a list of links rather than a dropdown. */
.filters--rail { padding: 1rem 1.1rem; margin-bottom: 0; }
.filters--rail .filters__grid { grid-template-columns: minmax(0, 1fr); gap: 0.85rem; }
/* The search box spans two columns in the band layout, and a span of two in a
   grid with one column does not clamp — it invents a second one. That is what
   put Players and Complexity side by side in a 240px rail, each 65px wide with
   its own label clipped to "Any nu" and "Any co" under the chevron. The rail
   has one column, so nothing in it spans. */
.filters--rail .filters__field--wide { grid-column: auto; }
.filters--rail .filters__foot {
  flex-direction: column;
  align-items: stretch;
  gap: 0.85rem;
}
.filters--rail .filters__actions { display: grid; gap: 0.5rem; }
.filters--rail .filters__actions .btn { justify-content: center; text-align: center; }
.filters__title {
  margin: 0 0 0.9rem;
  padding-bottom: 0.7rem;
  border-bottom: 1px solid var(--rule-strong);
  font-family: var(--font-eyebrow);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.filters__group { margin-bottom: 1.1rem; }
.filters__label--group { display: block; margin-bottom: 0.5rem; }

/* Department-list shape: a stack of rows, the count pushed to the right edge,
   the current one marked with a rule rather than a fill — a filled row in a
   column of rows reads as a button somebody pressed by accident. */
.cat-nav--rail { flex-direction: column; flex-wrap: nowrap; gap: 0; margin-bottom: 0; }
.cat-nav--rail .cat-nav__link {
  display: flex;
  justify-content: space-between;
  gap: 0.6rem;
  border: 0;
  border-left: 2px solid transparent;
  background: none;
  padding: 0.4rem 0.55rem;
  min-height: 34px;
  font-weight: 400;
  font-size: 0.875rem;
}
.cat-nav--rail .cat-nav__link:hover { background: var(--panel); border-left-color: var(--rule); }
.cat-nav--rail .cat-nav__link[aria-current="page"] {
  background: none;
  color: inherit;
  border-left-color: var(--accent);
  font-weight: 700;
}
.cat-nav--rail .cat-nav__link[aria-current="page"] .cat-nav__count { color: var(--ink-muted); }

/* ---------------------------------------------------------------------------
   The filter panel

   Auto-fitting columns rather than a fixed count: the panel carries six or
   seven controls on /shop and one fewer on a collection page, and a grid that
   counts its own children keeps both looking deliberate. Nothing here reacts
   to a change — there is no script to react with — so Apply is a real button
   and the whole thing reads as a form, which is what it is.
   --------------------------------------------------------------------------- */
.filters {
  background: var(--surface);
  border: 1px solid var(--rule);
  padding: 1.1rem 1.25rem;
  margin-bottom: 1.75rem;
}
.filters__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: 0.85rem 1rem;
}

/* The applied filters, above the controls that set them. */
.filters__applied {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.6rem;
  margin-bottom: 0.9rem;
  padding-bottom: 0.9rem;
  border-bottom: 1px solid var(--rule);
}
.filters__applied-label {
  font-family: var(--font-eyebrow);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.6rem;
  min-height: 30px;
  background: var(--ink);
  color: #fff;
  font-size: 0.75rem;
  font-weight: 600;
  text-decoration: none;
}
/* The accent on hover says what the click does: it removes the chip. */
.filter-chip:hover { background: var(--accent); }
.filter-chip__x { font-size: 0.875rem; line-height: 1; opacity: 0.75; }
.filters__clear { font-size: 0.75rem; font-weight: 600; margin-left: auto; }
/* The search box earns two columns wherever there are two to earn. */
.filters__field--wide { grid-column: span 2; }
.filters__field { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; }
.filters__label {
  font-family: var(--font-eyebrow);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.filters__field select,
.filters__field input {
  width: 100%;
  /* 44px of thumb, which is what these are tapped with far more often than
     they are clicked. */
  min-height: 44px;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--rule-strong);
  /* background-color, not the shorthand — see the select rule below, whose
     chevron a shorthand at this specificity would reset away. */
  background-color: var(--surface);
  font: inherit;
  font-size: 0.875rem;
  color: inherit;
  /* One option carries its own explanation after a dash, and in a 15rem rail
     that runs past the edge. An ellipsis is a cut somebody can see is a cut;
     the default clip takes it off mid-letter. */
  text-overflow: ellipsis;
}

/* Dropdowns, drawn rather than inherited from the platform.

   A native select is a grey bevelled box with a grey chevron, which is the
   one widget on this site nobody had drawn — on a page of square edges and a
   single rule weight it reads as a control borrowed from another program. So
   the platform look comes off, the border matches the fields beside it, and
   the arrow is ours: a data URI, because a select cannot hold a
   pseudo-element and the mark has to be a background. The stroke colour is
   baked into the SVG, so it has to be updated by hand if --ink moves. */
select {
  appearance: none;
  -webkit-appearance: none;
  border-radius: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5l5 5 5-5' fill='none' stroke='%2314110f' stroke-width='1.5' stroke-linecap='square'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.7rem center;
  background-size: 0.7rem auto;
  padding-right: 2.1rem;
}
select::-ms-expand { display: none; }
.filters__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1rem;
  margin-top: 1rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--rule);
}
.filters__check { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; }
.filters__check input { width: 1.05rem; height: 1.05rem; }
.filters__actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }

@media (max-width: 34rem) {
  .filters { padding: 1rem; }
  /* One column is the only honest layout at this width, and the search box
     stops asking for a second one that is not there. */
  .filters__grid { grid-template-columns: minmax(0, 1fr); }
  .filters__field--wide { grid-column: auto; }
  .filters__actions { flex: 1 1 100%; }
  .filters__actions .btn { flex: 1 1 auto; text-align: center; }
}

.game-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* Rows want more air than columns: across a gap the neighbour is another
     photo, but down one it is the previous card's price row. */
  column-gap: clamp(1.25rem, 2vw, 1.75rem);
  row-gap: clamp(2rem, 3vw, 2.75rem);
  border-top: 1px solid var(--rule-strong);
  /* Stands in for the top padding the cards used to carry under the rule. */
  padding: 1.75rem 0 0;
  list-style: none;
}
.game-card {
  display: flex;
  flex-direction: column;
  /* The containing block for the stretched title link below. */
  position: relative;
}
.game-card > .photo { margin-bottom: 0.9rem; }

/* Chips over the photograph's top corner: the discount, Featured, the tag.
   Absolutely placed so they read as part of the picture, the way every shop
   a customer has already used draws them. */
.card-flags {
  position: absolute;
  top: 0.5rem;
  left: 0.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0;
  /* Above the stretched link, but the chips are labels, not controls. */
  pointer-events: none;
  z-index: 2;
}
.card-flags--inline { position: static; pointer-events: auto; margin-bottom: 0.75rem; }
.card-flag {
  display: inline-block;
  padding: 0.2rem 0.5rem;
  font-family: var(--font-eyebrow);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
/* The discount is the actionable fact, so it takes the actionable colour. */
/* Red rather than the accent orange: orange on this site means "press me",
   and a discount is news, not a control. White clears AA on it at 6.9:1. */
.card-flag--sale { background: var(--sale-ink); color: #fff; }
.card-flag--featured { background: var(--ink); color: #fff; }
/* .card-flag--tag was the outlined chip carrying the shop's own tag, on the
   card and on the product page. Both went on 2026-09-15 at the client's word,
   and the rule with them: every chip left is a fill, so an outlined one would
   now be the only chip of its kind on the site. */
/* Not in stock, but orderable — see the pre-order note in db/orders.ts. Blue
   because it is neither good news nor bad: it is a date. */
/* The empty-shelf chip. White on --sale-ink is 7.5:1, which is the figure the
   admin's danger pill already rests on.

   It shares its fill with .card-flag--sale, and that is not a collision worth
   solving: the two never say anything like each other, "Out of stock" leads
   the row so it is read first, and a discount on a box nobody can send is the
   less important of the two facts anyway. Giving the shelf its own red would
   be a fifth hue on a card that already carries four. */
.card-flag--oos { background: var(--sale-ink); color: #fff; }

/* A price that has come down, and the price it came down from. The red is
   --sale-ink, which exists for exactly this and three other jobs — see the
   note by the token. <del> already draws the line through the old figure, so
   the muted grey is all the second one needs. */
.price-now { color: var(--sale-ink); }
.price-was { color: var(--ink-muted); font-weight: 400; font-size: 0.85em; }

/* The shop's own rating. Filled stars take the informational accent, exactly
   as the complexity meter's cells do — this is a meter drawn with a glyph a
   shopper already knows how to read. */
.stars { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.75rem; line-height: 1; }
.stars__cell { color: var(--rule); font-size: 0.9375rem; }
.stars__cell--on { color: var(--accent-2); }
.stars__value { font-size: 0.6875rem; color: var(--ink-muted); font-variant-numeric: tabular-nums; }

.game-card__title { font-size: var(--step-1); }
.game-card__title a { text-decoration: none; }
/* The whole card is the link, not just the title: the pseudo-element
   stretches the title link's hit area over photo, meta and whitespace alike.
   The Add form lifts itself back above it — see .game-card__add. */
.game-card__title a::after { content: ""; position: absolute; inset: 0; }
.game-card__title a:hover { text-decoration: underline; text-decoration-color: var(--accent); }
.game-card:hover .game-card__title a { text-decoration: underline; text-decoration-color: var(--accent); }
/* The one real control on the card stays pressable above the stretched link. */
.game-card__add { position: relative; z-index: 1; }
.game-card__meta { margin-top: 0.3rem; font-size: 0.75rem; color: var(--ink-muted); }

/* ---------------------------------------------------------------------------
   The facts under a game's title

   Shared by the shop card and the hero's featured card, which render the one
   GameMeta component in ui.tsx. It was a string with the dots typed into it until
   2026-09-10, when the client asked for the player count and the weight to
   come off on a phone: CSS cannot take a word out of the middle of a sentence,
   so each fact is its own element.

   The separator belongs to the part that follows it rather than to the
   sentence, which is what makes hiding one safe — a hidden part takes its own
   middot with it and the line closes up without any rule having to know what
   its neighbours are showing.
   --------------------------------------------------------------------------- */
.meta-part { white-space: nowrap; }
/* The separator is a drawn hairline rather than a character, at the client's
   word on 2026-09-15. It was a middot, and before that a middot typed into a
   string.

   Drawn rather than the literal "|" they wrote, because a pipe is a glyph and
   glyphs come out of the face: Archivo sets it nearly the full em, ink-weight
   and sitting on its own baseline, which reads as punctuation in the sentence
   rather than as a rule between two facts. A box is the site's own hairline at
   whatever height and weight this line wants, on any face.

   It takes its colour from the type beside it rather than from --rule, which
   is drawn for white table rows and measures about 1.2:1 on paper. At 32% of
   the meta's own ink it is a clear step under the words without disappearing,
   and it follows if that colour ever moves. Same idiom as the masthead's
   current-section rule.

   The space in front of it is a real one in the markup, which is the line's
   only break opportunity now that each fact refuses to break inside itself.
   The space after it is this margin. Keep the two halves apart: a leading
   space set here would be inside the nowrap part, and the line would have
   nowhere to break at all and would run past the card. */
.meta-part + .meta-part::before {
  content: "";
  display: inline-block;
  width: 1px;
  /* Baseline to cap, near enough: 0.8em standing 0.08em below the baseline
     lands on Archivo's cap height at the top, so the rule measures the same as
     the figures either side of it rather than running past them. */
  height: 0.8em;
  vertical-align: -0.08em;
  /* The air either side has to be set on both sides to be equal. Before the
     rule there is already a real space in the markup, which is 0.21em in this
     face, so the start margin is the difference and the end margin is the
     whole figure. Measured at 15px: 5.2px before, 5.2px after, where the
     margin alone gave 3.1 and 5.1. */
  margin-inline-start: 0.14em;
  margin-inline-end: 0.35em;
  background: color-mix(in srgb, currentColor 32%, transparent);
}

/* A mark before each fact, at the client's word on 2026-09-15.

   Sized in em rather than px, which is the whole of why there is no second
   rule for the phone: the line is 12px on a card, 15px on the hero's card and
   13px on that card below 46rem, and the mark is the height of whichever it
   is beside. It is also why the marks are drawn on a 24 viewBox like the rest
   of the set and not at some size of their own — at 1em the set's own 1.7
   stroke lands at about 0.9px, which is the hairline this site draws every
   other line at.

   The part stays inline, deliberately: as a flex row the separator above would
   become a flex item and the gap would be applied around the middot as well as
   around the mark, so " · " would stop measuring what it measures everywhere
   else on the site. So the mark is an inline box nudged onto the line the words
   sit on, and the space after it is a margin rather than a gap.

   -0.14em is the optical drop, not a centring: the cap height of Archivo at
   this size sits a little above half the em box, so a mark aligned to the box
   rides high against the digits beside it. Measured at 1280: the line box does
   not grow, because a 1em mark dropped 0.14em still sits inside the font's own
   ascent and descent. Check that again if the face ever changes.

   And it has to say inline-block out loud. The reset at the top of this sheet
   is img, svg { display: block }, which every other mark on the site gets away
   with because every other one sits in a flex row — the masthead's utilities,
   the tab bar, the footer, the Add button. This is the first mark to stand in
   running text, and as a block each one took a line of its own: measured at a
   158px card, the three facts became five rows of 18.6px where they had been
   one. The reset's height: auto is overridden here too, by the 1em above. */
.meta-mark {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.14em;
  margin-inline-end: 0.32em;
}

@media (max-width: 46rem) {
  /* One fact goes, at the client's word: the age, leaving who it is for and
     how long it takes. It is the same call the card's complexity meter got the
     same day — the specialist end of the line — and it is on the product page,
     which the title above links to.

     The weight went with it on 2026-09-10 and is not hidden here any more
     because nothing draws it: on 2026-09-15 the client asked for the weight to
     appear on the product page and nowhere else, so the hero stopped appending
     it and the rule has nothing left to match.

     Nothing has to be said about the separator. The age is the last
     part, so it takes its own middot with it and the line simply ends after
     the playing time; the player count is still the first part, and the rule
     above never matched a first child.

     It was the players and the weight for one build, which needed a rule here
     killing the leading dot the newly-first part drew. Worth knowing if the
     choice ever changes again: hide anything but a tail of this list and that
     rule comes back, and it needs the sibling combinator to beat the two
     classes above it, because a media query carries no specificity of its own.
     That is trap 5's neighbour and the same fault .page-head--bare hit on
     2026-09-09. */
  .meta-part--age { display: none; }
}
/* The stock line the client's 2026-09-09 design puts on every card. The words
   are the whole signal — the colour is the second one, which is the rule every
   semantic ink here follows. shelfState in site/data.ts writes the sentence,
   so a card, the hero and the product panel cannot disagree. */
.game-card__stock { margin-top: 0.3rem; font-size: 0.78125rem; font-weight: 600; }
.game-card__stock--ok { color: var(--ok-ink); }
.game-card__stock--warn { color: var(--warn-ink); }
.game-card__stock--info { color: var(--info-ink); }
.game-card__stock--danger { color: var(--sale-ink); }
/* Price left, the button hard right, on every card in the grid.

   A flex row with space-between does that only while both fit on one line.
   A reduced price is two figures — Rs 9,200 beside a struck-through
   Rs 10,000 — and at two cards to a row that wraps, which dropped Add to a
   line of its own at the LEFT of the card while every card beside it kept
   its button at the right. A grid instead: the price takes the column it is
   given and wraps inside it, and the button stays in its own column
   wherever the price ends. */
.game-card__foot {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.75rem;
  margin-top: auto;
  padding-top: 1rem;
}
.game-card__foot > * { min-width: 0; }
/* The button and the out-of-stock line are the second column, whichever of
   the two the card is showing. */
.game-card__add,
.game-card__oos { justify-self: end; }
.game-card__price { font-weight: 700; font-size: 0.9375rem; }
.game-card__oos { font-size: 0.75rem; font-weight: 600; color: var(--ink-muted); }

/* The complexity meter was here: five cells filled to the rounded weight, on
   any card whose title had no rating. It came off the card on a phone on
   2026-09-10 and off it entirely on 2026-09-15, at the client's word, so the
   weight is a product-page fact now and the whole block is gone with the
   component that drew it.

   The one meter left is the spec strip's own, in the product section below,
   which was always separate markup. The note that used to live here is worth keeping
   beside it and is: those cells are olive rather than orange because a shelf
   of eight cards put forty of them above the fold, and in orange they massed
   into something competing with the one button on the page. */

/* Column count, and nothing else. Three nth-child rules used to live here to
   re-apply the divider the previous breakpoint had removed and then clear it
   from the new last column; with no dividers there is nothing to track. */
@media (max-width: 1000px) { .game-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* Two columns all the way down to a small phone. It used to drop to one at
   520px, which put a single 300px-tall photograph on the screen at a time and
   made a twelve-title shelf a minute of scrolling. One column is kept for the
   genuinely narrow screen, where two cards would be 140px each and the Add
   button would not fit beside its own price. */
@media (max-width: 760px) { .game-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 22rem) { .game-grid { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------------------------
   The card on a phone: two fewer things to read

   Reported by the client on 2026-09-10 as cluttered, off a screenshot of the
   shelf, and it is: a 161px column carrying a tag chip, a credit chip, a
   title, a meta line, a stock line, a complexity meter, a price and a button
   is eight things in the width of a thumbnail. Two come off, and they are the
   two that answer questions nobody is asking at this size.

   The grid stays two-up. That was decided deliberately — one column at this
   width is a twelve-title shelf read a photograph at a time — and the density
   is what was wrong, not the count.
   --------------------------------------------------------------------------- */
@media (max-width: 46rem) {
  /* The complexity meter was hidden here on 2026-09-10 and is not hidden now
     because it is not rendered at all: the client asked for it off the card at
     every width on 2026-09-15, so the markup goes rather than the display. The
     saving that rule measured is unchanged and then some, since it comes off a
     wide card too now. */

  /* And the credit stops being a third of the picture. It was two lines and
     36px of a 121px photograph, because a card's photo is 161px wide and
     "Photo by Dietmar Rabich (CC BY-SA 4.0)" is 38 characters: it was already
     at 9px, so this is not a case of type that was never sized for a phone.
     It cannot be made to fit one line at any size worth reading, so what is
     cut is everything around the words.

     It is not hidden, though the cart's thumbnails do exactly that with
     credit={false}. Attribution is a condition of CC BY-SA and of BGG's own
     terms, and a licence is not a layout decision. Scoped to .game-card so
     the hero's chip and the product stage's, both on photographs wide enough
     to carry one line, are untouched.

     Most of this goes away on its own: these are placeholder Wikimedia
     images, and every photograph the shop takes replaces one long CC BY-SA
     string with a short credit or none. */
  .game-card .photo__credit {
    font-size: 0.5rem;
    letter-spacing: 0.02em;
    line-height: 1.25;
    padding: 0.125rem 0.375rem;
  }
}

/* ---------------------------------------------------------------------------
   Shop tiles — image-led routes into the catalogue
   --------------------------------------------------------------------------- */
.tiles {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px;
  background: var(--rule);
  list-style: none;
  padding: 0;
}
.tile { position: relative; background: var(--surface); }
.tile a { display: block; text-decoration: none; }
.tile .photo { --photo-ratio: 4 / 5; }
/* The label sits over the image, so the scrim has to carry the contrast on its
   own — a real photograph underneath may be any brightness. The stops keep the
   scrim at 85% or darker everywhere the text actually sits (white on that is
   ~13:1 even over a white photo); a plain two-stop fade drops to ~35% at the
   top of the text and would fail over light images. */
.tile__label {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  padding: 2.25rem 1rem 1rem;
  background: linear-gradient(
    to top,
    rgba(12, 10, 9, 0.95) 0%,
    rgba(12, 10, 9, 0.85) 72%,
    rgba(12, 10, 9, 0) 100%
  );
  color: #fff;
  font-family: var(--font-display);
  font-size: clamp(1rem, 1.2vw, 1.25rem);
  font-weight: 400;
  letter-spacing: var(--track-1);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
}
/* The hover is a fill, so it takes --accent itself. The label stays white,
   which means the shadow and everything else about it carry over unchanged —
   only the colour behind it swaps from the scrim to the orange.
   94% rather than the scrim's 85%: the photo showing through is arbitrary, so
   this has to hold at both extremes. It does — white lands at 4.8:1 over a
   pure-white image and 5.8:1 over a pure-black one. At 88% the white case
   dropped to 4.4:1. */
.tile a:hover .tile__label {
  background: linear-gradient(
    to top,
    var(--accent) 0%,
    color-mix(in srgb, var(--accent) 94%, transparent) 72%,
    rgba(12, 10, 9, 0) 100%
  );
}
.tile__count { display: block; font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; opacity: 0.85; margin-top: 0.2rem; }
/* The 0.85 above is comfortable over the dark scrim, but over the orange it
   puts this 11px label at 4.3:1. Full strength on hover, where it is 5.3:1. */
.tile a:hover .tile__count { opacity: 1; }
@media (max-width: 860px) { .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Four tall portraits are 550px of signposting on a phone. These are a way
   into the catalogue rather than the catalogue, so they take a landscape crop
   and lose a third of the height.

   aspect-ratio directly, not --photo-ratio: the Photo component writes that
   variable into a style attribute on every instance, and an inline custom
   property beats any stylesheet rule that tries to set the same one. What a
   stylesheet can still do is win on the property that reads it. */
@media (max-width: 46rem) { .tile .photo { aspect-ratio: 4 / 3; } }

/* ---------------------------------------------------------------------------
   Accent band
   --------------------------------------------------------------------------- */
/* Deliberately slim, and deliberately not the full accent. This used to be the
   loudest thing on the home page, which put the secondary action (events)
   above the primary one (buying a game). Slimming it was half the answer;
   --accent-muted is the other half, and it keeps the orange for the things
   that are actually actions. See the token for the figures.

   The field is flat. It carried a stitched diamond lattice — a folk
   cross-stitch motif, drawn from an SVG data URI — until 2026-08-26, when
   the client asked for it off. If a texture ever comes back here, the thread
   has to be DARKER than the field: white type over a pale-stitched orange
   drops the worst case under 4.5:1, where the flat field is 5.5:1. The
   figures are at the token.
   --------------------------------------------------------------------------- */
.band {
  background: var(--accent-muted);
  color: #fff;
  padding-block: clamp(1.5rem, 2.5vw, 2.25rem);
}
.band__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: clamp(1rem, 3vw, 2.5rem);
  align-items: center;
}
/* The title leads the band. It carried a 7px top margin to sit under an
   eyebrow naming a fixed night, a price and a neighbourhood; the client took
   that line out on 2026-09-09, and there is no fixed game night and no address
   closer than the city to put back. So the heading is the first thing in the
   column and the band's own padding is what holds it off the edge. The
   .band .eyebrow rule that put white on --ink-muted here went with it —
   nothing else in the site draws an eyebrow on an accent ground. */
.band__title { font-size: var(--step-2); max-width: 34ch; letter-spacing: var(--track-2); line-height: 1.1; }
/* The sentence under the headline, which the 2026-09-09 design added. Slightly
   off full white so it sits under the headline rather than beside it — 0.92
   keeps it at 4.8:1, where dimming it further would not. */
.band__body { font-size: 0.9375rem; margin-top: 0.5rem; max-width: 46ch; color: rgb(255 255 255 / 0.92); }
.band__cta { display: flex; }
@media (max-width: 720px) { .band__grid { grid-template-columns: 1fr; align-items: start; } }

/* ---------------------------------------------------------------------------
   Event list
   --------------------------------------------------------------------------- */
.events { list-style: none; padding: 0; border-top: 1px solid var(--rule-strong); }
.event {
  display: grid;
  grid-template-columns: 72px minmax(0, 1fr);
  gap: 1.25rem;
  padding-block: 1.25rem;
  border-bottom: 1px solid var(--rule);
}
/* A row with a poster grows a third column for it; without one, nothing about
   the calendar changes. */
.event--postered { grid-template-columns: 72px minmax(0, 1fr) clamp(6rem, 12vw, 9rem); align-items: center; }
.event__poster { max-width: 100%; }
@media (max-width: 520px) {
  .event--postered { grid-template-columns: 72px minmax(0, 1fr); }
  .event__poster { grid-column: 2; max-width: 12rem; }
}
.event__date { text-align: left; border-right: 1px solid var(--rule); padding-right: 1rem; }
.event__day { display: block; font-family: var(--font-display); font-size: var(--step-1); font-weight: 400; letter-spacing: var(--track-1); line-height: 1; }
.event__month { display: block; margin-top: 0.2rem; font-size: 0.5625rem; font-weight: 700; letter-spacing: 0.12em; color: var(--ink-muted); }
.event__title { font-size: 1.0625rem; }
.event__title a { text-decoration: none; }
.event__title a:hover { text-decoration: underline; text-decoration-color: var(--accent); }
.event__meta { margin-top: 0.3rem; font-size: 0.8125rem; color: var(--ink-muted); }
.event__note { margin-top: 0.4rem; font-size: 0.75rem; font-weight: 600; color: var(--ink-muted); }
/* An olive chip. These notes say "Beginners welcome" or "Rs 500 entry" — they
   describe the event rather than ask for a click, which is exactly the split
   --accent-2 marks, and a filled chip separates them from the plain muted note
   above without borrowing the button's colour. Ink on the olive is 4.9:1;
   white would be 3.8:1, so this is the side of the pairing that works.
   inline-block so the fill wraps the words rather than the column. */
.event__note--accent {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  background: var(--accent-2);
  color: var(--ink);
}

/* Three-across variant used on the home page. */
.events--columns { display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid var(--rule); }
.events--columns .event { border-bottom: 0; border-right: 1px solid var(--rule); padding-inline: 1.25rem; }
.events--columns .event:first-child { padding-left: 0; }
.events--columns .event:last-child { border-right: 0; }
@media (max-width: 860px) {
  .events--columns { grid-template-columns: 1fr; }
  .events--columns .event { border-right: 0; border-bottom: 1px solid var(--rule); padding-inline: 0; }
  .events--columns .event:last-child { border-bottom: 0; }
}

/* ---------------------------------------------------------------------------
   Page header (interior pages)
   --------------------------------------------------------------------------- */
.page-head { background: var(--paper); border-bottom: 1px solid var(--rule); padding-block: clamp(2rem, 5vw, 3.5rem); }
.page-head__title { font-size: var(--step-3); letter-spacing: var(--track-3); margin-block: 0.9rem 1rem; max-width: 16ch; }

/* The trail's own strip, against the foot of the masthead, at the client's
   word on 2026-09-10. It was the first line inside .page-head until then,
   which put it at a different height on every page and dropped it entirely on
   the pages that draw no banner.

   Three decisions in four declarations:

   - It takes --paper, the masthead's own ground, at the client's word. A
     --panel strip was tried first and read as a band belonging to the page;
     on the header's ground the two are one piece of chrome with a hairline
     between them, which is what "attached to the navbar" asks for. Ink-muted
     on --paper is 5.5:1.
   - Which makes its own bottom rule load-bearing rather than decoration: with
     the header, the strip and the page all on one ground, the two hairlines
     are the only thing saying where the chrome stops. It matters most on
     /shop, whose head is bare and draws no rule of its own.
   - It is NOT sticky. The masthead is, and "attached" here means adjoining it
     at rest rather than travelling with it: the header was cut from 119px to
     57px on 2026-09-09 precisely to stop the chrome eating a phone screen,
     and a second sticky strip would put 43px of it straight back for a line
     nobody reads twice. */
.crumb-bar {
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  padding-block: 0.5rem;
}

.breadcrumbs { font-size: 0.75rem; color: var(--ink-muted); }
/* align-items: baseline, because the links carry vertical padding for the
   tap target and the separator does not. Stretched, each item's text sat
   where its own box began and the slash rode 4px above the words. */
.breadcrumbs ol {
  list-style: none;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
}
.breadcrumbs li + li::before { content: "/"; margin-right: 0.4rem; color: var(--rule); }
.breadcrumbs a { text-decoration: none; }
.breadcrumbs a:hover { text-decoration: underline; text-decoration-color: var(--accent); }

/* ---------------------------------------------------------------------------
   Prose + card lists (placeholder pages)
   --------------------------------------------------------------------------- */
.prose { max-width: 62ch; }
.prose > * + * { margin-top: 1.1rem; }
.prose h2 { font-size: var(--step-1); margin-top: 2.25rem; }
.prose h3 { font-size: 1.0625rem; }
.prose section > * + * { margin-top: 0.4rem; }
.prose ul { padding-left: 1.1rem; }
.prose li + li { margin-top: 0.4rem; }
/* Ink, not orange: prose links are body-sized. They keep the underline the
   reset leaves on them, which is what marks them as links, and the accent
   arrives on hover as the decoration colour. */
.prose a { color: var(--ink); }

/* ---------------------------------------------------------------------------
   About blocks

   A statement on the left, the paragraphs that back it up on the right, with a
   rule between one block and the next. The shop's own idiom for this is
   .page-head split and .admin-toolbar: a heading and its detail beside each
   other rather than stacked, so the eye has somewhere to rest on a wide screen
   instead of reading one 62ch column all the way down.

   The eyebrow is accent type, which the note in the tokens calls an exception
   and is the third on the site after the semantic inks and the masthead's
   current section. It is derived rather than assumed, because it moves the day
   either token moves: b0501b on fffdf9 is 5.16 to 1, clear of the 4.5 body
   type needs. These blocks are therefore paper only. On panel the same pair is
   4.57 to 1, which passes but with nothing spare, so a block moved onto the
   alternate band wants the sum re-checked rather than assumed to carry over.
   --------------------------------------------------------------------------- */
.about-block { display: grid; gap: 1.25rem clamp(2rem, 5vw, 4rem); align-items: start; }
.about-block + .about-block {
  margin-top: clamp(2.5rem, 6vw, 4.5rem);
  padding-top: clamp(2.5rem, 6vw, 4.5rem);
  border-top: 1px solid var(--rule);
}
.about-block__eyebrow {
  font-family: var(--font-eyebrow);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0;
}
.about-block__title {
  font-size: var(--step-3);
  letter-spacing: var(--track-3);
  /* Short enough to wrap into two or three lines of its own, which is what
     makes it read as a statement rather than as a section heading. */
  max-width: 11ch;
  margin-block: 0.85rem 0;
}
.about-block .hero__actions { margin-top: 1.6rem; }
@media (min-width: 60rem) {
  .about-block { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

.link-grid {
  display: grid;
  /* auto-FIT, not auto-fill. Fill leaves the tracks it could not fill standing,
     and this grid paints its gaps by showing --rule through them — so four
     links in a row with space for five drew a full-height grey cell with
     nothing in it, which reads as a card that failed to load. Fit collapses
     the empty tracks and the cards take the width back. Reported off a
     screenshot on 2026-09-09; /events and the 404 render this too. */
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  list-style: none;
  padding: 0;
}
.link-card { background: var(--surface); padding: 1.25rem; }
.link-card__title { font-size: 1.0625rem; }
.link-card__title a { text-decoration: none; }
.link-card__title a::after { content: " →"; color: var(--ink-muted); }
.link-card__title a:hover { text-decoration: underline; text-decoration-color: var(--accent); }
.link-card p { margin-top: 0.4rem; font-size: 0.875rem; color: var(--ink-muted); }

/* A quiet aside at the foot of a section: the caveat, not the copy. */
.note {
  margin-top: 2rem;
  padding: 1rem 1.25rem;
  border-left: 3px solid var(--accent);
  background: var(--panel);
  font-size: 0.875rem;
  color: var(--ink-muted);
  max-width: 62ch;
}

/* ---------------------------------------------------------------------------
   The blog (2026-09-15, to the client's reference)

   Three blocks: a featured post beside a rail of the latest, a scrolling row
   of cards under it, and the post itself set in .prose.

   The reference is a rounded, drop-shadowed SaaS layout. What is taken from it
   is the *arrangement* — the two-thirds/one-third top row, the title written
   across the foot of the photograph, the thumbnail rows, the card row with its
   two arrows. What is not taken is its geometry: this site draws squared
   corners and hairlines, has five border-radius declarations in four thousand
   lines, and has no drop shadow on any surface. A rounded card here would be
   the only one on the site.

   Every chip is .card-flag, the site's own, for the same reason.
   --------------------------------------------------------------------------- */

/* The top row. Two thirds and one third above 60rem, stacked below, which is
   where a 22rem rail stops being a column and starts being a squeeze. */
.blog-top { display: grid; gap: clamp(1.5rem, 3vw, 2.5rem); }
@media (min-width: 60rem) {
  .blog-top { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); align-items: start; }
}

/* ---- the featured post ---- */

.blog-lead { position: relative; display: block; text-decoration: none; color: inherit; }
/* The scrim, and it is not decoration.

   White type over an uploaded photograph is white type over whatever the shop
   photographs next — a pale table, a bright window — so the contrast cannot be
   left to the picture. This reaches --ink at 0.86 by the bottom, which is
   13.1:1 against white, and the placeholder's own tint sits under the same
   wash. It starts at 38% of the height so the top of the picture is untouched.

   A pseudo-element rather than a background on the panel, because the panel is
   only as tall as its words and the fade has to begin well above them. */
.blog-lead::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    transparent 38%,
    color-mix(in srgb, var(--ink) 55%, transparent) 68%,
    color-mix(in srgb, var(--ink) 86%, transparent) 100%
  );
  pointer-events: none;
}
.blog-lead__panel {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  /* Above the scrim its own pseudo-element paints. */
  z-index: 1;
  padding: clamp(1.25rem, 3vw, 2rem);
  color: #fff;
}
.blog-lead__title {
  font-size: var(--step-2);
  letter-spacing: var(--track-2);
  line-height: 1.12;
  max-width: 26ch;
  margin-top: 0.75rem;
}
.blog-lead__meta { margin-top: 0.6rem; font-size: 0.8125rem; color: rgba(255, 255, 255, 0.82); }
/* The credit chip is bottom-left on every other photograph on the site, which
   is exactly where this panel now is. Moved to the top of the picture on this
   card only — the same scoped override the product stage takes, and for the
   same reason: the chip has to stay legible and stay off the words. */
.blog-lead .photo__credit { top: 0; bottom: auto; }
.blog-lead:hover .blog-lead__title { text-decoration: underline; text-decoration-color: #fff; }

/* ---- the rail beside it ---- */

.blog-latest__head { font-size: var(--step-1); letter-spacing: var(--track-1); }
.blog-latest__list {
  list-style: none;
  padding: 0;
  margin-top: 1rem;
  display: grid;
  /* A hairline between rows rather than a gap, which is what makes four short
     rows read as one list instead of four floating pairs. */
  gap: 0;
}
.blog-latest__list li + li { border-top: 1px solid var(--rule); }
.blog-row {
  display: grid;
  grid-template-columns: 4rem minmax(0, 1fr);
  gap: 0.85rem;
  align-items: start;
  padding-block: 0.85rem;
  text-decoration: none;
  color: inherit;
}
.blog-row__shot { width: 4rem; }
.blog-row__title {
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: 0;
}
.blog-row__meta { margin-top: 0.3rem; font-size: 0.75rem; color: var(--ink-muted); }
.blog-row:hover .blog-row__title { text-decoration: underline; text-decoration-color: var(--accent); }

/* ---- founders corner ---- */

.founders__head {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.5rem;
}
.founders__lede { margin-top: 0.35rem; color: var(--ink-muted); font-size: 0.9375rem; }
.founders__nav { display: flex; gap: 0.5rem; }
/* The one round thing on the site, and it is round because an arrow in a
   square reads as a button in a toolbar rather than as a pager on a rail. They
   are anchors, not buttons: each one jumps the rail to an end. 44px because
   they are standalone targets under a thumb. */
.founders__arrow {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--rule-strong);
  border-radius: 50%;
  text-decoration: none;
  color: var(--ink);
  line-height: 1;
}
.founders__arrow:hover { background: var(--ink); color: var(--paper); }

.founders__rail {
  /* Named, because the scrolling variant has to subtract it to work out how
     wide a card must be for exactly three to fit. */
  --rail-gap: clamp(1rem, 2vw, 1.5rem);
  list-style: none;
  padding: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--rail-gap);
}
/* Past three cards the row scrolls instead of squeezing. Fixed-width tracks,
   because an auto track in a scroller collapses to its content.

   No scroll-behavior: smooth here, deliberately — trap 17. With it set, the
   browser does not scroll a fragment target into a scroller at all, so the two
   arrows would set the hash, move the page and leave the rail exactly where it
   was. That is the fault the hero's dots carried, unnoticed, for weeks. */
.founders__rail--scrolls {
  /* Exactly three across, and the fourth off the edge. A minmax() here sizes
     every track to its own minimum the moment the row overflows, which put
     four 256px cards in a 968px rail: it scrolled by 117px, the fourth card
     was three quarters visible, and the arrows had almost nothing to do. The
     width is computed from the rail instead, so three fill it at any size. */
  grid-auto-columns: calc((100% - var(--rail-gap) * 2) / 3);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* The gap is inside the scroller, so the last card needs padding to clear
     the shell's own gutter when it is scrolled to. */
  padding-bottom: 0.5rem;
}
.founders__slide {
  scroll-snap-align: start;
  /* An anchor jump scrolls the page as well as the rail, and the masthead is
     sticky at 57px. Without this the card's own title is what goes under it. */
  scroll-margin-block-start: 5rem;
  scroll-margin-inline-start: 0;
}

.post-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--surface);
  border: 1px solid var(--rule);
  text-decoration: none;
  color: inherit;
}
/* --surface on the --panel band this section sits in. The other way round —
   a white card on --paper — is 1.01:1, which is the sum that stopped .section
   being white in the 2026-09-09 redesign and is why this band is --panel. */
.post-card:hover { border-color: var(--rule-strong); }
.post-card:hover .post-card__title { text-decoration: underline; text-decoration-color: var(--accent); }
.post-card__body { display: flex; flex-direction: column; flex: 1; padding: clamp(1rem, 2vw, 1.25rem); }
.post-card .card-flag { align-self: start; margin-bottom: 0.75rem; }
.post-card__title { font-size: var(--step-1); letter-spacing: var(--track-1); line-height: 1.2; }
.post-card__excerpt { margin-top: 0.6rem; color: var(--ink-soft); font-size: 0.9375rem; }
/* Pushed to the foot, so the datelines line up across a row of cards whose
   titles wrap to different numbers of lines. */
.post-card__meta { margin-top: auto; padding-top: 1rem; font-size: 0.75rem; color: var(--ink-muted); }

/* The accent chip. Every other chip on the site is a fill and this is one too;
   white on --accent is 5.2:1, which clears the bar for the small bold type
   .card-flag sets. */
.card-flag--blog { background: var(--accent); color: #fff; }

/* Two across where three would put the excerpt in a 200px column. */
@media (max-width: 60rem) {
  .founders__rail--scrolls { grid-auto-columns: calc((100% - var(--rail-gap)) / 2); }
}
@media (max-width: 46rem) {
  /* One card at a time, most of the width, so the next one peeks in at the
     edge — which is the only affordance a rail has with nothing running. */
  .founders__rail--scrolls { grid-auto-columns: 82%; }
  .blog-lead__title { font-size: var(--step-1); }
  /* The arrows are a pointer control on a row a thumb already swipes, and at
     this width they cost a line above the rail for nothing. */
  .founders__nav { display: none; }
}

/* ---- the post itself ----

   .prose already carries the 62ch measure, the rhythm and the heading sizes,
   so this adds only what a long-form page wants that a placeholder page does
   not: a step up in body size, because 62ch of 16px read for four minutes is
   not the same ask as three paragraphs on /about, and a soft ink for the same
   reason the tokens give one. */
.post__body { font-size: var(--step-1); color: var(--ink-soft); }
/* The headings stay full-strength ink against the softened body, which is what
   makes them read as structure rather than as slightly larger prose. */
.post__body h2 { color: var(--ink); letter-spacing: var(--track-1); }

/* ---------------------------------------------------------------------------
   Search + cart
   --------------------------------------------------------------------------- */
.search-form { display: flex; gap: 0.5rem; flex-wrap: wrap; max-width: 34rem; margin-top: 1.25rem; }
.search-form input {
  flex: 1 1 16rem;
  padding: 0.8rem 1rem;
  border: 1px solid var(--ink);
  background: var(--surface);
}
.search-form input::placeholder { color: var(--ink-muted); }

/* The heading over the recommended shelf, when nothing has been asked yet. */
.search-suggest__title { font-size: var(--step-2); letter-spacing: var(--track-2); margin: 1.5rem 0 1.5rem; }

/* ---------------------------------------------------------------------------
   One game's page

   Photographs on the left at a comfortable reading width, the facts and the
   buy control on the right. Below 52rem it stacks, image first — on a phone
   the picture is what tells you whether you have the right game.
   --------------------------------------------------------------------------- */
/* ---------------------------------------------------------------------------
   The product page (client design, 2026-09-03)

   Photographs left, one white card right, and the card is sticky so the price
   and the button stay with you down a long column of pictures. Everything here
   is drawn from the tokens at the top of this file — the design was made
   against them, so nothing new was needed but the arrangement.
   --------------------------------------------------------------------------- */
/* The two columns end on the same line, at the client's word on 2026-09-15.

   Stretch rather than start, and the card is what gives: the row is as tall as
   the taller of the two, and the white box grows into it. Where the photograph
   is the taller — which is every width wide enough for the card's facts to sit
   on one row — that is the whole of it, and the two end level to the pixel.

   The photograph is deliberately not the one that grows. Stretching the frame
   was built first and the client called it: the stage shows a picture whole
   rather than cropping it (2026-09-03), so a frame taller than its picture is
   a small photograph in a large mount. The card has no such problem — more of
   a white box is just more white box. */
.product {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  align-items: stretch;
  gap: clamp(1.75rem, 3.5vw, 3.75rem);
  margin-top: clamp(1.25rem, 2.5vw, 2.125rem);
}
/* The panel is the grid item and the card is inside it, so the card has to be
   told to take the height the panel was given. */
.product__panel { display: flex; flex-direction: column; }
.product__card { flex: 1 1 auto; }
/* One column before the card gets too narrow to hold a price and a button on
   one line. The photographs lead, which is the order somebody reads in. */
@media (max-width: 62rem) { .product { grid-template-columns: minmax(0, 1fr); } }

.product__media { display: flex; flex-direction: column; gap: 0.875rem; }

/* The gallery is a rail of thumbnails beside the stage rather than a row of
   them under it, which is the arrangement every large shop uses and it is a
   scaling decision rather than a stylistic one: a column costs one
   thumbnail's width whatever the count, where a row grows a line at a time
   and pushes the price and the Add button further down the page with every
   photograph the shop adds. Past what fits, the rail scrolls; the stage does
   not move.

   The stage is second in the markup and first in the reading order, so it is
   placed explicitly rather than by source position. */
.product__gallery { display: grid; gap: 0.75rem; }
.product__gallery[data-gallery] {
  grid-template-columns: 5.25rem minmax(0, 1fr);
  align-items: stretch;
}
.product__gallery[data-gallery] > .product__stage { grid-column: 2; grid-row: 1; }
.product__rail { position: relative; grid-column: 1; grid-row: 1; min-height: 0; }

.product__stage { position: relative; background: var(--surface); border: 1px solid var(--rule); }
.product__stage .photo { --photo-ratio: 4 / 3; }
/* The stage is the one box on the site that shows a photograph whole. Every
   other one is cover, because a card in a grid has to be the same shape as
   the card beside it; here there is nothing to line up with, and a shop
   photographing a tall box or a wide board should not have the edges of it
   taken off to fit our 4/3. So the box keeps its ratio — the column is sticky
   and the rail is measured against it, both of which need a stage that does
   not change height between shots — and the picture fits inside it.

   Two things follow from contain. The crop no longer applies, since nothing
   is being cut off, so --focal is overridden back to the centre rather than
   left to push a fitted picture into a corner. And the tinted wash behind it
   would show through the letterbox as a coloured border, so a stage holding a
   real photograph gets a flat mount; the placeholder, which is the div with
   role="img" and no picture in it, keeps the wash it is there to draw. */
.product__stage .photo img { object-fit: contain; object-position: 50% 50%; }
.product__stage .photo:not([role="img"]) { background: var(--surface); background-image: none; }
/* The label chip takes the bottom-left corner the credit chip used to have, so
   on the rare shot carrying a credit the two are stacked rather than one
   sitting on the other. Scoped here; every other credit chip is unmoved. */
.product__stage .photo__credit { top: 0; bottom: auto; }

.product__shot-label {
  position: absolute;
  left: 0;
  bottom: 0;
  margin: 0;
  padding: 0.4375rem 0.75rem;
  max-width: 75%;
  background: var(--ink);
  color: #fff;
  font-family: var(--font-eyebrow);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.product__shot-count {
  position: absolute;
  right: 0;
  bottom: 0;
  margin: 0;
  padding: 0.4375rem 0.75rem;
  background: rgb(255 255 255 / 0.92);
  color: var(--ink-muted);
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
}

/* Absolutely positioned inside the rail, which is why the rail has no height
   of its own: a grid row sizes to the tallest item, so a scroller measured
   normally would make twelve thumbnails set the height of the row and the
   stage would sit in the middle of a very tall column. Out of flow, the
   stage alone decides how tall the row is and the rail is stretched to
   exactly that — the strip is always the height of the photograph beside it,
   whether it holds two thumbnails or twenty. */
.product__thumbs {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
  position: absolute;
  inset: 0;
  overflow-y: auto;
  /* The gutter is held whether or not this gallery is long enough to scroll,
     so a thumbnail is the same size on a game with three photographs as on
     one with twelve. Without it the bar takes 10px off the rail the moment a
     shop adds the photograph that overflows it, and every tile in the column
     gets smaller. */
  scrollbar-gutter: stable;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}
.product__thumb {
  position: relative;
  display: block;
  border: 1px solid var(--rule);
  background: var(--surface);
}
/* The ring marks the shot on the stage. An inset shadow rather than a border,
   so the tile does not shift by a pixel as it becomes current. A thumbnail
   changes the stage when it is pressed and not when it is pointed at, so
   hover has to say "this is a control" on its own — it is no longer backed up
   by the picture changing. */
.product__thumb[aria-current="true"] { box-shadow: inset 0 0 0 2px var(--accent); }
.product__thumb:not([aria-current="true"]) .photo { opacity: 0.72; }
.product__thumb:hover .photo,
.product__thumb:focus-visible .photo { opacity: 1; }
.product__thumb:hover,
.product__thumb:focus-visible { border-color: var(--accent); }

/* On a phone a 4.5rem rail is a fifth of the screen taken from the
   photograph, so the strip goes back under the stage — as one scrolling row
   rather than a wrapping grid, so that a game with ten photographs costs the
   same vertical space as one with three and the price stays where it was. */
@media (max-width: 46rem) {
  .product__gallery[data-gallery] { grid-template-columns: minmax(0, 1fr); }
  .product__gallery[data-gallery] > .product__stage { grid-column: 1; grid-row: 1; }
  .product__rail { position: static; grid-column: 1; grid-row: 2; }
  .product__thumbs {
    position: static;
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: 4.25rem;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-gutter: auto;
    scroll-snap-type: x proximity;
  }
  .product__thumb { scroll-snap-align: start; }
}

/* Nothing here is sticky, at the client's word on 2026-09-15.

   It was: the photographs held while the taller column ran past them, which
   is the arrangement every large shop uses and was decided on 2026-09-03.
   What the client asked for instead is the two columns balanced and the
   section still — so the picture keeps its own 4/3 shape and the card is
   brought down to meet it, rather than either box being stretched to the
   other.

   Stretching the frame to the card's height was tried first and looked
   wrong, which is worth keeping: the stage shows a picture whole rather than
   cropping it (2026-09-03), so a frame as tall as a column measured by a
   price and a button is a small photograph in a large mount. The fix for a
   gap between two columns is the taller one, not the shorter one. */

.product__card {
  background: var(--surface);
  border: 1px solid var(--rule);
  padding: clamp(1.375rem, 2.2vw, 2.125rem);
}

/* The name, with the shelves it sits on ranged right of it.

   Baseline rather than centre, so the links sit on the line the title is
   written on however many lines that title takes; wrap, so a long name keeps
   the room it needs and the links drop below it rather than squeezing it. The
   title is the flex item that gives, which is why only it carries min-width. */
.product__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.35rem 1rem;
}
.product__head .product__title { flex: 1 1 auto; min-width: 0; }
.product__shelves {
  flex: 0 0 auto;
  margin: 0;
  font-size: 0.875rem;
  text-align: right;
}
.product__title {
  margin: 0;
  font-size: clamp(2rem, 2vw + 1.5rem, 3rem);
  letter-spacing: var(--track-2);
  line-height: 1.03;
}
.product__byline { margin: 0.5rem 0 0; color: var(--ink-muted); font-size: 0.875rem; }

/* Players, time, age and complexity, banded off by a rule top and bottom. */
.spec {
  margin: 1.375rem 0 0;
  padding: 1rem 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(5.25rem, 1fr));
  gap: 1rem;
}
.spec dt {
  font-family: var(--font-eyebrow);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.spec dd {
  margin: 0.3125rem 0 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.1875rem;
  letter-spacing: var(--track-1);
}
.spec__complexity { display: flex; align-items: center; gap: 0.4375rem; }
.spec__meter { display: flex; gap: 2px; }
.spec__cell { width: 9px; height: 9px; border: 1px solid var(--rule); }
.spec__cell--on { background: var(--accent-2); border-color: var(--accent-2); }
.spec__weight {
  font-size: 0.75rem;
  font-weight: 400;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

/* The air above and below the price is tighter than the card's other gaps, at
   the client's word on 2026-09-15: the card is being brought down towards the
   height of the photograph beside it, and this is space rather than anything
   anybody reads. The space *below* the price is the stock panel's own
   margin-top, so the two move together or only half of it moves. */
.product__price {
  margin-top: 0.75rem;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.75rem;
}
.product__now {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  /* A step down from clamp(2rem, 1.4vw + 1.6rem, 2.6rem), at the client's
     word: it is the tallest line in the card and the card is being brought
     down to the photograph beside it. Still the largest thing in the card by
     a clear margin, which is what the price has to be. */
  font-size: clamp(1.75rem, 1vw + 1.45rem, 2.125rem);
  letter-spacing: -0.028em;
}
/* Red only when it is genuinely reduced, and never the only signal — the
   struck-through full price and the word save carry it too. */
.product__now--sale { color: var(--sale-ink); }
.product__was { margin: 0; color: var(--ink-muted); font-size: 1.0625rem; }

/* The stock panel. What the state is, then the fact, then the thing to do. */
.stockbox {
  /* The other half of the price's air; see the note above .product__price. */
  margin-top: 0.75rem;
  border: 1px solid var(--ink);
  background: var(--panel);
  padding: 1.125rem 1.25rem;
}
.stockbox__status {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.5625rem;
  font-family: var(--font-eyebrow);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}
/* A second mark beside a word that already says it, which is the rule every
   semantic ink on this site follows. */
.stockbox__dot { width: 8px; height: 8px; border-radius: 50%; background: currentcolor; flex: none; }
.stockbox__status--ok { color: var(--ok-ink); }
.stockbox__status--warn { color: var(--warn-ink); }
/* An empty shelf, in the same red the card's chip uses. It was --warn-ink,
   which is the amber this site keeps for true-but-awkward — a few copies
   left, a slow restock. Nothing being there at all is not awkward, it is the
   answer, and the client asked for it to read as one. */
.stockbox__status--out { color: var(--sale-ink); }
.stockbox__head {
  margin: 0.5625rem 0 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.3125rem;
  letter-spacing: var(--track-2);
  line-height: 1.15;
}
.stockbox__note { margin: 0.5rem 0 0; font-size: 0.875rem; color: var(--ink-muted); max-width: 44ch; }
.stockbox__act {
  margin: 1rem 0 0;
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0.625rem;
}
.stockbox__qty {
  width: 4.5rem;
  text-align: center;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  border: 1px solid var(--ink);
  background: var(--surface);
  padding: 0.5rem;
}
.btn--buy { padding: 1rem 1.875rem; font-size: 1.0625rem; }
.stockbox__foot { margin: 0.75rem 0 0; font-size: 0.75rem; color: var(--ink-muted); }


/* Publisher, designer, edition and the shelves it sits on: label left, value
   right, a hairline under each. */
.product__facts { margin: 1.5rem 0 0; display: grid; gap: 0.5rem; }
.product__facts > div {
  display: flex;
  gap: 0.75rem;
  justify-content: space-between;
  align-items: baseline;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 0.4375rem;
}
.product__facts dt {
  font-family: var(--font-eyebrow);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.product__facts dd { margin: 0; font-size: 0.875rem; text-align: right; }


/* The ratings band, below everything that helps somebody decide to buy. */
.ratings {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: clamp(1.5rem, 4vw, 4.5rem);
}
.ratings__say { flex: 1 1 24rem; max-width: 52ch; }
.ratings__eyebrow {
  margin: 0;
  font-family: var(--font-eyebrow);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.ratings__title {
  margin: 0.625rem 0 0;
  font-size: clamp(1.5rem, 0.9vw + 1.3rem, 2rem);
  letter-spacing: var(--track-2);
  line-height: 1.06;
}
.ratings__note { margin: 0.625rem 0 0; color: var(--ink-muted); }
.ratings__card { flex: 0 1 24rem; border: 1px solid var(--rule); padding: 1.375rem 1.5rem; }
.ratings__card .rate { margin-top: 0; }

.cart-table { width: 100%; border-collapse: collapse; margin-top: 1.5rem; }
.cart-table caption { text-align: left; font-size: 0.8125rem; color: var(--ink-muted); padding-bottom: 0.75rem; }
.cart-table th, .cart-table td { text-align: left; padding: 0.9rem 0.75rem 0.9rem 0; border-bottom: 1px solid var(--rule); }
/* Only the column headings get the small-caps treatment. A row header is a
   game title and has to read like one. */
.cart-table thead th {
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
  border-bottom-color: var(--rule-strong);
}
.cart-table tbody th { font-weight: 600; }
.cart-table td:last-child, .cart-table th:last-child { text-align: right; padding-right: 0; }
.cart-total td, .cart-total th { font-weight: 700; border-bottom: 0; }
.cart-table tfoot td, .cart-table tfoot th { border-bottom: 0; padding-block: 0.35rem; }
.cart-table__sub { display: block; font-size: 0.8125rem; color: var(--ink-muted); font-weight: 400; margin-top: 0.15rem; }
.cart-table__sub--warn { color: var(--accent); }

/* The row an update came back to: a little air above it so it does not land
   flush against the top of the window, and a tint so the eye finds the line
   it just changed without having to re-read the basket. */
.cart-table tbody tr { scroll-margin-top: 2rem; }
.cart-table tbody tr:target { background: var(--surface); }

/* Quantity and its button sit on one line; the input stays thumb-sized. The
   hint wraps under them when it shows, which is why the form may wrap. */
/* align-items: stretch, not center. The field takes body type and the
   button takes small, so centred they were two boxes of different heights
   pretending to be one control. Stretched, the shorter one grows to the
   taller — and it keeps working under (pointer: coarse), where the button
   claims a 44px minimum and the field follows it up. The hint below is its
   own flex line and is unaffected. */
.cart-qty { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: stretch; }
.cart-qty input { width: 4.5rem; padding: 0.45rem 0.5rem; text-align: right; }

/* Nothing here watches the number change — but focus is a state CSS can see,
   so while somebody is in the field the Update button turns primary and a
   one-line hint appears. It is the closest a form can get to "you have
   unsaved changes", and it costs nothing where it is unsupported: the button
   still reads Update either way.

   Visibility rather than display, which is what it used to be. Toggling the
   hint in and out of the layout made the row taller the moment the field took
   focus, shoving every line below it — and the price being read — down the
   page while somebody was typing into it. The line is always there and always
   takes its space; only the ink appears. */
.cart-qty__hint {
  flex-basis: 100%;
  font-size: 0.75rem;
  color: var(--ink-muted);
  visibility: hidden;
}
.cart-qty:focus-within .cart-qty__hint { visibility: visible; }
.cart-qty:focus-within .cart-qty__save {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* Ranged right, under the column the totals are in. On the left they sat
   below the picture and the title, a long way from the figure somebody has
   just read and is deciding about. */
.cart-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.75rem;
  margin-top: 2rem;
}

.cart-note { margin-top: 1.25rem; font-size: 0.9375rem; color: var(--ink-muted); }
.cart-note--warn {
  color: var(--ink);
  border-left: 3px solid var(--accent);
  background: var(--surface);
  padding: 0.85rem 1rem;
}
/* A pre-order is not a problem, so it does not get the warning's orange. Blue
   is the token for "neither good nor bad news, but worth reading". */
.cart-note--preorder {
  color: var(--ink);
  border-left: 3px solid var(--info-ink);
  background: var(--surface);
  padding: 0.85rem 1rem;
  margin-top: 1rem;
}
.cart-table__sub--preorder { color: var(--info-ink); font-weight: 600; }
.cart-note--warn ul { margin: 0.4rem 0 0; padding-left: 1.1rem; }

/* The picture, the title and the each-price, side by side in the first cell.
   The shot is fixed-width so every row lines up regardless of how long the
   title is, and it shrinks rather than wraps on a narrow screen. */
.cart-item { display: flex; align-items: center; gap: 0.9rem; }
.cart-item__shot { flex: 0 0 4.5rem; display: block; }
.cart-item__shot .photo { margin: 0; }
.cart-item__body { display: flex; flex-direction: column; min-width: 0; }

/* A photograph beside each line of the checkout summary, at the size where it
   still says which box it is and no larger — this column is about the money. */
.checkout-line { display: flex; align-items: center; gap: 0.6rem; min-width: 0; }
.checkout-line__shot { flex: 0 0 auto; width: 2.5rem; height: 2.5rem; object-fit: cover; }

/* On a phone the cart stops being a table.

   Four columns — title, a quantity form, a price and a remove button — need
   about 410px of width, which is more than the phone most customers here are
   holding. Rather than let the page scroll sideways, each row becomes a block:
   the title on its own line, then the quantity control, the line total and
   remove sharing the next one. The column headings go, because each cell now
   says what it is. */
@media (max-width: 40rem) {
  .cart-table thead { display: none; }

  .cart-table tbody tr,
  .cart-table tfoot tr {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
  }
  .cart-table tbody tr { padding-block: 0.9rem; border-bottom: 1px solid var(--rule); }

  .cart-table tbody th { flex: 1 1 100%; padding: 0; border-bottom: 0; }
  .cart-item__shot { flex-basis: 3.5rem; }
  .cart-table tbody td { padding: 0; border-bottom: 0; text-align: left; }
  /* Remove sits at the far end, away from the quantity control it would
     otherwise be a mis-tap from. */
  .cart-table tbody td:last-child { margin-left: auto; }

  .cart-table tfoot tr { justify-content: space-between; padding-block: 0.15rem; }
  .cart-table tfoot th, .cart-table tfoot td { padding: 0; border-bottom: 0; }
  /* The spacer cells that keep the desktop columns aligned have nothing to
     say here and would otherwise take a share of the flex row. */
  .cart-table tfoot td:empty { display: none; }
  .cart-total { margin-top: 0.4rem; padding-top: 0.5rem; border-top: 1px solid var(--rule-strong); }
}

/* ---------------------------------------------------------------------------
   Checkout
   --------------------------------------------------------------------------- */
.checkout { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); gap: 2.5rem; margin-top: 1.5rem; align-items: start; }
.checkout__form fieldset { border: 0; padding: 0; margin: 0 0 1.5rem; }

/* Two fields to a row once the column is wide enough to hold two.

   Nine stacked boxes made a checkout a page you scroll rather than a page you
   fill in — the client's note was that the details do not fit in one view. The
   width was already there and unused, so it goes into a second column rather
   than into wider boxes: a phone number does not get easier to type at 700px.

   The legend needs no help. A fieldset renders it above the anonymous box
   that holds everything else, so it is not a grid item and cannot be pulled
   into a column.

   Everything in a fieldset spans both columns by default and only a field
   opts back into one, the way .admin-fieldset does upstairs. That way the
   shipping choice, its paragraph and any note dropped in later keep the full
   width they were drawn at, and the pairing stays a decision taken per field.

   The breakpoint is the form column's width, not the window's: at 62rem the
   left column is around 530px, which is two boxes of 255. Below that they
   stack, and the tighter spacing above still shortens the page. */
@media (min-width: 62rem) {
  .checkout__form fieldset {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 1.25rem;
    align-items: start;
  }
  .checkout__form fieldset > * { grid-column: 1 / -1; }
  .checkout__form fieldset > .checkout-field { grid-column: auto; }
  .checkout__form fieldset > .checkout-field--wide { grid-column: 1 / -1; }
}
.checkout__form legend {
  font-family: var(--font-eyebrow);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
  padding: 0 0 0.75rem;
}
.checkout-field { margin-bottom: 0.9rem; }
.checkout-field label { display: block; font-weight: 600; font-size: 0.9375rem; margin-bottom: 0.3rem; }
.checkout-field input {
  width: 100%;
  padding: 0.7rem 0.75rem;
  border: 1px solid var(--rule-strong);
  background: var(--surface);
  font: inherit;
  color: inherit;
}
.checkout-field--invalid input { border-color: var(--accent); }
.checkout-field__hint { display: block; font-size: 0.8125rem; color: var(--ink-muted); margin-top: 0.3rem; }
.checkout-field__error { display: block; font-size: 0.8125rem; color: var(--accent); margin-top: 0.3rem; }
.checkout__terms { font-size: 0.875rem; color: var(--ink-muted); margin-bottom: 1.25rem; }

.checkout__summary { background: var(--surface); border: 1px solid var(--rule); padding: 1.5rem; }
.checkout__summary h2 { font-size: 1rem; margin: 0 0 1rem; }
.checkout__summary ul { list-style: none; margin: 0 0 1rem; padding: 0; }
.checkout__summary li { display: flex; justify-content: space-between; gap: 1rem; padding: 0.4rem 0; font-size: 0.9375rem; }
.checkout__summary dl { margin: 0; border-top: 1px solid var(--rule); padding-top: 0.75rem; }
.checkout__summary dl div { display: flex; justify-content: space-between; gap: 1rem; padding: 0.25rem 0; font-size: 0.9375rem; }
.checkout__summary dt, .checkout__summary dd { margin: 0; }
.checkout__total { font-weight: 700; border-top: 1px solid var(--rule); margin-top: 0.5rem; padding-top: 0.6rem !important; }

@media (max-width: 46rem) {
  .checkout { grid-template-columns: minmax(0, 1fr); gap: 1.75rem; }
  /* The summary leads on a narrow screen: what you are paying matters more
     than the form you have not filled in yet. */
  .checkout__summary { order: -1; }
}

/* ---------------------------------------------------------------------------
   Footer
   --------------------------------------------------------------------------- */
/* The bottom padding is half the top, at the client's word on 2026-09-10.
   Symmetric, it put 56px of panel under the legal line — the last row of the
   page, three short sentences with nothing after them — which reads as the
   footer having failed to end rather than as air. Half is the figure and not
   zero because this type still has to stand off the edge of the window; what
   went is the half that was only there to match the padding above. */
.site-footer {
  background: var(--panel);
  padding-block: clamp(2.5rem, 5vw, 3.5rem) clamp(1.25rem, 2.5vw, 1.75rem);
  border-top: 1px solid var(--rule);
}
/* The brand column, then one per nav group, then Reach us. It was repeat(3)
   until the Help group arrived with /faq on 2026-09-09 — a count typed here
   and a list written in config.ts are two answers to how many columns there
   are, and the day they disagreed the last group would have wrapped onto a
   row of its own under a grid with a hole in it. */
.site-footer__grid { display: grid; grid-template-columns: minmax(0, 1.4fr) repeat(4, minmax(0, 1fr)); gap: 2rem; }
/* The brand is the lockup rather than the name in type since 2026-09-10, so
   what is left here is the anchor around it. inline-flex because an inline
   anchor holding an image gets a line box's worth of space under it, which
   read as the blurb sitting a few pixels low against the columns beside it. */
.site-footer__brand { display: inline-flex; text-decoration: none; }
/* A rule of its own for the same reason the footer heading is not the
   masthead's: the day the foot of the page wants a bigger lockup than the top
   of it, this is where that is said. That day is 2026-09-10, at the client's
   word, so this is 40px where the masthead's is 34 on a wide screen and 28 on
   a phone. It is the only lockup on the page at that point, with the whole
   width of the brand column to sit in, where the header's shares a 58px row
   with the utilities. */
.site-footer__logo { width: auto; height: 40px; }
.site-footer__blurb { margin-top: 0.9rem; font-size: 0.875rem; color: var(--ink-muted); max-width: 34ch; }
/* The three contact marks under the blurb, at the client's word on
   2026-09-10. Icon-only links, so each anchor is a 40px square rather than the
   20px drawing inside it — WCAG 2.2 (2.5.8) asks for 24 and a mark with no
   word beside it is pressed by a thumb as often as by a pointer. The marks are
   the muted ink at rest and the accent under a pointer, which is the same
   answer the masthead's utilities give. */
.site-footer__social {
  list-style: none;
  padding: 0;
  margin-top: 1.1rem;
  display: flex;
  gap: 0.35rem;
  /* Exactly the inset the 20px mark sits at inside its 40px box — (2.5rem -
     20px) / 2 — so the first glyph starts on the same pixel as the lockup and
     the blurb above it rather than a rounding away from it. It moves with
     either figure. */
  margin-inline-start: -0.625rem;
}
.site-footer__social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  color: var(--ink-muted);
}
.site-footer__social a:hover,
.site-footer__social a:focus-visible { color: var(--accent); }
.site-footer__mark { display: block; }
.site-footer__heading {
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: 0.9rem;
}
/* Tight gap because the links themselves carry padding for the tap target. */
.site-footer__list { list-style: none; padding: 0; display: grid; gap: 0.1rem; font-size: 0.875rem; }
.site-footer__list a { text-decoration: none; }
.site-footer__list a:hover { text-decoration: underline; text-decoration-color: var(--accent); }
/* The column carries its own "Reach us" heading, so these do not need a colour
   of their own. A standing underline marks them as the actionable ones — mail,
   phone, profiles — against the plain nav links beside them, and the hover rule
   above then swaps that underline to the accent. */
.site-footer__list--contact a { text-decoration: underline; text-decoration-color: var(--rule); }
/* Two items since 2026-09-10 — the copyright at one end of the line and the
   city at the other — so this is space-between doing what it is actually for,
   rather than the near-miss it makes of a centred middle item. It still wraps:
   below about 46rem the two stack and both range left. */
.site-footer__legal {
  margin-top: 2.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
  font-size: 0.75rem;
  color: var(--ink-muted);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 1.5rem;
  justify-content: space-between;
}
/* Narrower than the five-column row, the brand takes the full width and the
   four groups pair off beneath it. Spanning it is what makes the pairs line
   up: as an ordinary cell it sat in the first column and pushed the groups
   into a stagger — Shop beside the lockup, then Events and Help, then Reach us
   alone on a row with a hole beside it. Now the rows are Shop | Events and
   Help | Reach us, which is the order they are written in. */
@media (max-width: 860px) {
  .site-footer__grid { grid-template-columns: repeat(2, 1fr); }
  .site-footer__intro { grid-column: 1 / -1; }
}
/* And it stays two columns on a phone, at the client's word on 2026-09-10:
   stacked, the four groups were twelve links in one column and the footer ran
   most of a screen on its own. Only the gutter changes — 2rem between two
   columns of a 375px screen is a tenth of the width spent on nothing, where
   the space between the rows is what keeps the groups apart and is left alone.

   The contact column is the one that can overflow: an email address has no
   break the browser will take on its own, so the words are allowed to break
   anywhere rather than run past the edge. It applies to the whole list because
   a long enough game title would do the same. */
@media (max-width: 480px) {
  .site-footer__grid { gap: 2rem 1.25rem; }
  .site-footer__list { overflow-wrap: anywhere; }
}

/* ---------------------------------------------------------------------------
   Thumbs

   The filter controls already decided this: they carry a 44px min-height and a
   comment about being tapped more often than clicked. Nothing around them got
   the same treatment, so one form shipped two answers about how big a thumb is
   — a 44px select with a 35px Apply button sitting next to it.

   Settled here, and only under a coarse pointer. What a mouse sees does not
   move. That is the point rather than a hedge: the storefront proportions were
   drawn, and a pointer query is how you leave a drawing alone on the surface it
   was drawn for while still being tappable on the one it was not. The admin
   sheet says as much where it scopes its own coarse block to .admin.

   The masthead links grow through a pseudo-element instead of padding. They
   carry hover and current-page state on a bottom border, and padding would push
   that rule away from the word it underlines; an inset overlay adds the hit
   area and moves nothing.

   No backticks anywhere in here: this stylesheet is one String.raw template
   literal, and a backtick in a comment ends it.
   --------------------------------------------------------------------------- */
@media (pointer: coarse) {
  .btn { min-height: 44px; }
  .btn--small { min-height: 44px; padding-block: 0.55rem; }

  /* The floor above is a height, and a height is only half of a thumb target
     on a button with no word in it: an 18px bag in 0.45rem of padding is 33px
     across whatever its height is. The square has to be squared in both
     directions or the card's Add is the one control on the shelf a thumb can
     miss sideways. */
  .btn--icon { min-width: 44px; }

  /* 16px is the threshold under which iOS Safari zooms the document on focus.
     At 14px, tapping the search box rescales the page and leaves the shopper
     pinching back out to see the results they just asked for. */
  .filters__field select,
  .filters__field input,
  .shop-sort select { font-size: 1rem; }
  .filters__check input { width: 1.35rem; height: 1.35rem; }

  .masthead__link,
  .masthead__utils a { position: relative; }
  .masthead__link::after,
  .masthead__utils a::after {
    content: "";
    position: absolute;
    /* Vertical is the axis that was short: 28px of link against a 44px
       thumb. Horizontal grows too, but only to -0.35rem, because the list
       gap bottoms out at 1rem and two overlays wider than half of it would
       meet in the middle. Overlapping targets are worse than narrow ones:
       the tap still lands, just on the neighbour. */
    inset-block: -0.6rem;
    inset-inline: -0.35rem;
  }
}

/* ---------------------------------------------------------------------------
   Motion
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------------------------------------------------------------------------
   The basket badge, and what happens after Add

   Both of these are per-visitor, and so is the cacheability of any page
   carrying them. Read the cacheability note in src/app.tsx before putting
   anything else per-visitor into the chrome.
   --------------------------------------------------------------------------- */
/* The word and its number need visible air between them — glued together the
   badge reads as part of the word rather than a count beside it. The gap is
   .masthead__util's, up in the header block, which is where the mark, the word
   and the count are spaced as one row; centred rather than on the baseline,
   because a mark aligned to the baseline of the word beside it sits high by
   most of its own descender. Nothing in this file may contain a backtick — the
   whole stylesheet is one template literal. */

/* Deliberately unstyled: the count is part of the words, "Cart (2)", not a
   disc stuck to a link. It keeps its own element because the script has to
   find it and put a new number in it after an add. */

/* The add-to-cart confirmation.

   It used to be a line of grey text on a grey strip, which said the right
   thing so quietly that the client's note about it was "not noticeable". It is
   the loudest thing on the page for as long as it takes to read: white against
   the paper, an accent rule down its edge, a drawn tick, and the two onward
   moves as buttons. The wording — what is in the basket, and how far off free
   delivery it is — comes from addedSummary() in site/data.ts.

   It was a band across the top of main, on the reasoning that a strip which
   scrolls away with the page has better manners than one sitting over the
   content, and that nothing here could dismiss itself with no script. The
   client asked for the standard thing instead: a card in the corner. The
   reasoning was answered rather than dropped — an Add pressed on the fourth
   row of the shelf was being confirmed a screenful above the button, which is
   a confirmation nobody sees. The script closes it and takes it away after a
   few seconds; with no script it lasts until the next navigation, which is as
   long as the band lasted anyway.

   It slides in where motion is welcome and simply appears where it is not. */
.added {
  position: fixed;
  /* Over everything, including the masthead it may briefly cover: the
     masthead is not sticky here, so at the top of a page the card lands on
     it, and it lets go of its own accord seven seconds later. The corner is
     the top one at the client's word — an Add pressed anywhere on the shelf
     is answered where the eye goes first. */
  z-index: 120;
  inset-block-start: 1.25rem;
  inset-inline-end: 1.25rem;
  width: min(23rem, calc(100vw - 2.5rem));
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 4px solid var(--accent);
  box-shadow: 0 0.75rem 2rem rgba(28, 25, 23, 0.22);
}
.added__inner {
  position: relative;
  display: grid;
  gap: 0.75rem;
  /* Room on the right for the close button, which is positioned into it. */
  padding: 1rem 2.4rem 1.05rem 1.1rem;
}
.added__body { display: grid; gap: 0.15rem; }
/* A hairline square rather than a button: it is the smallest control on the
   page and it must not compete with the two that matter underneath it. */
.added__close {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  width: 1.75rem;
  height: 1.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-muted);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
}
.added__close:hover { color: var(--ink); }
/* The refusal wears the same card. No tick — it is the one thing that did not
   happen — and no onward buttons, because there is nowhere to go on to. The
   left edge keeps the house accent rather than borrowing one of the semantic
   inks, which are drawn on type and nowhere else; the sentence is what says
   this is bad news. */
.added--refused .added__head::before { display: none; }
.added__reason { margin: 0; font-size: 0.9375rem; color: var(--ink-muted); }

/* A drawn tick rather than a character: ✓ renders in whatever the fallback
   font decides, and at this size the difference is visible. */
.added__head {
  display: flex;
  align-items: baseline;
  gap: 0.55rem;
  margin: 0;
  font-family: var(--font-display);
  /* A step down from the band's size: this is a card in the corner now, not
     a heading across the page. */
  font-size: 1.0625rem;
  font-weight: 400;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
.added__head::before {
  content: "";
  width: 0.5rem;
  height: 0.85rem;
  flex: none;
  border-right: 2.5px solid var(--accent-2);
  border-bottom: 2.5px solid var(--accent-2);
  transform: rotate(38deg) translateY(-0.15rem);
}
.added__tally {
  margin: 0;
  font-family: var(--font-eyebrow);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
/* The one line that changes somebody's mind about adding another game, so it
   gets the informational accent rather than the muted grey. */
.added__nudge { margin: 0; font-size: 0.9375rem; font-weight: 600; color: var(--ok-ink); }
/* How many copies actually exist, when that is not what was asked for. The
   informational blue, not the warning amber: a pre-order is what the customer
   chose, and the shelf being down to its last copy is news rather than a
   mistake. Never colour alone — the sentence itself says which it is. */
.added__stock { margin: 0; font-size: 0.9375rem; font-weight: 600; color: var(--info-ink); }
.added__actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; }

/* Down from over the corner it is anchored to. */
@keyframes added-in {
  from { opacity: 0; transform: translateY(-0.6rem); }
  to { opacity: 1; transform: none; }
}
.added { animation: added-in 220ms ease-out both; }
@media (prefers-reduced-motion: reduce) {
  .added { animation: none; }
}

/* Corner to corner on a phone: 23rem of card against a 375px window would
   leave a 10px margin down one side and look like a mistake. */
@media (max-width: 30rem) {
  .added {
    inset-inline: 0.65rem;
    inset-block-start: 0.65rem;
    width: auto;
  }
}

/* ---------------------------------------------------------------------------
   Print
   --------------------------------------------------------------------------- */
/* ---------------------------------------------------------------------------
   The shop heading, and the band of offers under it

   A shop's first screen belongs to the shop. This heading is a signpost:
   breadcrumbs, kicker, title one step down from the full-height banner, and
   the standfirst laid alongside the title rather than under it.

   It was the full banner with a four-block promo panel filling the half beside
   it, and between them they took the whole first screen — on a 1080-tall
   window the first game was below the fold on the page whose entire job is
   showing games. The facts from that panel were worth keeping and the height
   was not, so they are the band underneath: one line each, generated from the
   same catalogue, two lines tall instead of eight.
   --------------------------------------------------------------------------- */
.page-head--compact { padding-block: clamp(1rem, 2vw, 1.6rem); }
.page-head--compact .page-head__title {
  font-size: var(--step-2);
  letter-spacing: var(--track-2);
  /* The banner clamps to 16ch so a long title breaks into a stack. Here it is
     one line beside its own standfirst, and the clamp would break it for no
     reason. */
  max-width: none;
  margin-block: 0.2rem 0;
}
.page-head--compact .eyebrow { margin-top: 0.55rem; }

/* The banner with a document beside it: what happened on the left, the
   receipt for it on the right.

   The band takes the paper background rather than its usual white, because
   what it now holds is a white card — a receipt on --surface inside a band on
   --surface is a document with a hairline round it, and the whole point of
   that card is that it looks like a piece of paper you could hand somebody.

   One column below 64rem, which is where the two of them stop being readable
   side by side: the receipt's own lines table wants 30rem before its figures
   start wrapping, and a banner headline under 20rem is four words a line. */
.page-head--split { background: var(--panel); }
.page-head__split { display: grid; gap: 1.75rem; align-items: start; }
.page-head__aside { min-width: 0; }
@media (min-width: 64rem) {
  .page-head__split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
    gap: 2.5rem;
  }
  /* It is a column now, not a page, so the 16ch clamp that breaks a headline
     into a stack is doing the job the column already does. */
  .page-head--split .page-head__title { max-width: 18ch; }
  /* The card fills its column: the 46rem cap is there to stop a receipt
     spanning a whole desktop page, and the column is already narrower. */
  .page-head__aside .receipt { max-width: none; }
}

/* Breadcrumbs and nothing else. The band keeps its rule, because that rule is
   what separates the masthead from the shelf; it just stops being a banner.

   The space below is deliberately much smaller than the space above. With no
   banner in it, the padding on both sides was air around one line of 12px
   type — 49px of it between the breadcrumb and the count, on a page whose
   whole job is showing games. Above it is a margin; below it is a gap. */
.page-head--quiet { padding-block: clamp(0.85rem, 1.5vw, 1.1rem) clamp(0.35rem, 0.7vw, 0.5rem); }
/* A quiet head with no trail left in it. It carries the h1 for machines and
   nothing else, so it draws nothing: no ground, no rule, no space. The element
   is still here because the section below it keys its own top padding off
   .page-head--quiet, and because the heading has to live somewhere. */
/* Two class names deep on purpose. A media query carries no specificity of its
   own, so the .page-head--compact rule in the 46rem block near the foot of
   this file beats a single-class rule up here on source order alone — /shop's
   head is compact AND quiet AND bare, and on a phone it was drawing 11.2px
   above and 12.8px below a box with nothing in it. Same trap the perks strip
   hit from the other side in August. */
.page-head.page-head--bare { background: none; border-bottom: 0; padding-block: 0; }
/* The section's own opening space, halved again for the same reason. Only
   after a quiet head: every other banner has something in it that wants
   clearing. */
.page-head--quiet + .section { padding-top: clamp(0.6rem, 1.2vw, 0.9rem); }

/* Title and standfirst on one line, divided by a rule. Aligned on the bottom
   edge rather than the baseline: the left cell holds the kicker above the
   title, so its first baseline belongs to the kicker and aligning to it would
   hang the standfirst off the wrong line. */
.page-head__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.35rem 1.5rem;
}
.page-head--compact .lede {
  font-size: 0.9375rem;
  max-width: 48ch;
  padding-left: 1.5rem;
  border-left: 1px solid var(--rule);
}

@media (max-width: 60rem) {
  /* Stacked, the rule beside the standfirst is a rule between two rows. */
  .page-head--compact .lede { padding-left: 0; border-left: 0; }
}

/* ---------------------------------------------------------------------------
   Above the shelf: how many, and in what order

   Sort used to be the last of six controls at the foot of the filter rail,
   which is not where anyone looks for it — a shopper looks at the top of the
   results, beside the count, which is also where the count now lives instead
   of in a section heading repeating what the rail already said.

   Its own GET form, carrying the rest of the filter as hidden fields. The Go
   button is real: the script submits on change and hides it, and with no
   script it is a select and a button, which is a working sort.
   --------------------------------------------------------------------------- */
.shop-toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem 1.25rem;
  margin-bottom: 1rem;
}
.shop-toolbar__count { font-size: 0.9375rem; }
.shop-toolbar__of { color: var(--ink-muted); }
/* Only ever visible in the one-column layout, where the rail is below. */
.shop-toolbar__jump { display: none; }

.shop-sort { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
.shop-sort__label {
  font-family: var(--font-eyebrow);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.shop-sort select {
  min-height: 38px;
  padding: 0.35rem 0.6rem;
  padding-right: 2.1rem;
  border: 1px solid var(--rule-strong);
  background-color: var(--surface);
  font-size: 0.875rem;
}

@media (max-width: 60rem) {
  .shop-toolbar__jump {
    display: inline-flex;
    align-items: center;
    min-height: 38px;
    padding: 0.35rem 0.8rem;
    border: 1px solid var(--rule-strong);
    background: var(--surface);
    font-family: var(--font-eyebrow);
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    text-decoration: none;
  }
}

/* ---------------------------------------------------------------------------
   The shop on a phone

   Everything above the first game is a cost, and on a 375-wide screen the
   redesigned desktop head still spent 450px of an 812-tall window before a
   photograph: two lines of title, two of standfirst, four stacked offers, and
   a toolbar that wrapped. Each of those is cut here, and none of them by
   deleting anything — the kicker is the one exception, and it only ever said
   what the title beneath it says.
   --------------------------------------------------------------------------- */
@media (max-width: 46rem) {
  .page-head--compact { padding-block: 0.7rem 0.8rem; }
  /* "The whole shelf" over "Every game we stock" is the same sentence twice.
     It earns its line on a wide screen, where the line is free. */
  .page-head--compact .eyebrow { display: none; }
  .page-head--compact .page-head__title {
    font-size: 1.375rem;
    letter-spacing: var(--track-2);
    margin-block: 0.25rem 0;
  }
  .page-head--compact .lede { font-size: 0.8125rem; line-height: 1.45; max-width: none; }
  .page-head__row { gap: 0.2rem 1rem; }

  /* Count, filter, sort — one row. It wrapped into two because the sort form
     was pushed to the far edge with nothing left to sit in. */
  .shop-toolbar { flex-wrap: nowrap; gap: 0.5rem; margin-bottom: 0.75rem; }
  .shop-toolbar__count { font-size: 0.875rem; white-space: nowrap; }
  .shop-toolbar__jump { flex: 0 0 auto; }
  .shop-sort { min-width: 0; flex: 0 1 auto; }
  .shop-sort select {
    min-width: 0;
    /* The chevron needs its lane whatever else happens to the width. */
    text-overflow: ellipsis;
  }

  /* Two cards to a row rather than one. A phone showing a single card is a
     catalogue you read one page at a time; two halves the scrolling and still
     leaves the photograph the biggest thing on the card. The gaps come in with
     them, because at this width the air between cards is the scarce thing. */
  /* The air between the band and the first photograph was 94px of nothing:
     the section's own top padding, the toolbar's margin and the rule's
     padding, each sized for a screen with room to spare. */
  .section--tight { padding-top: 0.9rem; }
  .game-grid {
    column-gap: 0.85rem;
    row-gap: 1.5rem;
    padding-top: 0.8rem;
  }
  .game-card > .photo { margin-bottom: 0.6rem; }
  .game-card__title { font-size: 1rem; }
  .game-card__meta { font-size: 0.6875rem; }
  .game-card__foot { padding-top: 0.7rem; gap: 0.4rem 0.5rem; }
  .card-flag { font-size: 0.5625rem; padding: 0.15rem 0.4rem; }
}

/* ---------------------------------------------------------------------------
   How to send it

   Two radios wearing cards, the same trick the denominations below use: the
   input stays in the tab order off-screen, the label is the target, and the
   chosen one is marked by a filled edge rather than a tick.

   Each card carries its own total. A split shipment is a second courier and
   therefore a second fee, and a choice that shows one price while charging
   another is a choice nobody made — so the price is inside the thing you
   press, not in a footnote under it.
   --------------------------------------------------------------------------- */
.ship-choice__intro { font-size: 0.875rem; color: var(--ink-muted); margin-bottom: 0.75rem; }
.ship-choice__list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 0.6rem;
}
.ship-choice__input {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.ship-choice__card {
  display: grid;
  gap: 0.2rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--rule-strong);
  border-left-width: 3px;
  border-left-color: var(--rule);
  background: var(--surface);
  cursor: pointer;
}
.ship-choice__card:hover { border-left-color: var(--accent); }
.ship-choice__input:checked + .ship-choice__card { border-left-color: var(--ink); }
/* The focus ring belongs to the label, since the input it belongs to is the
   one thing on this control nobody can see. */
.ship-choice__input:focus-visible + .ship-choice__card {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.ship-choice__head { font-weight: 700; font-size: 0.9375rem; }
.ship-choice__note { font-size: 0.8125rem; color: var(--ink-muted); line-height: 1.45; }
.ship-choice__price {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1rem;
  letter-spacing: var(--track-1);
  margin-top: 0.15rem;
}

/* The what-if line under the summary total: a figure that is not being charged
   yet, so it is quieter than the one that is. */
.checkout__aside-line dt,
.checkout__aside-line dd { font-size: 0.8125rem; color: var(--ink-muted); }

/* ---------------------------------------------------------------------------
   Gift card denominations

   Three radio buttons wearing cards. The input is the real control and stays
   in the tab order off-screen; the label is the target, and the selected one
   is marked by a filled edge rather than a tick, because at this size a tick
   is smaller than the thing it is describing.
   --------------------------------------------------------------------------- */
.denoms {
  list-style: none;
  padding: 0;
  margin: 0.4rem 0 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0.6rem;
}
.denoms__input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: -1px; }
.denoms__card {
  display: grid;
  gap: 0.25rem;
  height: 100%;
  padding: 0.9rem 1rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--rule);
  cursor: pointer;
}
.denoms__card:hover { border-color: var(--rule-strong); }
.denoms__input:checked + .denoms__card {
  border-color: var(--rule-strong);
  border-left-color: var(--accent);
}
.denoms__input:focus-visible + .denoms__card { outline: 2px solid var(--accent); outline-offset: 2px; }
.denoms__amount {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 400;
  letter-spacing: var(--track-1);
}
.denoms__note { font-size: 0.8125rem; color: var(--ink-muted); line-height: 1.4; }

/* A gift card coming off a receipt total. Green, because from the customer's
   side it is the good news on the page. */
.receipt__card { color: var(--ok-ink); font-weight: 700; }

/* ---------------------------------------------------------------------------
   Request a game

   The form on the left, and on the right the answers to the questions that
   stop somebody sending it. That half of the page used to be empty, which on
   the one page asking a visitor to trust a promise rather than buy a thing off
   a shelf was the worst place on the site to have nothing to say.
   --------------------------------------------------------------------------- */
.request-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 22rem);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: start;
}
.request-layout__main { min-width: 0; }

.request-aside {
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--rule);
  padding: clamp(1.1rem, 2vw, 1.5rem);
}
.request-aside__title {
  font-size: var(--step-1);
  letter-spacing: var(--track-1);
  padding-bottom: 0.8rem;
  border-bottom: 1px solid var(--rule);
}

.request-steps { list-style: none; padding: 0; margin: 1rem 0 0; display: grid; gap: 0.9rem; }
.request-steps li { display: flex; gap: 0.75rem; align-items: start; }
/* A numeral in the display face, because these are steps in order and the
   order is the point. Olive rather than orange: it describes rather than
   invites, which is what the informational accent is for. */
.request-steps__num {
  flex: none;
  width: 1.6rem;
  height: 1.6rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 0.875rem;
  color: #fff;
  background: var(--accent-2);
}
.request-steps__head { font-weight: 700; font-size: 0.9375rem; }
.request-steps__note { font-size: 0.8125rem; color: var(--ink-muted); margin-top: 0.1rem; line-height: 1.45; }

.request-aside__box {
  margin-top: 1.25rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--rule);
}
.request-aside__head {
  font-family: var(--font-eyebrow);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.request-aside__note { font-size: 0.875rem; margin-top: 0.35rem; }
.request-aside__links { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.8rem; }
.request-aside__ring { margin-top: 1.1rem; font-size: 0.875rem; color: var(--ink-muted); }

@media (max-width: 60rem) {
  /* Stacked, the panel goes under the form: it is reassurance for somebody
     hesitating over the button, and above the form it would be a wall of
     reading before the thing they came to do. */
  .request-layout { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------------------------------------------------------------------
   Rate this game

   Five radio buttons wearing stars. The inputs are the real control — a screen
   reader gets a proper radio group with a legend — and the labels are what you
   see and press.

   The markup lists 5 down to 1 so that hovering a star can light the ones
   before it: a sibling selector only reaches forwards, so the row is reversed
   in the DOM and reversed back with row-reverse. That is the whole trick.
   --------------------------------------------------------------------------- */
.rate {
  margin-top: 1.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
  max-width: 44ch;
}
.rate__set { border: 0; padding: 0; margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; }
.rate__legend {
  float: left;
  width: 100%;
  padding: 0;
  font-family: var(--font-eyebrow);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: 0.5rem;
}
/* Reversed, per the note above. */
.rate__stars { display: flex; flex-direction: row-reverse; gap: 0.1rem; }
/* Off-screen rather than display:none, so it keeps its place in the tab order
   and the label stays its visible proxy. */
.rate__input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: -1px; }
.rate__star {
  font-size: 1.5rem;
  line-height: 1;
  color: var(--rule);
  cursor: pointer;
  /* A thumb target. The glyph is 24px; the padding takes the hit area to 44. */
  padding: 0.35rem 0.15rem;
  transition: color 90ms ease;
}
/* The star being pointed at, and — because the row is reversed — every star
   below it, which is the direction a rating fills. */
.rate__star:hover,
.rate__star:hover ~ .rate__star,
.rate__input:checked ~ .rate__star,
.rate__input:focus-visible + .rate__star {
  color: var(--accent-2);
}
.rate__input:focus-visible + .rate__star { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .rate__star { transition: none; }
}
.rate__note { margin-top: 0.6rem; font-size: 0.875rem; color: var(--ink-muted); }

/* ---------------------------------------------------------------------------
   The receipt

   What somebody sees after placing an order, and — because it is cash on
   delivery — the thing they may well want on paper before a courier arrives
   asking for a specific number. So it is built as a document: a reference and
   an amount due at the top, the lines in the middle, the address and the phone
   number at the bottom, and a print rule that strips the site around it.
   --------------------------------------------------------------------------- */
.receipt {
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  padding: clamp(1.1rem, 2.5vw, 2rem);
  max-width: 46rem;
}

.receipt__head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: start;
  gap: 1rem 2rem;
  padding-bottom: 1.1rem;
  border-bottom: 2px solid var(--ink);
}
.receipt__label {
  font-family: var(--font-eyebrow);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.receipt__reference {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 400;
  letter-spacing: var(--track-2);
  font-variant-numeric: tabular-nums;
}
/* The figure the courier will ask for. It is the one number on the page that
   matters at the door, so it gets the accent and the size to match. */
.receipt__stamp { text-align: right; }
.receipt__due {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 400;
  letter-spacing: var(--track-2);
  color: var(--accent);
}
.receipt__note { font-size: 0.75rem; color: var(--ink-muted); }

.receipt__lines { width: 100%; border-collapse: collapse; margin-top: 1.25rem; }
.receipt__lines th, .receipt__lines td { text-align: left; padding: 0.6rem 0.5rem 0.6rem 0; }
.receipt__lines thead th {
  font-family: var(--font-eyebrow);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
  border-bottom: 1px solid var(--rule);
}
.receipt__lines tbody th { font-weight: 600; }
.receipt__lines tbody tr { border-bottom: 1px solid var(--rule); }
/* Numbers right, and tabular so the column of them lines up. */
.receipt__lines td { text-align: right; font-variant-numeric: tabular-nums; }
.receipt__lines th:first-child { width: 55%; }
.receipt__lines tfoot th { text-align: right; font-weight: 400; color: var(--ink-muted); }
.receipt__lines tfoot td { padding-block: 0.3rem; }
.receipt__free { color: var(--ok-ink); font-weight: 700; }
.receipt__total th, .receipt__total td {
  border-top: 2px solid var(--ink);
  padding-top: 0.7rem;
  font-weight: 800;
  color: var(--ink);
  font-size: 1.0625rem;
}

.receipt__facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 1.1rem;
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
}
.receipt__fact { font-size: 0.9375rem; line-height: 1.5; margin-top: 0.2rem; }
.receipt__aside { color: var(--ink-muted); font-size: 0.875rem; }

.receipt__message {
  margin-top: 1.25rem;
  padding: 0.85rem 1rem;
  background: var(--panel);
  border-left: 3px solid var(--accent-2);
  font-size: 0.9375rem;
}
.receipt__message .receipt__label { display: block; margin-bottom: 0.25rem; }

.receipt__keep {
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px dashed var(--rule);
  font-size: 0.875rem;
  color: var(--ink-muted);
}

/* The receipt on a phone.

   All three of them — an order, a game request, a gift card — share this
   head, and at the display face's --step-2 the reference and the amount due
   are around 350px of type between them. On a 375px screen that is a second
   line for the amount and, on the gift card, a code that runs out of the box
   it is printed in.

   The lines table is the order's alone, and it was the worse half: four
   columns of figures across 330px broke Rs 9,200 over two lines. Qty and Each
   fold into the game cell as one line under the title, which leaves the
   figure the courier will actually ask for a column of its own. Nothing is
   hidden that is not said somewhere else on the same row. */
@media (max-width: 46rem) {
  .receipt__head { gap: 0.75rem 1rem; padding-bottom: 0.9rem; }
  .receipt__reference,
  .receipt__due { font-size: var(--step-1); }
  /* A gift card code is four groups and a pair of hyphens, and it is the one
     string here that has no business breaking a layout. */
  .receipt__reference { overflow-wrap: anywhere; }

  /* Four columns that still read as four columns.

     What broke them was the 55% on the game column: it took 181px of a 330px
     card and left three money columns to share 149, so every figure wrapped
     between its rupees and its number — Rs above 9,200, in a column of
     figures nobody could then read down. The width goes back to auto, the
     figures are told never to break, and the type comes down a step so the
     four of them fit. The game is the only thing in the row allowed to wrap,
     because it is the only thing that is words. */
  .receipt__lines { font-size: 0.875rem; }
  .receipt__lines th:first-child { width: auto; }
  .receipt__lines th,
  .receipt__lines td { padding: 0.5rem 0 0.5rem 0.5rem; }
  .receipt__lines th:first-child { padding-left: 0; }
  .receipt__lines td,
  .receipt__lines tfoot th { white-space: nowrap; }
  .receipt__lines thead th { font-size: 0.5625rem; letter-spacing: 0.08em; }
  .receipt__total th,
  .receipt__total td { font-size: 0.9375rem; }

  .receipt__facts { gap: 0.9rem; margin-top: 1.1rem; padding-top: 1rem; }
}

@media print {
  .masthead__nav, .masthead__utils, .skip-link, .btn, .added { display: none !important; }
  body { background: #fff; }
  .photo { display: none; }
  /* A receipt on paper wants the paper, not a box drawn on it, and it wants
     the whole width. The rules inside it stay: they are what makes a column
     of figures readable. */
  .receipt { border: 0; padding: 0; max-width: none; }
  .receipt__due { color: var(--ink); }
  /* The two buttons under it are the only things on this page that cannot be
     pressed on paper, and .btn is already hidden above. */
}
