/* Bricolage Grotesque, self-hosted rather than loaded from fonts.googleapis.com.
   That link sent every visitor's IP to Google before anything was rendered —
   a real data transfer, ruled non-consensual by a German court in 2022, and
   the only third party the browser contacted at all. Nothing else on this
   site needed consent; this did.
   It is also faster: two fewer DNS lookups and TLS handshakes, and no
   render-blocking stylesheet on a third-party origin.

   Headings, prices and discount badges only; body text stays the system font.
   The files carry the weight axis alone (500 to 800): the discount badges set
   their weight from the discount (kineticBadgeStyle in js/app.js), and the
   width and optical-size axes would have tripled the file (131KB against 41KB
   for latin) for a difference nobody sees at badge size. Latin-ext is separate
   and only downloads if the page actually contains those characters — game
   titles routinely do.

   Refresh: scripts/ has no task for this; the files came from the css2 API
   for family=Bricolage+Grotesque:wght@500..800 and only need refetching if
   the family is updated upstream. Replaced Space Grotesk in the 2026 redesign. */
@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 500 800;
  /* swap: headings render immediately in the fallback and restyle when the
     font lands, rather than sitting invisible. */
  font-display: swap;
  src: url('/assets/fonts/bricolage-grotesque-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: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 500 800;
  font-display: swap;
  src: url('/assets/fonts/bricolage-grotesque-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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* The SaveSpawn palette (Respawn Cyan): near-black teal with one cyan brand colour, used for the logo,
   buttons and links only. Discounts are always green (--discount-*, --tier-*), price rises red, and violet is
   kept for free games only, so "free" never reads as just another discount.
   Every text/fill pair below is contrast-checked against what it sits on. */
:root {
  /* What the browser draws itself follows the theme: the list a select opens, the
     checkbox, the scrollbars. Left unset, they stayed light on the dark theme, and
     the options inherited the light text of their select: white on white. */
  color-scheme: dark;
  --bg: #0a1013;
  --bg-elev: #131b20;
  --bg-elev-2: #1a242b;
  --border: #24313a;
  --text: #edf4f6;
  /* 6.7:1 on --bg-elev. */
  --text-dim: #93a3ab;
  --accent: #3dd6f5;
  /* Text and icons ON an accent fill. The cyan is light, so the ink is dark
     here (9.5:1) and white in the light theme, where the accent is a deep
     cyan (5.4:1). Never hard-code white on --accent: it is 1.7:1 here. */
  --accent-ink: #06222b;
  --glow: rgba(61,214,245,0.30);
  /* Small text links: the accent itself is 11:1 on --bg. */
  --link: #6fe2f8;
  --accent-2: #3dd6f5;
  --free: #b79bff;
  /* 7.9:1 on --free. */
  --free-ink: #150a30;
  --price-old: #8797a0;
  --discount-bg: #16240f;
  /* 9.6:1 on --discount-bg. */
  --discount-text: #8be04e;
  /* Discount tiers (see .savings-badge). Each pair is contrast-checked against
     its own fill, not against the page, because both halves are set together.
     Warm and hot share the accent fill; the hot tier is told apart by the
     badge's weight and size, which grow with the discount. */
  --tier-warm-bg: #8be04e;
  --tier-warm-text: #0b1a05;
  --tier-hot-bg: #8be04e;
  --tier-hot-text: #0b1a05;
  --tier-hot-glow: rgba(139,224,78,0.45);
  --danger: #ff5a5f;
  /* Warnings a buyer should read (region-locked keys). Amber, not --danger:
     that one has no light-theme value and is 2.8:1 on a white card. */
  --warn-text: #ffb454;
  --radius: 12px;
  --shadow: 0 4px 16px rgba(0,0,0,0.4);
  --header-bg: rgba(10,16,19,0.78);
  /* Frosted controls (header pills, hero search, dock), a stronger hairline
     for them, a faint fill for chips, and the two glows at the top of the page. */
  --glass: rgba(19,27,32,0.62);
  --border-strong: rgba(148,163,184,0.28);
  --chip: rgba(148,163,184,0.10);
  --halo-1: rgba(61,214,245,0.10);
  --halo-2: rgba(183,155,255,0.09);
  --heading-font: 'Bricolage Grotesque', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* One system for every tile on the page, from the "At a glance" bento to the
     deal cards, the directory and the footer: the same gutter, corner and inner
     padding, and the same columns (4, then 3 at 1080px, then 2 at 800px: the deal
     grid's own steps), so a tile's edge lands on a card's edge wherever it sits.
     --tile-row is a card's height, so the bento's rows keep the cards' rhythm. */
  --tile-gap: 16px;
  --tile-radius: 22px;
  --tile-pad: 22px;
  --tile-row: 270px;
}
@media (max-width: 800px) { :root { --tile-gap: 12px; } }
@media (max-width: 640px) { :root { --tile-gap: 10px; --tile-radius: 16px; --tile-pad: 16px; } }

/* Light theme applies when the OS prefers it (unless explicitly forced
   back to dark below) or when explicitly forced to light regardless of
   OS preference — see the manual toggle in app.js. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;
    --bg: #f1f4f4;
    --bg-elev: #ffffff;
    --bg-elev-2: #e9eeef;
    --border: #d5dddf;
    --text: #141a1e;
    --text-dim: #4f5e66;
    --price-old: #5a6870;
    --accent: #06729a;
    --accent-ink: #ffffff;
    --glow: rgba(6,114,154,0.22);
    /* 4.9:1 on --bg, 4.6:1 on --bg-elev-2. */
    --link: #06729a;
    --accent-2: #06729a;
    --free: #6d28d9;
    --free-ink: #ffffff;
    --discount-bg: #e4f2da;
    /* 4.9:1 on --discount-bg, 5.7:1 on white. */
    --discount-text: #327510;
    --warn-text: #a35200;
    --tier-warm-bg: #327510;
    --tier-warm-text: #ffffff;
    --tier-hot-bg: #327510;
    --tier-hot-text: #ffffff;
    --tier-hot-glow: rgba(50,117,16,0.35);
    --shadow: 0 4px 16px rgba(15,23,42,0.08);
    --header-bg: rgba(241,244,244,0.82);
    --glass: rgba(255,255,255,0.72);
    --border-strong: rgba(15,23,42,0.20);
    --chip: rgba(15,23,42,0.05);
    --halo-1: rgba(6,114,154,0.10);
    --halo-2: rgba(109,40,217,0.07);
  }
}

:root[data-theme="light"] {
  /* Same values as the prefers-color-scheme block above. */
  color-scheme: light;
  --bg: #f1f4f4;
  --bg-elev: #ffffff;
  --bg-elev-2: #e9eeef;
  --border: #d5dddf;
  --text: #141a1e;
  --text-dim: #4f5e66;
  --price-old: #5a6870;
  --accent: #06729a;
  --accent-ink: #ffffff;
  --glow: rgba(6,114,154,0.22);
  --link: #06729a;
  --accent-2: #06729a;
  --free: #6d28d9;
  --free-ink: #ffffff;
  --discount-bg: #e4f2da;
  --discount-text: #327510;
  --warn-text: #a35200;
  --tier-warm-bg: #327510;
  --tier-warm-text: #ffffff;
  --tier-hot-bg: #327510;
  --tier-hot-text: #ffffff;
  --tier-hot-glow: rgba(50,117,16,0.35);
  --header-bg: rgba(241,244,244,0.82);
  --shadow: 0 4px 16px rgba(15,23,42,0.08);
  --glass: rgba(255,255,255,0.72);
  --border-strong: rgba(15,23,42,0.20);
  --chip: rgba(15,23,42,0.05);
  --halo-1: rgba(6,114,154,0.10);
  --halo-2: rgba(109,40,217,0.07);
}

* { box-sizing: border-box; }

/* Keyboard focus was invisible everywhere: the only :focus rule in this file
   was .filter-group:focus-within, and .search-wrap input sets outline:none
   with nothing in its place. That is a WCAG 2.4.7 failure and makes the site
   unusable by keyboard. :focus-visible (rather than :focus) means pointer
   users never see a ring on click. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
/* The search field is visually inside the rounded search pill, so the ring
   goes on the pill rather than the bare input. */
.search-wrap:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.search-wrap input:focus-visible { outline: none; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  min-height: 100vh;
}

/* Two soft glows behind the top of the page (green left, violet right), on
   the body so they scroll away with it. The bottom padding keeps the end of
   the page clear of the dock. */
body {
  background-image:
    radial-gradient(900px 340px at 18% -60px, var(--halo-1), transparent 70%),
    radial-gradient(760px 300px at 92% -40px, var(--halo-2), transparent 70%);
  background-repeat: no-repeat;
  padding-bottom: 88px;
}

a { color: inherit; }

/* Header */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--header-bg);
  backdrop-filter: blur(18px) saturate(1.4);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 16px 40px -24px rgba(0,0,0,0.9);
  transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
/* At the very top nothing is under the header yet, so it is part of the page
   rather than a bar over it. */
.site-header.is-top {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom-color: transparent;
  box-shadow: none;
}

.header-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 14px 20px;
  display: flex;
  align-items: center;
  gap: 24px;
}

.logo {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  font-family: var(--heading-font);
  font-weight: 700;
  font-size: 22px;
  letter-spacing: -0.02em;
  white-space: nowrap;
}

/* An <svg> now rather than an emoji glyph, so it needs a box rather than a
   font-size. The body's colour is applied here (the path uses currentColor) so
   the mark stays one colour decision rather than a hard-coded fill in the
   markup. Text colour, not the accent: the ring is the brand colour and the
   chart arrow inside it follows the text, so the mark is two colours. */
.logo-mark {
  display: block;
  flex-shrink: 0;
  color: var(--text);
}
.accent { color: var(--accent); }

.search-wrap {
  position: relative;
  flex: 1;
  display: flex;
  align-items: center;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 4px 4px 16px;
  max-width: 640px;
}

.search-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: var(--shadow);
  overflow: hidden;
  z-index: 30;
}
.search-dropdown.open { display: block; }

.search-dropdown-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  transition: background 0.15s ease;
}
.search-dropdown-item:last-child { border-bottom: none; }
.search-dropdown-item:hover, .search-dropdown-item.active { background: var(--bg-elev-2); }

.search-dropdown-thumb {
  width: 40px;
  height: 40px;
  border-radius: 7px;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--bg-elev-2);
}

.search-dropdown-info {
  flex: 1;
  min-width: 0;
}

.search-dropdown-title {
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.search-dropdown-meta {
  font-size: 11.5px;
  color: var(--text-dim);
}

.search-dropdown-price {
  font-family: var(--heading-font);
  font-size: 13px;
  font-weight: 700;
  flex-shrink: 0;
}
/* Violet, like free games on the cards. */
.search-dropdown-price.free { color: var(--free); }

.search-dropdown-note {
  padding: 12px;
  font-size: 12.5px;
  color: var(--text-dim);
  text-align: center;
}

.search-wrap input {
  flex: 1;
  /* A flex item will not shrink below its content by default, and an input's
     content is its placeholder: on a phone the hero box ran 29px off screen. */
  min-width: 0;
  background: transparent;
  border: none;
  outline: none;
  color: var(--text);
  font-size: 14px;
  padding: 8px 0;
}

.search-wrap input::placeholder { color: var(--text-dim); }

.search-wrap button {
  background: var(--accent);
  border: none;
  color: var(--accent-ink);
  width: 34px;
  height: 34px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  box-shadow: 0 6px 16px -4px var(--glow);
}
.search-wrap button:hover { opacity: 0.9; }
/* Only in the hero, where the button is wide enough to say it. */
.search-go-label { display: none; }

/* On the homepage the box belongs in the hero: hidden here from the first
   paint (no flash of a header search that then leaves), until js/app.js
   initHero moves it. */
body:has(#heroSearch) .site-header .search-wrap { display: none; }

/* The header's stand-in for the hero search once it has scrolled away. */
.header-find {
  flex: 1;
  max-width: 460px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
  height: 42px;
  padding: 0 14px;
  border: 1px solid var(--border);
  border-radius: 13px;
  background: var(--glass);
  color: var(--text-dim);
  font: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.18s ease;
}
.header-find:hover { border-color: var(--border-strong); color: var(--text); }
.header-find[hidden] { display: none; }
.header-find svg { flex: none; color: var(--accent); }
.header-find-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.header-find-kbd,
.cmd kbd,
.cmd-close {
  display: inline-flex;
  align-items: center;
  flex: none;
  height: 24px;
  padding: 0 7px;
  border-radius: 7px;
  border: 1px solid var(--border-strong);
  background: var(--chip);
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
}

/* The homepage's intro (index.html): a visible heading and one search box for
   both jobs, kept short enough that the deals still start on the first screen. */
.hero {
  max-width: 1200px;
  margin: 0 auto;
  padding: 30px 20px 6px;
  text-align: center;
}
.hero h1 {
  margin: 0 auto;
  max-width: 980px;
  text-wrap: balance;
  font-family: var(--heading-font);
  font-weight: 700;
  font-size: clamp(30px, 3.7vw, 52px);
  line-height: 1.04;
  letter-spacing: -0.035em;
}
.hero h1 em { font-style: normal; color: var(--accent); white-space: nowrap; }
.hero-sub {
  margin: 12px auto 0;
  max-width: 680px;
  text-wrap: balance;
  color: var(--text-dim);
  font-size: 16px;
  line-height: 1.5;
}
/* Holds the box's height until initHero moves it in. */
.hero-search { max-width: 780px; min-height: 64px; margin: 22px auto 0; }
.hero .search-wrap {
  max-width: none;
  height: 64px;
  padding: 0 8px 0 18px;
  gap: 12px;
  text-align: left;
  border-radius: 21px;
  background: var(--glass);
  border-color: var(--border-strong);
  backdrop-filter: blur(22px);
  -webkit-backdrop-filter: blur(22px);
  box-shadow: 0 0 0 4px var(--chip), 0 0 70px -14px var(--glow);
}
/* The magnifier, in the accent: a mask, so it takes the theme's colour. */
.hero .search-wrap::before {
  content: "";
  flex: none;
  width: 22px;
  height: 22px;
  background: var(--accent);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM21 21l-4.35-4.35'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM21 21l-4.35-4.35'/%3E%3C/svg%3E") center / contain no-repeat;
}
.hero .search-wrap:focus-within { outline: none; border-color: var(--accent); }
.hero .search-wrap input { height: 100%; font-size: 17px; font-weight: 500; }
.hero .search-wrap button {
  width: auto;
  height: 48px;
  padding: 0 22px;
  border-radius: 15px;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  white-space: nowrap;
  box-shadow: none;
}
.hero .search-wrap button svg { display: none; }
.hero .search-go-label { display: inline; }
.hero-hint { margin: 10px auto 0; color: var(--text-dim); font-size: 13.5px; }
.hero-hint a { color: var(--text); font-weight: 600; text-underline-offset: 3px; }
/* The hero's hint says the same thing, so the homepage drops the separate link. */
main:has(#hero) .wishlist-cta { display: none; }

/* The hero's three facts (js/app.js renderGlance). "–" until the picks arrive; a
   fact with nothing to say is then hidden, and the row with it if none has. */
.hero-facts {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px 40px;
  margin-top: 20px;
}
.hero-facts[hidden] { display: none; }
.fact { display: flex; align-items: baseline; gap: 10px; text-align: left; }
.fact[hidden] { display: none; }
.fact b {
  font-family: var(--heading-font);
  font-weight: 700;
  font-size: 24px;
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}
.fact b.is-accent { color: var(--discount-text); }
.fact b.is-free { color: var(--free); }
.fact span { max-width: 190px; color: var(--text-dim); font-size: 13px; line-height: 1.3; }

/* "At a glance" (index.html, js/app.js renderGlance): a bento box. Five tiles,
   each one idea, on the same columns and gutter as the deal cards under it:

     4 columns   [ lead  lead  drop  low  ]      3 columns   [ lead  lead  drop ]
                 [ lead  lead  cheap cheap]                  [ lead  lead  low  ]
                 [ the sale dates, a strip ]                 [ cheap cheap dates]

   The lead tile is the biggest, two cards by two; the others are a card, or
   two. Every row ends flush at the right, and when a tile is missing one of
   its neighbours grows into its place instead of leaving a hole (the :has()
   rules). Down to 2 columns the lead tile takes the width and the rest follow
   two by two; on a phone the five are a row to swipe. */
.glance { max-width: 1200px; margin: 28px auto 0; padding: 0 20px; }
.glance[hidden] { display: none; }
.glance-head h2 {
  margin: 0;
  font-family: var(--heading-font);
  font-weight: 700;
  font-size: 26px;
  line-height: 1.1;
  letter-spacing: -0.025em;
}
.glance-head p { margin: 6px 0 14px; color: var(--text-dim); font-size: 14px; }
.bento {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* The two rows of cards-sized tiles; the strip's row is its own height. */
  grid-template-rows: repeat(2, minmax(var(--tile-row), auto));
  grid-auto-rows: auto;
  grid-auto-flow: row dense;
  gap: var(--tile-gap);
}
.tile {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 16px;
  min-width: 0;
  padding: var(--tile-pad);
  border-radius: var(--tile-radius);
  border: 1px solid var(--border);
  background: var(--bg-elev);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
  overflow: hidden;
  color: var(--text);
}
.t-feat { grid-column: span 2; grid-row: span 2; }
.t-drop, .t-low { grid-column: span 1; }
.t-list { grid-column: span 2; }
.t-cal { grid-column: 1 / -1; min-height: 132px; }
/* The tiles that hold a list or a row of dates measure their own width (not the
   page's), so they lay themselves out for whichever space the grid gave them. */
.t-list, .t-cal { container-type: inline-size; }

/* A missing tile never leaves a hole in the box. */
@media (min-width: 1081px) {
  .bento:not(:has(.t-drop)) .t-low,
  .bento:not(:has(.t-low)) .t-drop { grid-column: span 2; }
  .bento:not(:has(.t-list)) .t-cal { grid-column: span 2; }
  .bento:not(:has(.t-drop)):not(:has(.t-low)) .t-list { grid-row: span 2; }
  .bento:not(:has(.t-drop)):not(:has(.t-low)):not(:has(.t-list)) .t-cal { grid-row: span 2; }
}
@media (min-width: 801px) and (max-width: 1080px) {
  .bento { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .t-cal { grid-column: span 1; min-height: 0; }
  .bento:not(:has(.t-drop)) .t-low,
  .bento:not(:has(.t-low)) .t-drop { grid-row: span 2; }
  .bento:not(:has(.t-list)) .t-cal { grid-column: span 3; }
}
.tile.is-loading {
  background: linear-gradient(90deg, var(--bg-elev-2) 25%, var(--bg-elev) 50%, var(--bg-elev-2) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
}

/* Tiles over artwork: the picture fills the tile under a dark scrim, so the
   white text reads in either theme. */
.tile.on-art { color: #fff; border-color: transparent; background: #0b1018; }
.tile-art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.tile-art.is-blur { filter: blur(38px) saturate(1.35); transform: scale(1.3); opacity: 0.9; }
.tile-scrim {
  position: absolute;
  inset: 0;
  /* Darker than the design's 0.10 at the top: on a narrow tile the title sits
     high, over the brightest part of the picture. */
  background: linear-gradient(180deg, rgba(8,13,16,0.30) 0%, rgba(8,13,16,0.66) 42%, rgba(8,13,16,0.95) 100%);
}
.tile > *:not(.tile-art):not(.tile-scrim) { position: relative; }
.tile-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.tile-label {
  display: inline-flex;
  align-items: center;
  height: 28px;
  padding: 0 11px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  white-space: nowrap;
}
.tile-label.is-free { background: var(--free); color: var(--free-ink); }
.tile-label.is-quiet { background: var(--chip); color: var(--text-dim); border: 1px solid var(--border); }
.tile .savings-badge {
  font-size: calc(18px + var(--k, 0) * 18px);
  padding: 3px 11px 4px;
  border-radius: 12px;
  letter-spacing: calc(-0.01em - var(--k, 0) * 0.04em);
  box-shadow: 0 8px 24px -8px rgba(0,0,0,0.6);
}
.feat-cover {
  display: block;
  width: 100%;
  max-width: 520px;
  margin: 0 auto;
  aspect-ratio: 460 / 215;
  object-fit: cover;
  border-radius: calc(var(--tile-radius) - 6px);
  border: 1px solid rgba(255,255,255,0.16);
  box-shadow: 0 26px 50px -22px rgba(0,0,0,0.85);
}
/* The free tile when Epic's weekly games are together (js/app.js freeSetTileHtml): their covers side by side, each with its own title,
   worth and Claim button; --n is how many (2 or 3). Where the tile is narrow they are rows, the cover beside the text. */
.feat-set { display: grid; grid-template-columns: repeat(var(--n, 2), minmax(0, 1fr)); gap: 14px; }
.feat-item { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.feat-item .feat-cover { max-width: none; margin: 0; }
.feat-text { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; min-width: 0; }
.feat-item .tile-title { font-size: 20px; }
.tile-big.sm { font-size: 28px; }
.t-free-set .tile-row { align-items: center; }
@media (max-width: 640px) {
  .feat-set { grid-template-columns: 1fr; gap: 12px; }
  .feat-item { flex-direction: row; align-items: center; gap: 12px; }
  .feat-item .feat-cover { width: 124px; flex: none; }
}
.tile-foot { display: flex; flex-direction: column; gap: 12px; }
.tile-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.tile-chip {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding: 0 8px;
  border-radius: 8px;
  background: rgba(255,255,255,0.14);
  border: 1px solid rgba(255,255,255,0.22);
  font-size: 12px;
  font-weight: 600;
}
.tile-title {
  margin: 0;
  font-family: var(--heading-font);
  font-weight: 700;
  font-size: 26px;
  line-height: 1.08;
  letter-spacing: -0.03em;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.tile-title.big { font-size: 38px; letter-spacing: -0.04em; }
.tile-title.cal { font-size: 24px; }
.tile-title a { color: inherit; text-decoration: none; }
.tile-title a:hover { text-decoration: underline; text-underline-offset: 4px; }
.tile-row { display: flex; align-items: flex-end; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
.tile-price { display: flex; align-items: baseline; gap: 10px; }
.tile-big {
  font-family: var(--heading-font);
  font-weight: 700;
  font-size: 40px;
  line-height: 1;
  letter-spacing: -0.035em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* On the scrim the theme's text colour is not what the text sits on: these two are
   checked against the dark scrim in both themes. */
.on-art .tile-big { color: #edf4f6; }
.tile-big.free { color: var(--free); }
.on-art .tile-big.free { color: #cdb9ff; }
.tile-was { color: var(--price-old); text-decoration: line-through; font-variant-numeric: tabular-nums; }
.on-art .tile-was { color: rgba(255,255,255,0.72); }
.tile-note { margin: 0; color: var(--text-dim); font-size: 12.5px; line-height: 1.45; }
.on-art .tile-note { color: rgba(255,255,255,0.82); }
.tile-ctas { display: flex; flex-wrap: wrap; gap: 8px; }
.tile-cta {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  gap: 8px;
  height: 42px;
  padding: 0 16px;
  border-radius: 13px;
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 14px;
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
}
.tile-cta.is-free { background: var(--free); color: var(--free-ink); }
.tile-cta.is-ghost { background: rgba(255,255,255,0.14); border-color: rgba(255,255,255,0.28); color: #fff; }
.tile:not(.on-art) .tile-cta.is-ghost { background: var(--chip); border-color: var(--border-strong); color: var(--text); }
.tile-cta:hover { filter: brightness(1.08); }

/* The cheap list. */
.tile-rows { display: flex; flex-direction: column; gap: 8px; }
.tile-lr {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px;
  border-radius: 14px;
  border: 1px solid transparent;
  background: var(--chip);
  color: inherit;
  text-decoration: none;
}
.tile-lr:hover { border-color: var(--border-strong); }
.tile-lr img { flex: none; width: 78px; height: 36px; border-radius: 7px; object-fit: cover; background: var(--bg-elev-2); }
.lr-name { flex: 1; min-width: 0; font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lr-price { font-family: var(--heading-font); font-weight: 700; font-size: 17px; color: var(--text); font-variant-numeric: tabular-nums; }
.lr-pct { min-width: 44px; text-align: right; color: var(--text-dim); font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
/* The padding and the equal negative margin make the target 28px tall (it was
   18px) without moving anything. */
.tile-more { display: inline-block; padding: 5px 0; margin: -5px 0; color: var(--link); font-size: 13.5px; font-weight: 600; text-decoration: none; }
.tile-more:hover { text-decoration: underline; }

/* The cheap list with room for two columns (a tile two cards wide): each game a
   thumbnail with its name over its price, two rows of two, so a name still has
   ~180px where a one-line row would leave it 50. */
@container (min-width: 460px) {
  .t-list .tile-rows { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .t-list .tile-lr {
    display: grid;
    grid-template-columns: 64px auto minmax(0, 1fr);
    grid-template-areas: "img name name" "img price pct";
    align-items: center;
    column-gap: 12px;
    row-gap: 1px;
  }
  .t-list .tile-lr img { grid-area: img; width: 64px; }
  .t-list .lr-name { grid-area: name; }
  .t-list .lr-price { grid-area: price; font-size: 16px; }
  .t-list .lr-pct { grid-area: pct; min-width: 0; text-align: left; }
}

/* The sale dates (calendarTileHtml): a column in a card-sized tile; with room,
   what is on now beside the next two; across the page, one strip. */
.cal-body { flex: 1; display: flex; flex-direction: column; justify-content: space-between; gap: 16px; }
.cal-now { min-width: 0; }
.cal-rows { margin-top: 12px; }
@container (min-width: 520px) {
  .cal-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas: "label label" "now rows" "go go";
    column-gap: 32px;
    row-gap: 12px;
    align-items: start;
  }
  .cal-label { grid-area: label; }
  .cal-now { grid-area: now; }
  .cal-rows { grid-area: rows; margin-top: 0; }
  .cal-go { grid-area: go; }
}
@container (min-width: 820px) {
  .cal-body {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.5fr) auto;
    grid-template-areas: "label rows go" "now rows go";
    align-items: center;
    column-gap: 40px;
    row-gap: 8px;
  }
  .cal-go { align-self: center; }
}
.cal-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 9px 0; border-top: 1px solid var(--border); font-size: 14px; }
.cal-row b { font-weight: 600; }
.cal-row span { color: var(--text-dim); white-space: nowrap; }

/* Two columns (the deal grid's too): the lead tile across, then two by two. */
@media (max-width: 800px) {
  .bento { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: none; grid-auto-rows: auto; }
  .t-feat { grid-column: 1 / -1; grid-row: auto; min-height: 420px; }
  .t-drop, .t-low, .t-list { min-height: var(--tile-row); }
  .t-drop, .t-low { grid-column: span 1; grid-row: auto; }
  .t-list, .t-cal { grid-column: 1 / -1; min-height: 0; }
  .bento:not(:has(.t-drop)) .t-low,
  .bento:not(:has(.t-low)) .t-drop { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  .hero-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 16px; }
  .fact { flex-direction: column; gap: 4px; }
  .fact b { font-size: 22px; }
  .fact span { font-size: 11.5px; }

  .glance { margin-top: 22px; padding: 0 12px; }
  .glance-head h2 { font-size: 22px; }
  /* A row to swipe rather than a column: stacked, the five tiles were 1,345px
     and put the first deal card 2,400px down the page. The next tile peeks in
     at the edge, which is what says the row moves. */
  .bento {
    display: flex;
    gap: var(--tile-gap);
    margin: 0 -12px;
    padding: 0 12px 4px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding: 0 12px;
    scrollbar-width: none;
  }
  .bento::-webkit-scrollbar { display: none; }
  .bento > .tile { flex: 0 0 86%; min-height: 400px; scroll-snap-align: start; }
  .tile-title { font-size: 23px; }
  .tile-title.big { font-size: 30px; }
  .tile-big { font-size: 34px; }
  .tile .savings-badge { font-size: calc(16px + var(--k, 0) * 12px); }
}

/* The command bar (index.html #cmdBackdrop, js/app.js initCommandBar): over
   everything, the game window included (.modal-backdrop is 50). */
.cmd-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(3,6,12,0.62);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.cmd-backdrop[hidden] { display: none; }
html.cmd-open { overflow: hidden; }
.cmd {
  position: absolute;
  left: 50%;
  top: 90px;
  transform: translateX(-50%);
  width: min(740px, calc(100% - 32px));
  max-height: calc(100vh - 120px);
  display: flex;
  flex-direction: column;
  background: var(--bg-elev);
  border: 1px solid var(--border-strong);
  border-radius: 24px;
  box-shadow: 0 44px 110px -30px rgba(0,0,0,0.85), 0 0 90px -30px var(--glow);
  overflow: hidden;
  color: var(--text);
}
.cmd-inp { flex: none; display: flex; align-items: center; gap: 12px; height: 68px; padding: 0 20px; border-bottom: 1px solid var(--border); }
.cmd-inp > svg { flex: none; width: 22px; height: 22px; color: var(--accent); }
.cmd-inp input { flex: 1; min-width: 0; height: 100%; border: 0; outline: 0; background: transparent; color: var(--text); font: inherit; font-size: 17px; font-weight: 500; }
.cmd-inp input::placeholder { color: var(--text-dim); }
.cmd-close { cursor: pointer; }
.cmd-close:hover { color: var(--text); }
.cmd-close-word { display: none; }
.cmd-res { flex: 1 1 auto; padding: 6px 8px 10px; overflow-y: auto; overscroll-behavior: contain; }
.cmd-sec { padding: 12px 12px 4px; color: var(--text-dim); font-size: 11px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
.cmd-it {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 10px 12px;
  border: 0;
  border-radius: 14px;
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.cmd-it.is-active { background: var(--chip); box-shadow: inset 0 0 0 1px var(--border); }
.cmd-ib { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 12px; background: var(--accent); color: var(--accent-ink); }
.cmd-ib svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.cmd-it img { flex: none; width: 72px; height: 34px; border-radius: 7px; object-fit: cover; background: var(--bg-elev-2); }
.cmd-t { flex: 1; min-width: 0; }
.cmd-t b { display: block; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cmd-t span { display: block; color: var(--text-dim); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cmd-pp { flex: none; font-family: var(--heading-font); font-weight: 700; font-size: 17px; color: var(--text); font-variant-numeric: tabular-nums; }
.cmd-pp.free { color: var(--free); }
.cmd-note { margin: 0; padding: 12px; color: var(--text-dim); font-size: 13.5px; }
.cmd-fx { flex: none; display: flex; gap: 18px; padding: 12px 20px; border-top: 1px solid var(--border); color: var(--text-dim); font-size: 12.5px; }
.cmd-fx kbd { height: 20px; padding: 0 5px; font-size: 11px; }
@media (max-width: 640px) {
  .cmd { top: 12px; width: calc(100% - 24px); max-height: calc(100vh - 24px); border-radius: 20px; }
  .cmd-inp { height: 60px; padding: 0 12px 0 16px; }
  .cmd-inp input { font-size: 16px; }
  .cmd-close { border: 0; background: none; color: var(--accent); font-size: 14px; }
  .cmd-close-key { display: none; }
  .cmd-close-word { display: inline; }
  .cmd-fx { display: none; }
  .cmd-it { gap: 12px; padding: 9px 8px; }
  .cmd-it img { width: 64px; height: 30px; }
}

/* The dock (index.html): floating at the foot of the screen, above the page
   and below the game window (.modal-backdrop is z-index 50). */
.dock {
  position: fixed;
  left: 50%;
  bottom: 18px;
  transform: translateX(-50%);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 2px;
  max-width: calc(100% - 24px);
  padding: 6px;
  border-radius: 999px;
  background: var(--glass);
  border: 1px solid var(--border-strong);
  backdrop-filter: blur(26px) saturate(1.5);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
  box-shadow: 0 26px 60px -22px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.08);
}
.dock-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 42px;
  padding: 0 14px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.dock-item svg {
  width: 18px;
  height: 18px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.dock-item:hover { color: var(--text); background: var(--chip); }
.dock-item.is-on { background: var(--accent); color: var(--accent-ink); }
.dock-sep { flex: none; width: 1px; height: 22px; margin: 0 6px; background: var(--border-strong); }
.dock-item[hidden],
.dock-sep[hidden] { display: none; }

/* "More" is the phone dock's way to the pages that do not fit the row; the full dock has them all inline. A <details>,
   so it opens without script on the pages that do not load app.js. */
.dock-more { display: none; position: relative; }
.dock-more > summary { list-style: none; }
.dock-more > summary::-webkit-details-marker { display: none; }
.dock-label-phone { display: none; }
.dock-more-list {
  position: absolute;
  right: 0;
  bottom: calc(100% + 12px);
  min-width: 190px;
  display: grid;
  padding: 6px;
  border-radius: 18px;
  background: var(--bg-elev);
  border: 1px solid var(--border-strong);
  box-shadow: 0 26px 60px -22px rgba(0,0,0,0.7);
}
.dock-more-link {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 14px;
  border-radius: 12px;
  color: var(--text);
  font-size: 14.5px;
  font-weight: 600;
  text-decoration: none;
}
.dock-more-link[hidden] { display: none; }
.dock-more-link:hover, .dock-more-link:focus-visible { background: var(--chip); }
.dock-more-link.is-on, .dock-more-link[aria-current="page"] { background: var(--accent); color: var(--accent-ink); }
.dock-more[open] > summary, .dock-more:has(.is-on, [aria-current="page"]) > summary { background: var(--accent); color: var(--accent-ink); }

/* With every label the dock is about 1050px wide. Below 1100px it ran off the
   screen: at 768px (a tablet upright) Under $5, Calendar and Search were out
   of sight and out of reach (it only scrolled on phones), and at 1024px Search
   was cut. Down to the phone breakpoint below, only the page you are on keeps
   its name; the other icons keep their title and aria-label. */
@media (max-width: 1100px) {
  .dock-item:not(.is-on) .dock-label { display: none; }
}

@media (max-width: 640px) {
  .hero { padding: 18px 12px 2px; }
  .hero h1 { font-size: 28px; }
  .hero-sub { margin-top: 8px; font-size: 14.5px; }
  .hero-search { min-height: 56px; margin-top: 16px; }
  .hero .search-wrap { height: 56px; padding: 0 6px 0 14px; gap: 10px; border-radius: 18px; }
  .hero .search-wrap::before { width: 20px; height: 20px; }
  .hero .search-wrap input { padding: 0; }
  .hero .search-wrap button { height: 44px; padding: 0 14px; border-radius: 13px; font-size: 14px; }
  .hero-hint { font-size: 13px; }

  /* Not on a phone: the header row is full (logo, Saved, currency, theme),
     and the dock's Search button does the same job at the thumb. */
  .header-find { display: none !important; }

  /* Five labelled tabs and a "More" (Deals, Free, Top rated, Cheap games, More, Search): nine bare icons were hard to tell
     apart. The rest of the row's links are in the More list. */
  .dock { bottom: 12px; padding: 4px; gap: 0; overflow: visible; }
  .dock-item { flex-direction: column; justify-content: center; gap: 3px; height: 52px; min-width: 56px; padding: 0 5px; border-radius: 18px; font-size: 10.5px; line-height: 1; letter-spacing: -0.01em; }
  .dock-minor, .dock-sep { display: none; }
  .dock-more { display: block; }
  .dock .dock-item .dock-label { display: block; }
  .dock .dock-item .dock-label-phone { display: block; }
  /* The two long labels are shortened (data-short) so six tabs fit a 360px screen without touching. */
  .dock-label[data-short] { font-size: 0; }
  .dock-label[data-short]::before { content: attr(data-short); font-size: 10.5px; }
  .dock-item svg { width: 20px; height: 20px; }
  body { padding-bottom: 88px; }
}

/* Label text kept for screen readers while the control shows only "EUR €" —
   the currency code is self-describing visually, but a bare <select> with no
   accessible name is not. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Currency selector in the header. Deliberately shaped like the site's own
   pill controls (matching .filter-group's radius and border treatment) rather
   than the flat bar this idea came from — same compactness, GGD's own look. */
.header-currency {
  display: flex;
  align-items: center;
  height: 40px;
  padding: 0 4px 0 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--glass);
  flex-shrink: 0;
  transition: border-color 0.18s ease;
}

.header-currency:hover,
.header-currency:focus-within { border-color: var(--accent); }

.header-currency select {
  appearance: none;
  -webkit-appearance: none;
  background: transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' stroke='%239aa0b0' stroke-width='2.4' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 6px center/11px;
  border: none;
  outline: none;
  color: var(--text);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 0 22px 0 0;
  height: 100%;
  cursor: pointer;
}

.theme-toggle {
  background: var(--glass);
  border: 1px solid var(--border);
  color: var(--text-dim);
  width: 40px;
  height: 40px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  transition: border-color 0.18s ease, color 0.18s ease;
}
.theme-toggle:hover { border-color: var(--accent); color: var(--text); }

/* Icon shown = the mode a click switches TO. Dark (the default) shows the
   sun; light shows the moon — same OS-preference/explicit-override
   precedence as the theme tokens above. */
.theme-toggle .icon-moon { display: none; }

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .theme-toggle .icon-sun { display: none; }
  :root:not([data-theme="dark"]) .theme-toggle .icon-moon { display: block; }
}
:root[data-theme="light"] .theme-toggle .icon-sun { display: none; }
:root[data-theme="light"] .theme-toggle .icon-moon { display: block; }

/* The header's Saved link. Same pill as the currency select beside it. The
   count is plain text colour; the filled heart carries the accent. */
.header-saved {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 40px;
  padding: 0 12px;
  margin-left: auto;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--glass);
  color: var(--text-dim);
  text-decoration: none;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  flex-shrink: 0;
  transition: border-color 0.18s ease, color 0.18s ease;
}
.header-saved:hover,
.header-saved:focus-visible { border-color: var(--accent); color: var(--text); }
.header-saved svg { display: block; flex-shrink: 0; }
.header-saved.has-saved svg path { fill: var(--accent); stroke: var(--accent); }
.header-saved-count { color: var(--text); font-weight: 700; }

/* The Steam wishlist checker, the header's primary action: the accent filled, at the end of the row. */
.header-wishlist {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 40px;
  padding: 0 16px;
  border: 1px solid transparent;
  border-radius: 12px;
  background: var(--accent);
  color: var(--accent-ink);
  text-decoration: none;
  font-size: 13.5px;
  font-weight: 700;
  white-space: nowrap;
  flex-shrink: 0;
  transition: filter 0.18s ease, box-shadow 0.18s ease;
}
.header-wishlist:hover,
.header-wishlist:focus-visible { filter: brightness(1.08); box-shadow: 0 8px 22px -10px var(--glow); }
.header-wishlist:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.header-wishlist[hidden] { display: none; }
.header-wishlist-icon { display: block; flex-shrink: 0; }
/* On a tablet the row (logo, search, Saved, currency, theme) has no room for the word: the icon alone, as on a phone. */
@media (max-width: 900px) {
  .header-wishlist { width: 40px; padding: 0; justify-content: center; }
  .header-wishlist-label { display: none; }
}

/* Filters */
.filters {
  max-width: 1200px;
  margin: 20px auto 0;
  padding: 0 20px;
  display: flex;
  flex-wrap: wrap;
  /* stretch so Reset is exactly as tall as the pills; flex-end only lined up
     their bottom edges, leaving Reset 3px short at the top. */
  align-items: stretch;
  gap: 16px;
}

/* display:contents so the three grouped pills and Reset take part in .filters'
   own flex row exactly as they did before they were wrapped — the wrapper
   exists purely so they can be collapsed together on a phone. `order` then
   restores the original left-to-right reading: Store, Sort by, Category,
   Min. discount, Reset. Without it the DOM order (sort first, which the
   collapsible requires) would show through. */
.filters-collapsible { display: contents; }
.filter-group--store { order: 1; }
.filter-group--sort { order: 2; }
.filter-group--category { order: 3; }
.filter-group--discount { order: 4; }
.reset-btn { order: 6; }
.layout-switch { order: 7; }

/* Narrow screens only. */
.filters-toggle { display: none; }

.filter-group {
  display: flex;
  /* center, not stretch: a stretched <label> box is as tall as the pill but
     renders its 12px of text at the TOP of that box, so the caption sat about
     7px above the value it labels. The select keeps its own vertical padding
     (below), so the pill height and full-height tap target are unchanged. */
  align-items: center;
  gap: 8px;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 999px;
  /* No vertical padding here on purpose: it used to be 9px top/bottom, which
     made the wrapper 35px tall while the <select> inside it stayed 15px. The
     padding is not part of the control, so the actual tap target was 15px —
     under the 24px WCAG 2.5.8 minimum, and unpleasant on a phone. The same
     space now lives on the select itself (below), so the whole pill height
     is tappable. */
  padding: 0 14px 0 16px;
  transition: border-color 0.18s ease;
}
.filter-group:hover, .filter-group:focus-within { border-color: var(--accent); }

.filter-group label {
  font-size: 10.5px;
  color: var(--text-dim);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
}

/* The open list. An option takes its select's text colour but not its background
   (the select's is transparent), so each one carries both. */
select option,
select optgroup {
  background-color: var(--bg-elev);
  color: var(--text);
}
select option:checked {
  background-color: var(--accent);
  color: var(--accent-ink);
}
select option:disabled { color: var(--text-dim); }

.filter-group select {
  appearance: none;
  -webkit-appearance: none;
  background: transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' stroke='%239aa0b0' stroke-width='2.4' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right center/11px;
  border: none;
  outline: none;
  color: var(--text);
  /* Vertical padding moved here from .filter-group so the control itself is
     the full-height tap target — see the comment there. */
  padding: 11px 16px 11px 0;
  font-size: 13px;
  font-weight: 600;
  min-width: 90px;
  cursor: pointer;
}

.reset-btn {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-dim);
  padding: 8px 16px;
  border-radius: 999px;
  cursor: pointer;
  font-size: 13px;
}
.reset-btn:hover { color: var(--text); border-color: var(--accent); }
/* Nothing to reset (js/app.js updateResetButton): greyed out, not clickable, out of the tab order. */
.reset-btn:disabled { opacity: 0.45; cursor: default; }
.reset-btn:disabled:hover { color: var(--text-dim); border-color: var(--border); }

/* Grid or list (js/app.js initLayoutSwitch). Two buttons in one pill, at the
   end of the filter bar. */
.layout-switch {
  display: inline-flex;
  align-items: stretch;
  margin-left: auto;
  padding: 3px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg-elev);
}
.layout-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  /* The pill's 3px padding is on top of this: 38px in all, 32px to hit, where
     the 18px icon alone made it 20px. */
  min-height: 32px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
}
.layout-btn svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.layout-btn:hover { color: var(--text); }
.layout-btn.is-on { background: var(--accent); color: var(--accent-ink); }

/* Status bar */
.status-bar {
  max-width: 1200px;
  margin: 14px auto 0;
  padding: 0 20px;
  color: var(--text-dim);
  font-size: 13px;
  min-height: 18px;
}

/* The Free to keep and Saved lists. Hidden unless js/app.js (showShelf) or
   functions/free.js opens the section. */
.steam-shelves:not(.is-open) { display: none; }
/* A list's heading, as the pages' (/sales, /calendar): its name large, a line on what it shows, the way out beside it. */
.shelf-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin: 0 0 16px; }
.shelf-titles { min-width: 0; }
.shelf-head h2 { margin: 0; font-family: var(--heading-font); font-weight: 800; font-size: clamp(26px, 3.4vw, 38px); line-height: 1.05; letter-spacing: -0.035em; }
.shelf-lead { margin: 8px 0 0; max-width: 62ch; color: var(--text-dim); font-size: 15px; line-height: 1.5; }
.shelf-close {
  height: 36px;
  padding: 0 14px;
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  background: var(--glass);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.shelf-close:hover { border-color: var(--accent); }
.steam-shelves {
  max-width: 1200px;
  margin: 20px auto 0;
  padding: 0 20px;
  display: flex;
  flex-direction: column;
  gap: 22px;
}

/* "See all" changes target with the tab, so it lives in the strip rather than
   being repeated per shelf. margin-left:auto pins it to the right edge. */

.steam-shelf-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 0 0 12px;
}

.steam-shelf-title {
  font-family: var(--heading-font);
  font-weight: 700;
  font-size: 16px;
  margin: 0;
}

/* Sits in the grid as the last tile, filling cells the final row leaves empty.
   Dashed rather than solid so it reads as an affordance and not as a game card
   whose artwork failed to load — the distinction matters when it is standing
   next to eleven real ones. */
.shelf-see-all-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 100%;
  padding: 12px;
  background: transparent;
  border: 1px dashed var(--border-strong);
  border-radius: var(--tile-radius);
  color: var(--text-dim);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.18s ease, color 0.18s ease, background 0.18s ease;
}

/* Accent on the BORDER, full-strength text on the label — the same split
   .theme-toggle:hover already uses. Colouring the label with var(--accent)
   was the obvious version and fails 4.5:1 in both themes (4.0 dark, 4.35
   light); the brand purple cannot carry text anywhere on this site. */
.shelf-see-all-tile:hover {
  border-color: var(--accent);
  color: var(--text);
  background: var(--bg-elev);
}

.shelf-see-all-icon {
  font-size: 18px;
  line-height: 1;
  font-weight: 400;
}

.shelf-see-all-label { line-height: 1.3; }

/* A wrapping grid rather than a horizontal carousel. The shelf holds 100
   entries, of which the slider showed about seven — the other 93 sat behind a
   sideways scroll almost nobody performs. A grid shows a screenful at once and
   reflows to the viewport instead of running off the edge.
   The count is capped in js/app.js (see SHELF_COLLAPSED_COUNT); rendering all
   100 here would be roughly 4,000px of chart above the deals. */
.steam-shelf-row {
  display: grid;
  /* Four across, three, two: the deal grid's columns, so the lists read as part of the same page. The "See all"
     tile counts the columns it is given (shelfColumnCount in js/app.js), so it follows. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--tile-gap);
  /* A list's lead card (is-feature) spans two rows: the cards after it fill the cells beside it. */
  grid-auto-flow: row dense;
  padding-bottom: 4px;
  /* Reserved so filling this in does not shove the filter bar and the entire
     deal grid down the page. PageSpeed measured CLS 0.478 — nearly five times
     the 0.1 "good" threshold, and a Core Web Vital — with this and the grid
     below expanding from zero after their fetches land.
     Only /free, whose list is open on load, still reserves anything (below):
     the other lists open on a press, so nothing is waiting for them. */
  min-height: 0;
  /* Rows at their own height, not stretched to fill the reservation: with
     one row of cards (a short Saved list) the cards grew to 346px tall. */
  align-content: start;
}
/* Opened on demand, and as long as the list is, so none holds room for cards
   that have not come (only /free, below, which is open when the page loads). */
@media (min-width: 801px) {
  /* Two rows of the 4-column cards (7 and the "See all" tile): measured 472px at 1280px wide. */
  #freeRow { min-height: 472px; }
}
@media (max-width: 1080px) {
  .steam-shelf-row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 800px) {
  .steam-shelf-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The loading and empty-state messages are single children of the grid and
   would otherwise be squeezed into one 146px column. */
.steam-shelf-row .steam-shelf-loading { grid-column: 1 / -1; }


.steam-shelf-loading {
  color: var(--text-dim);
  font-size: 13px;
  margin: 0;
}
.shelf-retry {
  min-height: 44px;
  padding: 0 4px;
  border: 0;
  background: none;
  color: var(--link);
  font: inherit;
  font-weight: 600;
  text-decoration: underline;
  cursor: pointer;
}

/* The shelf is context, not the product: at its old 200px its art was the
   loudest thing on the page while the deals sat underneath as small grey rows,
   which inverts the hierarchy for a site people come to for prices. The width
   now comes from the grid's 146px minimum track (see .steam-shelf-row) rather
   than a flex basis, so cards stretch to fill the row evenly instead of
   leaving a ragged gap at the end. */
/* The deal cards' shape (see .deal-card): artwork on top, rounded, lifting on hover with the accent's glow. */
.steam-card {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--tile-radius);
  overflow: hidden;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 2px 10px -6px rgba(0,0,0,0.5);
  transition: transform 0.22s cubic-bezier(.2,.8,.2,1), border-color 0.22s ease, box-shadow 0.22s cubic-bezier(.2,.8,.2,1);
}
.steam-card:hover {
  transform: translateY(-3px);
  border-color: var(--accent);
  box-shadow: 0 20px 44px -26px var(--glow), 0 0 0 1px var(--glow);
}

.steam-card-thumb-wrap {
  position: relative;
  overflow: hidden;
}

.steam-card img, .steam-card-thumb-empty {
  width: 100%;
  aspect-ratio: 460 / 215;
  object-fit: cover;
  display: block;
  background: var(--bg-elev-2);
  transition: transform 0.35s cubic-bezier(.2,.8,.2,1);
}
.steam-card:hover img { transform: scale(1.05); }

/* The rank, in the deal badges' shape and colours. */
.steam-card-rank {
  position: absolute;
  top: 10px;
  left: 10px;
  background: var(--accent);
  color: var(--accent-ink);
  font-family: var(--heading-font);
  font-weight: 700;
  font-size: 14px;
  line-height: 1.05;
  padding: 3px 9px 4px;
  border-radius: 10px;
  box-shadow: 0 8px 24px -8px rgba(0,0,0,0.6);
}


.steam-card-body { padding: 12px 14px 14px; }

.steam-card-title {
  font-family: var(--heading-font);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.25;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: 2.5em;
}

.steam-card-meta {
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 6px;
}

/* The price leads, in the accent like the deal cards'; free to play in the free violet. */
.steam-card-price-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 8px;
  margin-top: 8px;
  font-size: 13px;
  min-height: 1.4em;
}
.steam-card-price-row .price-new {
  font-size: 20px;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.steam-card-price-row .price-new.free { color: var(--free); font-size: 16px; }
.steam-card-price-row .price-old { font-size: 12px; font-variant-numeric: tabular-nums; }
/* An upcoming giveaway: listed, but visibly not claimable yet. */
.price-new.free-soon { color: var(--text-dim); }

/* A card is a link: to the game's page and popup (every store's price), or to where a giveaway is claimed. */
a.steam-card { display: block; color: inherit; text-decoration: none; }
a.steam-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.steam-card-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; }
.steam-card-foot .steam-card-price-row { flex: 1 1 auto; min-width: 0; }
.steam-card-go {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--chip);
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1;
  transition: background 0.18s ease, color 0.18s ease;
}
.steam-card:hover .steam-card-go,
a.steam-card:focus-visible .steam-card-go { background: var(--accent); color: var(--accent-ink); }

/* The lead card of Most played and New releases: two columns by two rows (the whole width on two columns, and the picture
   beside the words from 521 to 800px), the picture over words at their largest, and the way in as a button. */
.steam-card.is-feature { grid-column: span 2; grid-row: span 2; display: flex; flex-direction: column; }
.steam-card.is-feature .steam-card-thumb-wrap { flex: none; }
.steam-card.is-feature .steam-card-body { flex: 1; display: flex; flex-direction: column; justify-content: flex-start; gap: 6px; padding: var(--tile-pad); }
.steam-card.is-feature .steam-card-title { order: 1; font-size: clamp(24px, 2.6vw, 34px); font-weight: 800; letter-spacing: -0.03em; line-height: 1.1; -webkit-line-clamp: 3; min-height: 0; }
.steam-card.is-feature .steam-card-meta { order: 0; margin: 0; font-size: 13.5px; }
.steam-card.is-feature .steam-card-foot { order: 2; margin-top: 4px; }
.steam-card.is-feature .steam-card-price-row { margin-top: 4px; }
.steam-card.is-feature .steam-card-price-row .price-new { font-size: 30px; }
.steam-card.is-feature .steam-card-price-row .price-new.free { font-size: 22px; }
.steam-card.is-feature .steam-card-go { display: none; }
.steam-card.is-feature .steam-card-rank { top: 14px; left: 14px; font-size: 18px; padding: 4px 12px 5px; border-radius: 12px; }
.steam-card-cta {
  order: 3;
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 44px;
  margin-top: auto;
  padding: 0 20px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 14.5px;
  font-weight: 600;
}
@media (max-width: 800px) {
  .steam-card.is-feature { grid-column: 1 / -1; grid-row: auto; flex-direction: row; }
  .steam-card.is-feature .steam-card-thumb-wrap { width: 48%; }
  .steam-card.is-feature .steam-card-thumb-wrap img,
  .steam-card.is-feature .steam-card-thumb-empty { height: 100%; aspect-ratio: auto; }
  .steam-card.is-feature .steam-card-title { font-size: 24px; }
  .steam-card.is-feature .steam-card-body { justify-content: center; }
  .steam-card.is-feature .steam-card-cta { margin-top: 10px; }
}
@media (max-width: 520px) {
  .steam-card.is-feature { flex-direction: column; }
  .steam-card.is-feature .steam-card-thumb-wrap { width: auto; }
  .steam-card.is-feature .steam-card-thumb-wrap img,
  .steam-card.is-feature .steam-card-thumb-empty { height: auto; aspect-ratio: 460 / 215; }
}

@media (max-width: 640px) {
  .steam-card-body { padding: 10px 10px 11px; }
  .steam-card-title { font-size: 14px; }
  .steam-card-price-row .price-new { font-size: 18px; }
  .steam-card-rank { top: 7px; left: 7px; font-size: 12px; padding: 2px 7px 3px; border-radius: 8px; }
  .steam-card.is-feature .steam-card-body { padding: var(--tile-pad); }
  .steam-card.is-feature .steam-card-title { font-size: 22px; }
  .steam-card.is-feature .steam-card-rank { top: 10px; left: 10px; font-size: 14px; padding: 3px 9px 4px; border-radius: 10px; }
}

.shelf-credit {
  margin: 8px 0 0;
  font-size: 12px;
  color: var(--text-dim);
}
.shelf-credit a { color: inherit; text-decoration: underline; }

/* Deal list */
/* Art cards since the 2026 redesign: the game's artwork on top, the discount
   badge on the art, then title, price and the store. Four across on a desktop,
   two on a phone — one card per row there made 22 deals about 7,000px long. */
.deal-grid {
  max-width: 1200px;
  margin: 12px auto 0;
  padding: 0 20px;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--tile-gap);
  /* Rows at their own height, not stretched to fill the reservation below.
     The same trap as .steam-shelf-row above: a category with eight deals made
     every card taller, because the rows were sharing 55vh between them. */
  align-content: start;
}
@media (max-width: 1080px) {
  .deal-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 800px) {
  .deal-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Space held for deals that have not arrived: the empty grid index.html ships
   and the skeleton cards app.js puts in it while fetching. A viewport-relative
   reservation rather than a fixed one, because no single number fits both a
   desktop and a phone. CLS only counts shifts of content that is actually
   visible, so covering the part of the grid inside the viewport is what
   matters — anything reflowing below the fold is free. 55vh does that.
   Only while loading: held after the deals arrived, a filter that found 11
   left a blank block the height of half a screen above "Load more deals". */
.deal-grid:empty,
.deal-grid:has(> .skeleton) { min-height: 55vh; }

.deal-card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  text-decoration: none;
  color: var(--text);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--tile-radius);
  overflow: hidden;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 2px 10px -6px rgba(0,0,0,0.5);
  transition: transform 0.22s cubic-bezier(.2,.8,.2,1), box-shadow 0.22s cubic-bezier(.2,.8,.2,1), border-color 0.22s ease;
}

.deal-card:hover {
  transform: translateY(-3px);
  border-color: var(--accent);
  box-shadow: 0 20px 44px -26px var(--glow), 0 0 0 1px var(--glow);
}

/* Steam's header art is 460x215; the box keeps that shape for every store, so
   a row of cards lines up whatever artwork each one gets. */
.deal-art {
  position: relative;
  aspect-ratio: 460 / 215;
  background: var(--bg-elev-2);
  overflow: hidden;
}

.deal-art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.3s cubic-bezier(.2,.8,.2,1);
}
.deal-card:hover .deal-art img { transform: scale(1.05); }
/* A tiered bundle: no percentage, a plain "Bundle" label, and what the whole set is worth beside the "from" price. */
.savings-badge.tier-bundle { background: var(--bg-elev-2, #2a2f3a); color: var(--text, #edf4f6); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18); }
.price-worth { font-size: 12.5px; color: var(--text-dim, #94a3b8); white-space: nowrap; }
/* A deal with no picture of its own: the store's logo on a plain panel. */
.art-empty {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, var(--bg-elev-2), var(--bg-elev-1, var(--bg-elev-2)));
}
.deal-art .art-empty .art-empty-logo,
.deal-card:hover .deal-art .art-empty .art-empty-logo {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  background: #fff;
  object-fit: contain;
  opacity: 0.92;
  transform: none;
}

.deal-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 14px 14px;
}

/* Two lines: at one, the part cut off was the edition — "Rims Racing :
   Ultimate …" — which is what tells two deals for one game apart. */
.deal-title {
  font-family: var(--heading-font);
  font-size: 16px;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.015em;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Score, "Lowest ever" and "Region-locked". Its height is held even when it
   is empty, so a label that arrives after the card renders moves nothing. */
.deal-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 15px;
  font-size: 11.5px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
}

.metacritic {
  font-weight: 700;
  padding: 1px 5px;
  border-radius: 4px;
  font-size: 11px;
  flex-shrink: 0;
}
/* #1e9e63 was 3.43:1 under white text; #17804f is 4.96:1. */
.metacritic.high { background: #17804f; color: #fff; }
.metacritic.mid { background: #c9a227; color: #111; }
.metacritic.low { background: #c9432f; color: #fff; }

/* "Lowest ever" (isHistoricalLow in js/app.js). In the meta row so it can
   arrive after the card renders without changing the card's height.
   Outlined, not filled: on the default sort about half the grid is at its
   low (measured 50 of 97), and a filled tag on every other card would drown
   out the discount badges beside it. */
.low-badge {
  flex-shrink: 0;
  font-size: 10.5px;
  font-weight: 700;
  /* 13px + the 2px border = 15px: the height of a meta row without the
     label, so the text beside it does not move when it arrives. */
  line-height: 13px;
  padding: 0 5px 0 3px;
  border-radius: 4px;
  /* --discount-text, not --accent-2: the latter has no light-theme value and
     is 1.77:1 on a white card. This one is contrast-checked in both. */
  color: var(--discount-text);
  border: 1px solid currentColor;
  /* The mark (the logo's small drawing) in front of the words; 11px keeps the row 15px tall. */
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
.low-badge .low-mark { flex: none; display: block; }

/* "Region-locked" (resolveRegionLocks in js/app.js): the key will not
   activate in the visitor's country. Same box as .low-badge, so it can also
   arrive after the card renders without moving anything; the country is in
   its title and in the game window's row. */
.region-badge {
  flex-shrink: 0;
  font-size: 10.5px;
  font-weight: 700;
  line-height: 13px;
  padding: 0 5px;
  border-radius: 4px;
  color: var(--warn-text);
  border: 1px solid currentColor;
}

/* The same warning on its store's row in the game window, under the name. */
.modal-store-region {
  display: block;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--warn-text);
  white-space: normal;
}

/* Discount intensity, carried three ways at once: fill, weight and size.
   Every badge used to render in the same muted green, so -20% and -95% looked
   identical on a site whose whole job is telling those apart. The fill runs
   from a quiet outline at the low end to the solid accent, and on the cards
   the type itself gets heavier and larger with the discount
   (kineticBadgeStyle in js/app.js sets --k from 0 to 1), so the best deals
   are findable by glance rather than by reading — without colour vision too.

   Thresholds match the Min. discount dropdown's 25/50/75, so the page and the
   control agree on what counts as a big discount. */
.savings-badge {
  font-family: var(--heading-font);
  font-weight: 700;
  font-size: 12px;
  padding: 3px 9px 4px;
  border-radius: 10px;
  line-height: 1.05;
  white-space: nowrap;
  border: 1px solid transparent;
  font-variant-numeric: tabular-nums;
}

/* On the art: solid fills only (a translucent one would take the artwork's
   colour), a shadow to lift it off a busy picture, and the kinetic size. */
.deal-art .savings-badge {
  position: absolute;
  left: 10px;
  top: 10px;
  font-size: calc(14px + var(--k, 0) * 12px);
  letter-spacing: calc(-0.01em - var(--k, 0) * 0.035em);
  box-shadow: 0 8px 24px -8px rgba(0,0,0,0.6);
}

/* The heart on a game's card (js/app.js heartHtml): saves it, and on the Saved list, where it is filled, removes it. Top right of the
   artwork, opposite the discount badge; always shown (a touch screen has no hover), over a dark disc so it reads on any picture. */
.heart-btn {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: rgba(8, 12, 19, 0.58);
  border: 1px solid rgba(255, 255, 255, 0.28);
  color: #fff;
  cursor: pointer;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  transition: transform 0.14s ease, background 0.14s ease, color 0.14s ease, border-color 0.14s ease;
  -webkit-tap-highlight-color: transparent;
}
.heart-btn .btn-ico { width: 18px; height: 18px; margin: 0; vertical-align: 0; }
.heart-btn:hover { background: rgba(8, 12, 19, 0.78); border-color: rgba(255, 255, 255, 0.5); transform: scale(1.08); }
.heart-btn.is-on { color: #ff6b88; border-color: rgba(255, 107, 136, 0.55); }
.heart-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The list layout's small picture, and the phone's two-column cards. */
html[data-layout="list"] .deal-art .heart-btn { top: 6px; right: 6px; width: 28px; height: 28px; }
html[data-layout="list"] .deal-art .heart-btn .btn-ico { width: 15px; height: 15px; }
@media (max-width: 640px) {
  .heart-btn { top: 7px; right: 7px; width: 30px; height: 30px; }
  .heart-btn .btn-ico { width: 16px; height: 16px; }
}
/* Said to a screen reader and not shown (js/app.js announceHeart). */
.heart-live { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* Under 25%: deliberately quiet. Most of the grid sits here, and shouting
   about every small discount is what made the big ones invisible. */
.savings-badge.tier-low {
  background: var(--bg-elev);
  color: var(--text-dim);
  border-color: var(--border);
}

.savings-badge.tier-mild {
  background: var(--discount-bg);
  color: var(--discount-text);
}

.savings-badge.tier-warm {
  background: var(--tier-warm-bg);
  color: var(--tier-warm-text);
}

.savings-badge.tier-hot {
  background: var(--tier-hot-bg);
  color: var(--tier-hot-text);
}
.deal-art .savings-badge.tier-hot { box-shadow: 0 8px 24px -8px rgba(0,0,0,0.6), 0 0 22px -4px var(--tier-hot-glow); }

/* Free games are violet everywhere: not a bigger discount but a different
   kind of thing. */
.savings-badge.tier-free {
  background: var(--free);
  color: var(--free-ink);
  letter-spacing: 0.02em;
}

.deal-price-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 8px;
  margin-top: auto;
}

.price-old {
  color: var(--price-old);
  text-decoration: line-through;
  font-size: 11px;
}

.price-new {
  font-family: var(--heading-font);
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.01em;
}

/* The single most-scanned number on the page, so the card leads with it. */
.deal-price-row .price-new {
  font-size: 24px;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.deal-price-row .price-old { font-size: 12.5px; font-variant-numeric: tabular-nums; }
/* Same token as .search-dropdown-price.free. */
.price-new.free,
.deal-price-row .price-new.free { color: var(--free); }

/* The 30-day price line under a card's price (priceLineHtml in js/app.js). The slot is in every card from the start, so a line that
   arrives after the card is drawn changes nothing about its size. */
.deal-spark { height: 26px; flex: none; }
.deal-spark .spark { display: block; width: 100%; height: 100%; }
.spark-base { fill: none; stroke: var(--border-strong); stroke-width: 1; stroke-dasharray: 2 3; vector-effect: non-scaling-stroke; }
.spark-area { fill: var(--accent); fill-opacity: 0.14; }
.spark-line { fill: none; stroke: var(--accent); stroke-width: 1.6; stroke-linejoin: round; vector-effect: non-scaling-stroke; }

/* Where the deal is: the store's logo and name, under a hairline. */
.deal-store {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  font-size: 12.5px;
  color: var(--text-dim);
  white-space: nowrap;
  min-width: 0;
}
.deal-store img {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  border-radius: 5px;
  object-fit: contain;
  /* A light backing: some store icons are dark on transparent and would vanish on the dark theme's cards. */
  background: #fff;
}
.deal-store span {
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 640px) {
  .deal-art .savings-badge {
    left: 7px;
    top: 7px;
    padding: 2px 7px 3px;
    border-radius: 8px;
    font-size: calc(12px + var(--k, 0) * 6px);
  }
  .deal-body { padding: 10px 10px 11px; gap: 6px; }
  .deal-title { font-size: 14px; }
  .deal-price-row .price-new { font-size: 19px; }
  .deal-price-row .price-old { font-size: 11px; }
  .deal-store { padding-top: 8px; gap: 6px; font-size: 11.5px; }
  .deal-store img { width: 16px; height: 16px; }
}

/* The list layout: one card per row, the artwork at the left, the title and
   score beside it, the price and the store at the right. */
html[data-layout="list"] .deal-grid { grid-template-columns: minmax(0, 1fr); gap: 10px; }
html[data-layout="list"] .deal-card { flex-direction: row; align-items: stretch; border-radius: 16px; }
html[data-layout="list"] .deal-card:hover { transform: translateY(-2px); }
html[data-layout="list"] .deal-art { flex: none; width: 190px; aspect-ratio: 460 / 215; }
html[data-layout="list"] .deal-art .savings-badge {
  left: 8px;
  top: 8px;
  font-size: calc(13px + var(--k, 0) * 9px);
  padding: 2px 8px 3px;
  border-radius: 9px;
}
html[data-layout="list"] .deal-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 120px auto auto;
  grid-template-areas: "title spark price store" "meta spark price store";
  align-content: center;
  align-items: center;
  column-gap: 24px;
  row-gap: 4px;
  padding: 10px 18px;
}
html[data-layout="list"] .deal-title { grid-area: title; font-size: 17px; -webkit-line-clamp: 1; align-self: end; }
html[data-layout="list"] .deal-spark { grid-area: spark; align-self: center; }
html[data-layout="list"] .deal-meta { grid-area: meta; align-self: start; }
html[data-layout="list"] .deal-price-row { grid-area: price; margin-top: 0; flex-wrap: nowrap; justify-content: flex-end; align-items: baseline; }
html[data-layout="list"] .deal-price-row .price-new { font-size: 26px; }
html[data-layout="list"] .deal-store { grid-area: store; width: 150px; padding-top: 0; border-top: 0; }
html[data-layout="list"] .skeleton { min-height: 96px; }
/* On a wide screen a row ran the whole 1160px of the page, which put the price
   about 600px from its title across empty space, and the eye lost its line
   between them. From the width where each of two columns is still 570px, rows
   sit two across, the store goes under the title (its own column took 150px of
   a row that needs them for the name) and the price stays at the right. */
@media (min-width: 1160px) {
  html[data-layout="list"] .deal-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  html[data-layout="list"] .deal-body {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "title price" "meta price" "store price";
    column-gap: 16px;
    row-gap: 3px;
  }
  html[data-layout="list"] .deal-spark { display: none; }
  html[data-layout="list"] .deal-title { font-size: 16px; }
  html[data-layout="list"] .deal-price-row .price-new { font-size: 23px; }
  html[data-layout="list"] .deal-store { width: auto; align-self: start; }
}
@media (max-width: 640px) {
  html[data-layout="list"] .deal-grid { gap: 8px; }
  html[data-layout="list"] .deal-art { width: 118px; }
  html[data-layout="list"] .deal-art .savings-badge { left: 6px; top: 6px; font-size: calc(11px + var(--k, 0) * 6px); padding: 1px 6px 2px; border-radius: 7px; }
  html[data-layout="list"] .deal-body {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "title title" "meta meta" "store price";
    row-gap: 3px;
    column-gap: 10px;
    padding: 8px 10px;
  }
  html[data-layout="list"] .deal-spark { display: none; }
  html[data-layout="list"] .deal-title { font-size: 14px; -webkit-line-clamp: 2; align-self: start; }
  html[data-layout="list"] .deal-price-row .price-new { font-size: 19px; }
  html[data-layout="list"] .deal-store { width: auto; min-width: 0; }
}

/* Load more */
/* Steam wishlist checker (/wishlist). */
.wishlist-tool {
  max-width: 1200px;
  margin: 20px auto 0;
  padding: 0 20px;
}
.wishlist-title {
  font-family: var(--heading-font);
  font-size: 22px;
  margin: 0 0 6px;
}
.wishlist-intro {
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.5;
  margin: 0 0 14px;
  max-width: 640px;
}
.wishlist-form { display: flex; gap: 8px; max-width: 640px; }
.wishlist-form input {
  flex: 1;
  min-width: 0;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 999px;
  padding: 11px 16px;
  font-size: 14px;
}
.wishlist-form input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.wishlist-form button {
  background: var(--accent);
  color: var(--accent-ink);
  border: 0;
  border-radius: 999px;
  padding: 11px 20px;
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
}
.wishlist-form button:disabled { opacity: 0.6; cursor: default; }
.wishlist-status {
  color: var(--text-dim);
  font-size: 13px;
  margin: 12px 0 0;
  min-height: 18px;
}
.wishlist-forget {
  background: none;
  border: 0;
  padding: 0;
  margin-top: 4px;
  color: var(--text-dim);
  font-size: 12px;
  text-decoration: underline;
  cursor: pointer;
}
/* Same cards as the main grid, but inside the section's own padding, and
   without the 55vh space the main grid reserves against layout shift. */
.deal-grid.wishlist-results { min-height: 0; max-width: none; margin: 12px 0 0; padding: 0; }
.wishlist-tool .sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
@media (max-width: 640px) {
  .wishlist-form { flex-direction: column; }
}

/* /wishlist as a bento box on the site's tile grid (the markup is in index.html, the questions in
   functions/wishlist.js):

     4 columns   [ the checker (3 wide)                          | wishlist not showing? ]
                 [ the price-alert email, once a wishlist is checked, across the page    ]
                 [ the games on sale: the deal grid's own cards                           ]
                 [ the questions across the page, in two columns                          ]

   3 columns: the checker two wide, the tip beside it. 2 columns and a phone: one tile to a row. */
.wl-bento {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--tile-gap);
}
.wl-bento .tile { margin: 0; max-width: none; justify-content: flex-start; gap: 14px; }
.wl-main { grid-column: span 3; min-height: var(--tile-row); }
.wl-tip { grid-column: span 1; }
.wl-alert { grid-column: 1 / -1; }
.wl-alert[hidden] { display: none; }

.wl-main .wishlist-title {
  margin: 0;
  font-weight: 800;
  font-size: clamp(32px, 5vw, 48px);
  line-height: 1.04;
  letter-spacing: -0.035em;
}
.wl-main .wishlist-intro { margin: 0; max-width: 62ch; font-size: 16px; line-height: 1.5; }
.wl-main .wishlist-form { max-width: 640px; gap: 10px; }
.wl-main .wishlist-form input { height: 52px; padding: 0 18px; border-radius: 16px; font-size: 16px; }
.wl-main .wishlist-form button { height: 52px; padding: 0 26px; border-radius: 16px; font-size: 15px; }
.wl-main .wishlist-status { margin: 0; min-height: 20px; font-size: 14px; line-height: 1.45; }
.wl-main .wishlist-forget { align-self: flex-start; margin: -6px 0 0; padding: 8px 0; font-size: 13px; }
.wl-main > .tile-label { align-self: flex-start; }
.wl-bento h2.tile-label {
  margin: 0;
  align-self: flex-start;
  height: auto;
  min-height: 28px;
  padding-top: 4px;
  padding-bottom: 4px;
  line-height: 1.2;
  white-space: normal;
}

/* What to do when the wishlist is private: three steps. */
.wl-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; counter-reset: step; }
.wl-steps li { display: flex; align-items: center; gap: 12px; font-size: 14px; line-height: 1.35; counter-increment: step; }
.wl-steps li::before {
  content: counter(step);
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 12.5px;
  font-weight: 800;
}
.wl-steps b { font-weight: 700; }
.wl-tip p { margin: 0; font-size: 13px; line-height: 1.5; color: var(--text-dim); }

/* The price-alert box, once a wishlist is checked: a strip of the box, the button beside what it covers. */
.wl-alert { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 10px 20px; padding-top: 16px; padding-bottom: 16px; }
.wl-alert .watch-price-btn { width: auto; flex: none; }
.wl-alert .bulk-alert-hint { margin: 0; }
.wl-alert .watch-price-form { flex: 1 1 320px; max-width: 520px; }
.wl-alert .watch-price-note { flex-basis: 100%; }
.wl-alert .watch-price-note:empty { display: none; }

/* The games on sale, the deal grid's own cards, the box's gutter above them (none while empty). */
.deal-grid.wishlist-results { margin-top: var(--tile-gap); }
.deal-grid.wishlist-results:empty { margin-top: 0; }

/* The questions: a tile across the page, two columns. */
.wl-wrap { max-width: 1200px; margin: var(--tile-gap) auto 0; padding: 0 20px; }
.wl-faq { margin: 0; max-width: none; justify-content: flex-start; gap: 14px; }
.wl-faq .faq-items { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 40px; align-items: start; }
.wl-faq .wishlist-faq-item { max-width: none; }

@media (max-width: 1080px) {
  .wl-bento { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .wl-main { grid-column: span 2; }
}
@media (max-width: 800px) {
  .wl-bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .wl-main, .wl-tip { grid-column: 1 / -1; min-height: 0; }
  .wl-faq .faq-items { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 640px) {
  .wl-wrap { padding: 0 12px; }
  .wl-main .wishlist-form { flex-direction: column; }
  /* flex: none: the input's flex: 1 is a zero height in a column, and its 52px would be ignored. */
  .wl-main .wishlist-form input { flex: none; width: 100%; }
  .wl-main .wishlist-form button { width: 100%; }
}

/* /wishlist's questions (faqHtml in functions/wishlist.js): one line each
   until opened, so they sit under the checker without pushing it away. */
.wishlist-faq {
  max-width: 1200px;
  /* Small: the checker above ends in its status line and results grid, whose
     own margins already leave room for a message. */
  margin: 8px auto 0;
  padding: 0 20px;
}
.wishlist-faq h2 {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.wishlist-faq-item {
  max-width: 720px;
  border-bottom: 1px solid var(--border);
}
.wishlist-faq-item summary {
  cursor: pointer;
  list-style: none;
  padding: 9px 22px 9px 0;
  font-size: 14px;
  font-weight: 600;
  position: relative;
}
.wishlist-faq-item summary::-webkit-details-marker { display: none; }
.wishlist-faq-item summary::after {
  content: "+";
  position: absolute;
  right: 2px;
  top: 7px;
  color: var(--text-dim);
  font-weight: 400;
  font-size: 16px;
}
.wishlist-faq-item[open] summary::after { content: "−"; }
.wishlist-faq-item summary:hover { color: var(--accent); }
.wishlist-faq-item summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.wishlist-faq-item p {
  margin: 0 0 12px;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.55;
}

/* The way in, under the shelves. Plain text colour rather than the accent:
   accent purple is 4.35:1 on the light theme, under what 13.5px text needs. */
.wishlist-cta {
  max-width: 1200px;
  margin: 14px auto 0;
  padding: 0 20px;
  font-size: 13.5px;
  font-weight: 600;
}
.wishlist-cta a { color: var(--text); text-decoration: none; }
.wishlist-cta a:hover { text-decoration: underline; }
.wishlist-cta span { color: var(--accent); }

/* Landing-page heading (functions/_landing.js): the page's name large, a line on what it shows, and the pages
   beside it as pills. The same on /store, /genre, /under, /top-rated, /bundles, /free, /black-friday, /sales and
   /wishlist's neighbours, and the same size as the wishlist checker's title, so every page with an <h1> reads as one
   family. A game page's intro (.game-intro) is a title among prices, so it keeps the compact one. */
.landing-intro {
  max-width: 1200px;
  margin: 20px auto 4px;
  padding: 0 20px;
}
.landing-intro h1 {
  font-family: var(--heading-font);
  font-size: 22px;
  margin: 0 0 6px;
}
.landing-intro p {
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.5;
  margin: 0 0 10px;
  max-width: 720px;
}
.landing-intro:not(.game-intro) { margin-top: 28px; }
.landing-intro:not(.game-intro) h1 {
  font-weight: 800;
  font-size: clamp(32px, 5vw, 48px);
  line-height: 1.04;
  letter-spacing: -0.035em;
  margin: 0;
}
.landing-intro:not(.game-intro) p { margin: 12px 0 16px; max-width: 64ch; font-size: 17px; line-height: 1.5; }
.landing-links {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.landing-links a {
  font-size: 12px;
  color: var(--text-dim);
  text-decoration: none;
  padding: 3px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
}
.landing-intro:not(.game-intro) .landing-links a { font-size: 13px; padding: 6px 12px; }
.landing-links a:hover {
  border-color: var(--accent);
  color: var(--text);
}
/* A game page's own prices (gameIntroHtml in functions/game/[[path]].js).
   Plain and compact: the popup app.js opens over it is the rich view, this is
   the page underneath that says what the address is about. */
.game-prices {
  border-collapse: collapse;
  font-size: 13px;
  margin: 2px 0 10px;
  min-width: min(100%, 360px);
}
.game-prices caption {
  text-align: left;
  color: var(--text-dim);
  font-size: 12px;
  padding-bottom: 4px;
}
.game-prices th,
.game-prices td {
  text-align: left;
  padding: 5px 16px 5px 0;
  border-bottom: 1px solid var(--border);
}
.game-prices th {
  color: var(--text-dim);
  font-weight: 600;
  font-size: 12px;
}
.game-prices td:nth-child(n+2),
.game-prices th:nth-child(n+2) {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.game-prices td:last-child,
.game-prices th:last-child { padding-right: 0; }
/* Price history: the game page's section (priceHistoryHtml in
   functions/game/[[path]].js) and the popup's (loadPriceChart in js/app.js)
   draw the same chart, priceChartHtml in both files. The figures above it, the
   line over a price axis, the dates under it. The SVG holds only the shapes
   (it is stretched to the column); the dots, price labels and the hover tooltip
   are HTML placed in percent, so they stay round and sharp. */
.price-history {
  margin: 4px 0 10px;
  max-width: 560px;
}
.price-history h2,
.ph-title {
  font-size: 13px;
  color: var(--text-dim);
  font-weight: 600;
  margin: 0 0 4px;
}
.ph-title { margin: 16px 0 6px; }
/* The game page's "Is it a good deal right now?" (dealVerdictHtml in
   functions/game/[[path]].js): a heading like the price history's, then a
   paragraph in the intro's own text style. */
.game-verdict {
  margin: 10px 0;
  max-width: 560px;
}
.game-verdict h2 {
  font-size: 13px;
  color: var(--text-dim);
  font-weight: 600;
  margin: 0 0 4px;
}
.ph-since { font-weight: 500; }
.ph { margin: 6px 0 4px; }
.ph-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 18px;
  margin: 0 0 8px;
}
.ph-stat {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.25;
}
.ph-stat-label {
  font-size: 11px;
  color: var(--text-dim);
}
.ph-stat strong {
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
/* --discount-text, not --accent-2: it has a light-theme value. */
.ph-stat-low strong { color: var(--discount-text); }
.ph-stat small {
  font-size: 11px;
  color: var(--text-dim);
}
.ph-plot {
  position: relative;
  height: 112px;
  border-radius: 8px;
  background: var(--bg-elev-2);
  cursor: crosshair;
  /* Dragging along the chart reads it; dragging up or down still scrolls. */
  touch-action: pan-y;
  -webkit-user-select: none;
  user-select: none;
}
.ph-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  color: var(--accent);
  overflow: hidden;
  border-radius: 8px;
}
.ph-grid {
  stroke: var(--border);
  stroke-width: 1;
  stroke-dasharray: 3 4;
  vector-effect: non-scaling-stroke;
}
.ph-area {
  fill: currentColor;
  fill-opacity: 0.14;
  stroke: none;
}
.ph-line {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.ph-y {
  position: absolute;
  left: 6px;
  padding: 0 3px;
  border-radius: 3px;
  background: var(--bg-elev-2);
  font-size: 10px;
  line-height: 14px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.ph-y-hi { top: 9%; }
.ph-y-lo { bottom: 9%; }
.ph-dot {
  position: absolute;
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--bg-elev-2);
  pointer-events: none;
}
.ph-dot-low { background: var(--discount-text); }
.ph-cursor,
.ph-dot-hover,
.ph-tip { display: none; }
.ph-cursor {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--text-dim);
  opacity: 0.5;
  pointer-events: none;
}
.ph-tip {
  position: absolute;
  bottom: calc(100% + 6px);
  z-index: 2;
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  box-shadow: var(--shadow);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;
}
.ph-plot.is-active .ph-cursor,
.ph-plot.is-active .ph-tip { display: block; }
.ph-plot.is-active .ph-dot-hover { display: block; }
.ph-x {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-top: 5px;
  font-size: 11px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.ph-note {
  margin: 4px 0 0;
  font-size: 11px;
  color: var(--text-dim);
}
/* Which systems a game runs on, and its system requirements (platformsHtml and
   requirementsHtml in functions/game/[[path]].js; the popup builds the same
   markup in js/app.js). Plain text from the game's Steam page, laid out as a
   column per level, side by side where there is room. */
.game-intro-platforms strong { color: var(--text); font-weight: 600; }
.req-wrap { max-width: 760px; margin: 14px 0 10px; }
.game-requirements h2 {
  font-size: 13px;
  color: var(--text-dim);
  font-weight: 600;
  margin: 0 0 8px;
}
.req-os { margin: 0 0 12px; }
.req-os-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  margin: 0 0 4px;
}
.req-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 6px 28px;
}
.req-level {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 0 0 3px;
}
.req-wrap ul {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 13px;
  line-height: 1.5;
}
.req-wrap li { margin: 0 0 3px; color: var(--text); }
.req-wrap li strong { color: var(--text-dim); font-weight: 600; }
.req-wrap p.req-source,
.modal .req-wrap p.req-source {
  font-size: 12px;
  color: var(--text-dim);
  margin: 6px 0 0;
}
.modal .req-wrap { margin: 14px 0 0; max-width: none; }
/* The Metacritic tag: the card's coloured score beside the name, a link. */
/* A genre tag that links to its genre page. */
.genre-tag-link { text-decoration: none; }
.genre-tag-link:hover { border-color: var(--accent); color: var(--text); }
.genre-tag-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.modal .gm-metascore {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding-left: 4px;
  color: var(--text);
  text-decoration: none;
}
.modal a.gm-metascore:hover { border-color: var(--accent); }
.modal a.gm-metascore:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.game-intro-metacritic strong,
.game-intro-deck strong { color: var(--text); font-weight: 600; }
.modal .genre-tag.platform-tag {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-dim);
}
@media (max-width: 640px) {
  .landing-intro.game-intro h1 { font-size: 19px; }
  .landing-intro.game-intro p { font-size: 13px; }
  .landing-intro:not(.game-intro) { margin-top: 22px; padding: 0 12px; }
  .landing-intro:not(.game-intro) p { font-size: 15.5px; }
  .game-prices { width: 100%; }
}

/* No bottom padding: whatever follows (a page's game list, the store links)
   brings its own 34px margin, and the two stacked left a 74px gap under the
   button. With nothing more to load the button hides, and so does its space. */
.load-more-wrap {
  display: flex;
  justify-content: center;
  padding: 28px 20px 0;
}
.load-more-wrap:has(> .load-more-btn.hidden) { padding: 0; }

.load-more-btn {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 10px 24px;
  border-radius: 999px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 600;
}
.load-more-btn:hover { border-color: var(--accent); color: var(--accent); }
.load-more-btn:disabled { opacity: 0.5; cursor: default; }
.load-more-btn.hidden { display: none; }

/* Empty / loading states */
.empty-state, .error-state {
  grid-column: 1 / -1;
  text-align: center;
  padding: 60px 20px;
  color: var(--text-dim);
}
.empty-state p { margin: 0 0 16px; }
/* The empty result's way out (js/app.js emptyStateHtml): the same pill as Reset, but meant to be pressed. */
.empty-reset {
  background: transparent;
  border: 1px solid var(--accent);
  color: var(--text);
  padding: 9px 20px;
  border-radius: 999px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 600;
}
.empty-reset:hover { background: var(--bg-elev); }

/* Where a whole-range store's games at their regular price begin (dealsGridHtml in app.js): the deals are above it. */
.grid-divider {
  grid-column: 1 / -1;
  margin-top: 14px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 14px;
  font-weight: 600;
}

.skeleton {
  pointer-events: none;
}
.skeleton .deal-art {
  background: linear-gradient(90deg, var(--bg-elev-2) 25%, var(--bg-elev) 50%, var(--bg-elev-2) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
}
@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.skeleton .deal-body { gap: 10px; }
.skeleton .bar {
  height: 10px;
  border-radius: 4px;
  background: var(--bg-elev-2);
}
.skeleton .bar.w60 { width: 60%; }
.skeleton .bar.w80 { width: 80%; }
.skeleton .bar.w40 { width: 40%; }

/* The game page (page mode, openGamePage in js/app.js): the popup's content as a page of its own, in the main area. The shelves
   and the list stay in the page, hidden, so Back finds them as they were. A .modal, so that every rule of the popup's parts
   (the price tile, the store rows, the alerts, the chart, About) applies; the box of the popup itself is taken off. */
body.is-game-page main > *:not(#gamePage) { display: none !important; }
.game-page { max-width: 1200px; margin: 0 auto; padding: 12px 20px 28px; }
.game-page[hidden] { display: none; }
.modal.is-page {
  max-width: none;
  max-height: none;
  overflow: visible;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  animation: none;
}
.gp-loading, .gp-fail { color: var(--text-dim); padding: 40px 0; text-align: center; }
.gp { display: grid; gap: 20px; }
.gp-crumbs { font-size: 13px; color: var(--text-dim); }
.gp-crumbs a { color: var(--link); text-decoration: none; }
.gp-crumbs a:hover { text-decoration: underline; text-underline-offset: 3px; }
.gp-hero {
  display: grid;
  grid-template-columns: minmax(0, 340px) minmax(0, 1fr);
  gap: 28px;
  align-items: center;
  padding: 22px;
  border-radius: var(--tile-radius, 22px);
  background: var(--bg-elev);
  border: 1px solid var(--border);
}
.gp-hero.has-no-art { grid-template-columns: minmax(0, 1fr); }
.gp-hero .gm-media { max-width: none; width: 100%; margin: 0; }
.gp-title {
  margin: 0 0 12px;
  font-family: var(--heading-font);
  font-size: clamp(30px, 4.4vw, 46px);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.035em;
  overflow-wrap: anywhere;
}
.gp-hero .gm-head { padding: 0; }
.gp-hero .gm-head .genre-tags { margin: 0 0 12px; }
.gp-hero .gm-head .tagline { margin: 0; color: var(--text-dim); font-size: 15px; line-height: 1.55; max-width: 62ch; }
.gp-grid { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 24px; align-items: start; }
.gp-main { display: grid; gap: 16px; min-width: 0; }
.gp-side { display: grid; gap: 12px; position: sticky; top: 16px; }
/* The popup's order and margins were for its own column. */
.gp .gp-main > *, .gp .gp-side > * { order: 0; margin: 0; }
.gp .gp-side .gm-cta { position: static; margin: 0; padding: 0; background: none; border: 0; box-shadow: none; }
.gp-main .modal-actions { margin: 0; }
@media (max-width: 980px) {
  .gp-grid { grid-template-columns: minmax(0, 1fr); }
  .gp-side { position: static; order: -1; }
  .gp-hero { grid-template-columns: minmax(0, 1fr); gap: 18px; padding: 16px; }
}

/* Modal */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(3,6,12,0.62);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 50;
  padding: 20px;
}
.modal-backdrop.open { display: flex; }

.modal {
  background: var(--bg-elev);
  border: 1px solid var(--border-strong);
  border-radius: 24px;
  max-width: 560px;
  width: 100%;
  max-height: 90vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 20px;
  position: relative;
  box-shadow: 0 44px 110px -30px rgba(0,0,0,0.85), 0 0 90px -30px var(--glow);
}
.modal-backdrop.open .modal { animation: modal-in 0.18s ease-out; }
@keyframes modal-in {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
}
@media (prefers-reduced-motion: reduce) {
  .modal-backdrop.open .modal { animation: none; }
}

.modal .tagline {
  color: var(--text-dim);
  font-size: 13px;
  margin: 0 0 10px;
  line-height: 1.5;
}

/* Popup layout (renderModal in js/app.js), redesigned 2026-10-14 as a band and a bento:

     phone                       from 860px
     [ band: picture, name ]     [ band: picture | name, tags, tagline        ]
     [ price tile            ]   [ price tile  ] [ price history tile ]
     [ stores                ]   [ buy button  ] [ About tile         ]
     [ alerts                ]   [ stores      ] [ requirements       ]
     [ price history, About  ]   [ alerts      ]
     [ buy button, pinned    ]

   The band is the game: its picture over a blurred wash of the picture's own colours, its name, its tags
   and a line of its text. Under it the buying (.gm-buy) and the game's text (.gm-info), each part a tile on
   the popup's own surface. On a phone they stack and the buy button, which sits last in the markup, is
   pinned to the bottom edge of the popup; from 860px they sit side by side, the buying block staying in
   view while the game's text scrolls (on a screen tall enough to hold it).
   The block that holds the price is display: contents on a phone so its parts can be ordered among the
   others. */
.modal { --gm-radius: 18px; }
.gm {
  display: flex;
  flex-direction: column;
}
.gm-buy { display: contents; }
.gm-band { order: 1; }
.gm-deal { order: 3; }
.gm-stores { order: 4; }
.modal-actions { order: 6; }
/* The popup when a game's details could not be loaded: a pause, with two ways on. */
.modal-fail-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.modal-fail-actions .watch-price-btn { width: auto; flex: 1 1 calc(50% - 4px); padding: 10px 14px; text-align: center; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
#nlPrompt { order: 6; }
.gm-info { order: 8; }
.gm-cta { order: 9; }

/* The band: bleeds to the popup's edges (over its padding), the picture's own colours washed behind it. */
.gm-band {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  margin: -20px -20px 16px;
  padding: 18px 20px 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  background: var(--bg-elev);
  border-bottom: 1px solid var(--border);
}
/* Scoped under .modal like the other popup images (`.modal img` sets a width and a margin). Larger than the band, so
   the blur's soft edge falls outside it. */
.modal .gm-band-bg {
  position: absolute;
  inset: -50px;
  width: calc(100% + 100px);
  height: calc(100% + 100px);
  max-width: none;
  margin: 0;
  border-radius: 0;
  object-fit: cover;
  z-index: -1;
  filter: blur(36px) saturate(1.5);
  opacity: 0.5;
  pointer-events: none;
}
/* Readability (2026-10-14): the wash is the picture's glow, not the ground the words sit on. A scrim over it, clear
   by the picture and the popup's own surface by the words, so the tagline and the pills are read on the surface
   whatever the picture's colours are (a blue wash under grey text was the case that was hard to read). */
.gm-band::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(180deg, transparent 0, var(--bg-elev) 60%);
  opacity: 0.88;
  pointer-events: none;
}
/* Solid pills, a step larger, over the band: the translucent platform pills were the faintest thing on it. */
.modal .gm-band .genre-tag,
.modal .gm-band .genre-tag.platform-tag {
  background: var(--bg-elev);
  border-color: var(--border-strong);
  color: var(--text-dim);
  font-size: 12px;
  padding: 4px 10px;
}
.gm-head .tagline { color: var(--text); line-height: 1.6; }

/* A fixed shape, so the capsule that is there at once and the larger header
   image that replaces it (upgradeHero) fill the same box and nothing moves.
   The capsule is wider than the box, so it is shown whole (contain) on the
   box's own tint; the header image is the box's shape and fills it. */
.gm-media {
  aspect-ratio: 460 / 215;
  width: 100%;
  /* Steam's header image is 460 wide: not stretched past that. */
  max-width: 460px;
  margin: 0 auto;
  background: var(--bg-elev-2);
  border-radius: 14px;
  overflow: hidden;
  box-shadow: 0 0 0 1px var(--border-strong), 0 18px 40px -16px rgba(0, 0, 0, 0.55);
}
.modal .gm-hero {
  display: block;
  width: 100%;
  height: 100%;
  margin: 0;
  border-radius: 0;
  object-fit: contain;
}
.modal .gm-hero.is-header { object-fit: cover; }

.gm-head { min-width: 0; }
.modal .gm-head h2 {
  margin: 0 0 10px;
  font-size: 23px;
  line-height: 1.15;
  letter-spacing: -0.02em;
  overflow-wrap: anywhere;
}
.gm-head .genre-tags { margin: 0 0 10px; }
.gm-head .tagline { margin: 0; }

/* The price tile: the accent's own tint, the price as large as anything in the popup, with what it is and where. */
.gm-deal {
  margin: 0 0 12px;
  padding: 18px 20px;
  border-radius: var(--gm-radius);
  background: linear-gradient(155deg, var(--discount-bg), var(--bg-elev-2) 78%);
  border: 1px solid var(--border);
  box-shadow: inset 0 0 0 1px var(--glow), 0 20px 44px -28px var(--glow);
}
.gm-price { margin: 0; }
.gm-price-main { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.modal .gm-price .price-new { font-size: 40px; line-height: 1.05; font-weight: 700; letter-spacing: -0.03em; }
.gm-badge-off {
  background: var(--discount-bg);
  color: var(--discount-text);
  font-size: 13px;
  font-weight: 700;
  padding: 3px 9px;
  border-radius: 999px;
}
.gm-price-sub { color: var(--text-dim); font-size: 13px; margin-top: 4px; }
.gm-ever { color: var(--text-dim); font-size: 12px; margin-top: 4px; }
/* When a sale ends (loadEndTimes in js/app.js): a box of days, hours, minutes and seconds under the headline
   price where the store gives the second, a line where it gives only the day, and a line under a store's name in
   the price list for the others. */
.gm-ends {
  /* Inside the price tile, under the price, on the popup's own surface. */
  margin: 14px 0 0;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--bg-elev);
  text-align: center;
}
.gm-ends-title {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.gm-ends-tiles { display: flex; justify-content: center; gap: 6px; margin-top: 6px; }
.gm-ends-tile {
  min-width: 52px;
  padding: 5px 6px;
  border-radius: 8px;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}
.gm-ends-tile b { display: block; font-size: 19px; line-height: 1.15; color: var(--text); }
.gm-ends-tile span { display: block; font-size: 11px; color: var(--text-dim); }
.gm-ends-day { padding: 8px 12px; }
.gm-ends-day .gm-ends-title { text-transform: none; letter-spacing: 0; font-size: 13px; color: var(--text); }
.modal-store-ends {
  display: block;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--warn-text);
  white-space: normal;
  font-variant-numeric: tabular-nums;
}
.gm-badge-low {
  display: inline-block;
  background: var(--discount-bg);
  color: var(--discount-text);
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 999px;
}

/* The stores: a tile, one row each, the cheapest outlined. */
.gm-stores {
  margin: 0 0 12px;
  padding: 14px 16px 8px;
  border-radius: var(--gm-radius);
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
}
.gm-label {
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  margin: 0 0 6px;
}
/* The tile is the row's own colour, so a hover is the darker surface. */
.gm-stores .modal-store-row:hover,
.gm-stores .modal-store-row.is-current { background: var(--bg-elev); }
.gm-stores .modal-store-row.is-current:hover { background: var(--bg); }
.modal-store-tag {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--discount-bg);
  color: var(--discount-text);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  vertical-align: 1px;
}
.modal-store-row.is-best .modal-store-price { color: var(--discount-text); }
/* The cheapest store, outlined in the accent (the redesign's compare panel). */
.modal-store-row.is-best { border-radius: 10px; box-shadow: inset 0 0 0 1px var(--accent), 0 0 30px -16px var(--glow); }

/* The game's text: the price history, About and the requirements, each a tile. */
.gm-info { display: flex; flex-direction: column; gap: 12px; margin-top: 4px; }
#modalPriceChart:not(:empty),
.modal .about-section {
  padding: 16px 18px;
  border-radius: var(--gm-radius);
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
}
#modalPriceChart .ph-title { margin: 0 0 8px; }
#modalPriceChart .ph-note { font-size: 12px; line-height: 1.5; }
#modalPriceChart .ph { margin-top: 0; }

/* The buy button on a phone: pinned to the bottom edge of the popup, over the
   popup's own bottom padding, so it is one tap away however far the visitor has
   read. At the end of the content it sits where it would have anyway. */
.gm-cta {
  position: sticky;
  /* A sticky offset is measured inside the popup's padding, so -20px (the
     padding) puts the bar flush with the popup's bottom edge. */
  bottom: -20px;
  margin: 12px -20px -20px;
  padding: 12px 20px;
  background: var(--bg-elev);
  border-top: 1px solid var(--border);
  box-shadow: 0 -8px 16px rgba(0, 0, 0, 0.08);
  z-index: 3;
}
.gm-cta .go-to-store { margin-top: 0; }

/* About: about seven lines, the rest behind "Read more". Sized so that on a
   laptop screen (800px tall) the whole popup fits without scrolling. */
.gm-about-text.is-clamped {
  max-height: 9.5em;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent);
  mask-image: linear-gradient(to bottom, #000 70%, transparent);
}
.gm-readmore {
  background: none;
  border: none;
  padding: 4px 0;
  margin: 0 0 6px;
  color: var(--link);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.gm-readmore:hover { text-decoration: underline; }

/* Requirements, folded. */
.modal .req-modal { margin: 12px 0 0; max-width: none; border-top: 1px solid var(--border); }
.modal .req-modal summary {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding: 11px 0;
  cursor: pointer;
  list-style: none;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.modal .req-modal summary::-webkit-details-marker { display: none; }
.modal .req-modal summary::after { content: "▾"; margin-left: auto; padding-left: 8px; }
.modal .req-modal[open] summary::after { content: "▴"; }
.modal .req-modal .req-systems {
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
  order: 2;
}
.modal .req-modal summary:hover { color: var(--text); }
.modal .req-modal summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.modal .req-modal .req-cols { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }

/* Phones: the tagline to two lines, so the price starts higher. */
@media (max-width: 859px) {
  .modal .tagline {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
  }
}

/* A screen wide enough for both blocks. The popup widens: the band across the top, then the buying block
   on the left (the price first, since it is what most visitors came for) and the game's text on the
   right, whose About and requirements are the long part. The buying block keeps its place while that
   scrolls, on a screen tall enough to hold it (on a shorter one a block taller than the window would
   hide its lower half until the end of the page). */
@media (min-width: 860px) {
  .modal { max-width: 1000px; padding: 24px; }
  .gm {
    display: grid;
    grid-template-columns: 380px minmax(0, 1fr);
    column-gap: 24px;
    align-items: start;
  }
  /* The phone's order is for the flex column; here the cells are named. */
  .gm-band, .gm-info, .gm-cta { order: 0; }
  .gm-band {
    grid-column: 1 / -1;
    grid-row: 1;
    flex-direction: row;
    align-items: center;
    gap: 30px;
    margin: -24px -24px 22px;
    padding: 28px 30px 28px 28px;
  }
  .gm-media { flex: 0 0 340px; width: 340px; margin: 0; }
  /* The words are to the right of the picture here: the scrim runs sideways. */
  .gm-band::after { background: linear-gradient(90deg, transparent 0, var(--bg-elev) 42%); }
  /* Room for the close button, which is over the band's corner. */
  .gm-head { flex: 1 1 auto; padding-right: 36px; }
  .gm-buy { display: block; grid-column: 1; grid-row: 2; }
  .gm-info { grid-column: 2; grid-row: 2; margin-top: 0; }
  .gm-buy > * { order: 0; }
  /* A pointer, not a thumb: the rows need not be 44px tall. */
  .modal .modal-store-row { min-height: 40px; padding-top: 6px; padding-bottom: 6px; }
  .modal .gm-head h2 { font-size: 30px; }
  .gm-cta {
    position: static;
    margin: -2px 0 12px;
    padding: 0;
    background: none;
    border-top: 0;
    box-shadow: none;
  }
  .modal .tagline { font-size: 14px; }
}
@media (min-width: 860px) and (min-height: 900px) {
  .gm-buy { position: sticky; top: 12px; }
}

.genre-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 8px;
}
.genre-tags:empty { margin: 0; }

.genre-tag {
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 600;
  padding: 3px 8px;
  border-radius: 999px;
}

.modal .about-section h3 {
  font-size: 13px;
  color: var(--text-dim);
  margin: 0 0 8px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.modal .about-section p {
  font-size: 14px;
  line-height: 1.65;
  margin: 0 0 10px;
  color: var(--text);
}

/* Pinned to the top right of the popup as it scrolls: it used to scroll away
   with the content, which on a phone, a long way down, left no visible way
   out. A float so it takes no room of its own; its margins cancel its height,
   so the content beside it keeps its full width. */
.modal-close {
  position: sticky;
  top: 0;
  float: right;
  /* -8 + 32 - 32: a margin box with no height, so the layout beside it is not
     narrowed to make room (a flex or grid container next to a float is). */
  margin: -8px -8px -32px 8px;
  z-index: 5;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  color: var(--text);
  width: 32px;
  height: 32px;
  border-radius: 50%;
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}
.modal-close:hover { border-color: var(--accent); }
.modal-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The visible circle stays 28px — at 44px it reads as a second button rather
   than a dismiss affordance. This transparent overlay gives it the 44x44 hit
   area Apple and Material both ask for (WCAG 2.2 AA only needs 24, which the
   circle already met; this is about thumbs, not the floor). Pointer events
   land on the pseudo-element because it belongs to the button. */
.modal-close::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

.modal img { width: 100%; border-radius: 8px; margin-bottom: 12px; }
.modal h2 { margin: 0 0 8px; font-family: var(--heading-font); font-size: 18px; }
/* One store row in the comparison list. The whole row is an <a> to that
   store's affiliate link — the price alone used to be the only clickable
   part, which made the target about 45px wide and left the name and logo
   inert even though they look like the thing you would click. */
.modal-store-row {
  display: flex;
  align-items: center;
  gap: 10px;
  /* 44px minimum so the row is a comfortable touch target; the padding is
     what gets it there, since the content is only ~20px tall. */
  min-height: 44px;
  padding: 8px;
  margin: 0 -8px;
  border-bottom: 1px solid var(--border);
  /* Square until it has a background to round: a rounded border on its own
     curls up at both ends into a smile. */
  border-radius: 0;
  text-decoration: none;
  color: inherit;
  transition: background 0.15s ease;
}
.modal-store-row:last-child { border-bottom: none; }
.modal-store-row:hover,
.modal-store-row.is-current { border-radius: 8px; }
.modal-store-row:hover { background: var(--bg-elev-2); }

/* The store the visitor arrived from. Keeps its tint on hover so the row
   still responds rather than looking already-hovered. */
.modal-store-row.is-current { background: var(--bg-elev-2); }
.modal-store-row.is-current:hover { background: var(--border); }

/* Scoped under .modal deliberately: `.modal img` below sets width:100% for the
   hero screenshot, and at equal-or-higher specificity it wins and stretches
   this 16x16 store icon across the whole row. The old markup escaped that only
   because it was an inline style. Keep this selector at least as specific as
   `.modal img` if either rule is ever touched. */
.modal .modal-store-logo {
  width: 20px;
  height: 20px;
  border-radius: 4px;
  margin-bottom: 0;
  flex-shrink: 0;
  object-fit: contain;
  background: #fff;
}

.modal-store-name {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.modal-store-save {
  color: var(--discount-text);
  font-size: 12px;
  font-weight: 700;
  flex-shrink: 0;
}

.modal-store-price {
  font-weight: 700;
  color: var(--text);
  flex-shrink: 0;
}

.modal .price-new { font-size: 26px; }
.modal .go-to-store {
  display: block;
  text-align: center;
  background: var(--accent);
  color: var(--accent-ink);
  text-decoration: none;
  padding: 15px 12px;
  border-radius: 14px;
  font-size: 16px;
  font-weight: 700;
  margin-top: 14px;
  box-shadow: 0 14px 30px -14px var(--glow);
  transition: filter 0.18s ease, box-shadow 0.18s ease;
}
.modal .go-to-store:hover { filter: brightness(1.08); box-shadow: 0 16px 34px -12px var(--glow); }
.modal .go-to-store:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

.watch-price { margin-top: 14px; }

/* The price alert on a row of its own, Save and Share sharing the one below.
   The popup's buying column is only ~360px (and a phone's no wider), and all
   three on one row left the alert's label wrapping to two lines; on its own
   row it also has the whole width when it opens into its email form. */
.modal-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  /* stretch, so both buttons share one height: the bell emoji makes the
     alert button ~2px taller than a plain-text one. */
  align-items: stretch;
  margin: 0 0 12px;
}
.modal-actions .watch-price { flex: 1 1 100%; min-width: 0; margin-top: 0; }
.modal-actions .share-btn,
.modal-actions .save-btn { width: auto; flex: 1 1 calc(50% - 4px); padding: 10px 14px; }
.modal-actions .watch-price-btn { border-radius: 12px; background: var(--bg-elev-2); }
/* --discount-text rather than --accent: the accent purple is 4.35:1 on the
   light card, just under the 4.5:1 this 13px text needs. */
.save-btn[aria-pressed="true"] { border-color: var(--discount-text); color: var(--discount-text); }
/* The Saved tab shows no price (the popup does, live); this is its prompt. */
.price-new.saved-compare { color: var(--text-dim); font-weight: 600; }

.watch-price-btn {
  width: 100%;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
  padding: 10px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 13px;
  font-weight: 600;
}
.watch-price-btn:hover { border-color: var(--accent); color: var(--accent); }

/* The line icons that replaced the emoji in these buttons and notes (js/app.js iconHtml). */
.btn-ico {
  width: 1.15em;
  height: 1.15em;
  margin-right: 7px;
  vertical-align: -0.2em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.btn-ico.is-fill { fill: currentColor; }

.watch-price-form { display: flex; gap: 8px; }

.watch-price-form input {
  flex: 1;
  min-width: 0;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  padding: 9px 12px;
  font-size: 13px;
}

.watch-price-form button {
  background: var(--accent);
  border: none;
  color: var(--accent-ink);
  padding: 9px 16px;
  border-radius: 8px;
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
  white-space: nowrap;
}
.watch-price-form button:hover { opacity: 0.9; }
.watch-price-form button:disabled { opacity: 0.5; cursor: default; }

/* The alert's optional target price, under the email row: a label, the amount
   with its currency sign, and a shortcut to the lowest ever. */
.watch-target {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 8px;
  margin-top: 8px;
  font-size: 12px;
  color: var(--text-dim);
}
.watch-target-field {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  padding: 0 8px;
}
.watch-target-field:focus-within { border-color: var(--accent); }
.watch-target-field input {
  width: 84px;
  background: transparent;
  border: none;
  outline: none;
  color: var(--text);
  padding: 6px 2px;
  font-size: 13px;
}
.watch-target-low,
.gm-ever-alert {
  background: none;
  border: none;
  padding: 0;
  color: var(--link);
  font: inherit;
  cursor: pointer;
  text-decoration: underline;
}
.watch-target-low:hover,
.gm-ever-alert:hover { opacity: 0.8; }

.watch-price-note {
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 6px;
}
.watch-price-note.success { color: var(--discount-text); }

/* "Notify this device": the alert as a browser notification, on a row of its
   own under the email alert. On, it is a line with a Stop link instead. */
.modal-actions .watch-push { flex: 1 1 100%; min-width: 0; }
.watch-push-on { font-size: 13px; color: var(--discount-text); }
.watch-push-stop {
  background: none;
  border: none;
  padding: 0;
  color: var(--link);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  text-decoration: underline;
}
.watch-push-stop:hover { opacity: 0.8; }

/* Alerts for several games at once, under the Saved tab and the wishlist
   checker. Same button and form as the popup's single-game alert. */
.bulk-alert { max-width: 520px; margin: 10px 0 0; }
.bulk-alert .watch-price-btn { width: auto; padding: 10px 16px; }
.bulk-alert-hint { margin: 6px 0 0; font-size: 12px; color: var(--text-dim); }

/* Footer */
/* The site directory: everything below the grid, as tiles on the grid's own
   columns. The page's own list (a landing page's games, a game's related deals)
   is the first tile; then the Stores, Genres and More deals that
   renderStoreLinks() in js/app.js writes (#storeLinks only holds them, its
   groups are tiles of this grid). Still plain links: they earn their place as
   crawlable internal links, not as navigation competing with the filter bar.
   Until 2026-09-22 this was five separately headed blocks, three of them rows
   of pill buttons; until 2026-10-13 columns of bare text under one divider.

     4 columns   [ list  Stores  Genres  More ]      3 columns   [ list   Stores  Genres ]
                                                                 [ (2 tall)(2 tall) More  ]
   A page with no list of its own: Stores takes two columns, in two columns. */
.site-directory {
  max-width: 1200px;
  margin: 48px auto 0;
  padding: 0 20px;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--tile-gap);
}
.site-directory .store-links { max-width: none; margin: 0; padding: 0; }
#storeLinks { display: contents; }
.site-directory .deal-list,
.site-directory .dir-group {
  min-width: 0;
  padding: var(--tile-pad);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--tile-radius);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
}
.site-directory:not(:has(.deal-list)) .dir-group:first-child { grid-column: span 2; }
.site-directory:not(:has(.deal-list)) .dir-group:first-child .store-links-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 24px;
}

.store-links h2 {
  margin: 0 0 10px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* Spacing as padding on the links, not a gap between them, so each link's tap
   target is ~25px tall: PageSpeed flagged 17px targets 6px apart on phones. */
.store-links-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.store-links-row a {
  padding: 4px 0;
  font-size: 13px;
  line-height: 1.35;
  color: var(--text-dim);
  text-decoration: none;
}
.store-links-row a:hover {
  color: var(--accent);
  text-decoration: underline;
}

/* Three columns, as the cards above: the list and Stores are two tiles tall,
   with Genres over More deals beside them. */
@media (max-width: 1080px) {
  .site-directory { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .site-directory .deal-list,
  .site-directory .dir-group:first-child { grid-row: span 2; }
  .site-directory:not(:has(.deal-list)) .dir-group:first-child { grid-column: span 1; }
  .site-directory:not(:has(.deal-list)) .dir-group:first-child .store-links-row { display: flex; }
}
/* Two columns: the list across, then Stores two tiles tall beside Genres over
   More deals (a phone's pattern too). */
@media (max-width: 800px) {
  .site-directory { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .site-directory .deal-list { grid-column: 1 / -1; grid-row: auto; }
}
@media (max-width: 640px) {
  .site-directory { margin-top: 32px; padding: 0 12px; }
}

/* A landing page's games as plain links (dealListHtml in functions/_landing.js),
   the first column of the site directory. One column of titles: the grid is
   the view, this is the index. */
.deal-list p {
  margin: -4px 0 10px;
  font-size: 12.5px;
  color: var(--text-dim);
}
.deal-list ol {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 13px;
  line-height: 1.4;
}
.deal-list li {
  padding: 3px 0;
}
.deal-list li a {
  color: var(--text);
  text-decoration: none;
}
.deal-list li a:hover {
  color: var(--accent);
  text-decoration: underline;
}
.deal-list-price {
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.deal-list-off { color: var(--discount-text); }
/* A line of a page's own after a game's price: /lowest's "6 days tracked". */
.deal-list-note { font-size: 11.5px; }
/* "Show all N" / "Show fewer": a text link, like everything else in the
   directory. */
.deal-list-rest summary {
  display: inline-block;
  /* 26px tall, not 18: padding and margin together are what the margin alone was. */
  padding: 4px 0;
  margin: 2px 0 0;
  font-size: 13px;
  color: var(--link);
  cursor: pointer;
  list-style: none;
}
.deal-list-rest summary::-webkit-details-marker { display: none; }
.deal-list-rest summary:hover { text-decoration: underline; }
.deal-list-rest summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.deal-list-rest summary::after { content: " ▾"; }
.deal-list-rest[open] summary::after { content: " ▴"; }
.deal-list-rest[open] .deal-list-more,
.deal-list-rest:not([open]) .deal-list-less { display: none; }

/* /black-friday (functions/black-friday.js): its two lists are one block, the site directory's first column (so the
   directory looks for a list anywhere inside it, not only as its child); the second tile reaches the foot of the
   row. The rest of the page is on /sales' grid (the block after /sales' below). */
.bf-lists { min-width: 0; display: flex; flex-direction: column; gap: var(--tile-gap); }
.bf-lists > .deal-list:last-child { flex: 1 1 auto; }
@media (max-width: 1080px) { .bf-lists { grid-row: span 2; } }
@media (max-width: 800px) { .bf-lists { grid-column: 1 / -1; grid-row: auto; } }
/* /sales (functions/sales.js): a bento box on the site's tile grid, over the deal grid and with the same columns,
   gutter and corners.

     4 columns   [ Steam countdown | Black Friday | Epic countdown | the calendar ]
                 [ Steam's seasonal sales (2)     | Epic's sales (2)             ]
                 [ the themed sales (2, tall)     | Black Friday and Cyber Monday (2) ]
                 [                                | Steam Next Fest (2)          ]
                 [ the questions, across the page, in two columns                ]

   A tile that is missing (Epic's guide runs out each quarter, the Fests too) is replaced by a neighbour growing into
   its place (the :has() rules), not a hole. 3 columns: the countdowns a tile each, everything else across. 2 columns:
   the countdowns and the calendar two by two. A phone: the countdown tiles stack, the number beside the name. */
.sales-info {
  max-width: 1200px;
  margin: 24px auto 0;
  padding: 0 20px;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--tile-gap);
}
.sales-info + .sales-info { margin-top: var(--tile-gap); }
.sales-info .tile { margin: 0; max-width: none; justify-content: flex-start; gap: 14px; }
.sales-info .tile.cal-tile-next { justify-content: space-between; min-height: var(--tile-row); }
.sales-info h2.tile-label {
  margin: 0;
  align-self: flex-start;
  height: auto;
  min-height: 28px;
  padding-top: 4px;
  padding-bottom: 4px;
  line-height: 1.2;
  white-space: normal;
}
.sales-info .tile p { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--text-dim); }
.sales-info .tile a { color: var(--link); }
.sales-info .tile .cal-button { color: var(--accent-ink); }
.sales-info .tile h3.tile-title { margin: 0 0 6px; }
/* The note under a schedule sits at the foot of its tile, so two tiles side by side end alike. */
.sales-info .sales-block > p:last-child { margin-top: auto; }
/* A few dates as rows: what, and when. */
.s-dates { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.s-dates li { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 11px 0; border-top: 1px solid var(--border); }
.s-dates li:first-child { border-top: 0; padding-top: 0; }
.s-dates b { display: inline-flex; align-items: center; gap: 10px; font-size: 14px; font-weight: 600; }
.s-dates b::before { content: ""; flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--k, var(--text-dim)); }
.s-dates span { font-size: 13px; color: var(--text-dim); text-align: right; }
.s-cal { justify-content: space-between; min-height: var(--tile-row); }
.s-cal .cal-actions { margin: 0; align-items: center; }
.s-cal .cal-button { display: inline-flex; align-items: center; height: 44px; padding: 0 20px; font-size: 14.5px; }
.s-cal .cal-link { display: inline-flex; align-items: center; min-height: 44px; font-size: 13.5px; }
.s-seasonal, .s-epic, .s-bf, .s-nextfest { grid-column: span 2; container-type: inline-size; }
.s-fest { grid-column: span 2; grid-row: span 2; container-type: inline-size; }

/* The schedules: a table, a dot in the event's colour before each name, the one that is on marked. */
.sales-info .sales-table { width: 100%; min-width: 0; margin: 0; border-collapse: collapse; font-size: 13.5px; }
.sales-info .sales-table caption { caption-side: top; padding: 0 0 8px; text-align: left; font-size: 12.5px; color: var(--text-dim); }
.sales-info .sales-table th,
.sales-info .sales-table td { padding: 11px 16px 11px 0; text-align: left; border-bottom: 1px solid var(--border); vertical-align: baseline; }
.sales-info .sales-table td:last-child,
.sales-info .sales-table th:last-child { padding-right: 0; }
.sales-info .sales-table tbody tr:last-child th,
.sales-info .sales-table tbody tr:last-child td { border-bottom: 0; }
.sales-info .sales-table thead th {
  padding-top: 0;
  padding-bottom: 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.sales-info .sales-table tbody th { color: var(--text); font-size: 14px; font-weight: 600; }
.sales-info .sales-table tbody th::before {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-right: 10px;
  border-radius: 50%;
  background: var(--k, var(--text-dim));
}
.sales-info .sales-table td { color: var(--text-dim); }
.sales-info .sales-table td:last-child { white-space: nowrap; font-variant-numeric: tabular-nums; }
.sales-info .sales-table tr.is-live td:last-child { color: var(--link); font-weight: 600; }
/* A narrow tile (a phone): no dot, so the names have room, and the last column may wrap. */
@container (max-width: 460px) {
  .sales-info .sales-table td:last-child { white-space: normal; }
  .sales-info .sales-table tbody th::before { display: none; }
  .sales-info .sales-table th,
  .sales-info .sales-table td { padding-right: 10px; }
  .sales-info .sales-table { font-size: 13px; }
}

/* The questions: one tile across the page, two columns of them. */
.sales-faq { grid-column: 1 / -1; }
.sales-faq .faq-items { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 40px; align-items: start; }
.sales-faq .wishlist-faq-item { max-width: none; }
.sales-faq .wishlist-faq-item:last-child { border-bottom: 0; }

/* A missing tile never leaves a hole. */
.sales-info:not(:has(.s-epic)) .s-seasonal { grid-column: span 4; }
.sales-info:not(:has(.s-nextfest)) .s-bf { grid-row: span 2; }
.sales-info:not(:has(.s-fest)):not(:has(.s-nextfest)) .s-bf { grid-row: auto; grid-column: span 4; }

/* Three columns: the countdowns a tile each, the calendar and the two schedules across the page, then the Fests two
   wide beside Black Friday over Next Fest (the same as four columns). "1 / -1" and not "span 4" in the fallbacks: a
   fourth column would be a column that is not there. */
@media (max-width: 1080px) {
  .sales-info { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .s-cal { grid-column: 1 / -1; min-height: 0; }
  .s-seasonal, .s-epic,
  .sales-info:not(:has(.s-epic)) .s-seasonal { grid-column: 1 / -1; }
  .s-fest { grid-column: span 2; grid-row: span 2; }
  .s-bf, .s-nextfest { grid-column: span 1; grid-row: auto; }
  .sales-info:not(:has(.s-nextfest)) .s-bf { grid-row: span 2; }
  .sales-info:not(:has(.s-fest)) .s-bf { grid-column: span 2; grid-row: auto; }
  .sales-info:not(:has(.s-fest)):not(:has(.s-nextfest)) .s-bf { grid-column: 1 / -1; }
}
/* Two columns: the three countdowns and the calendar two by two, then one tile to a row. */
@media (max-width: 800px) {
  .sales-info { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .s-cal { grid-column: span 1; min-height: var(--tile-row); }
  .s-fest, .s-bf, .s-nextfest,
  .sales-info:not(:has(.s-nextfest)) .s-bf,
  .sales-info:not(:has(.s-fest)) .s-bf { grid-column: 1 / -1; grid-row: auto; }
  .sales-faq .faq-items { grid-template-columns: minmax(0, 1fr); }
  .sales-info .tile.cal-tile-next { min-height: 0; }
}
@media (max-width: 640px) {
  .sales-info { padding: 0 12px; }
  .s-cal { grid-column: 1 / -1; min-height: 0; }
}

/* /black-friday (functions/black-friday.js) on /sales' grid (.sales-info), its tiles and its breakpoints:

     4 columns   [ days to Black Friday | at their lowest price ever | half off or more | your wishlist ]
                 ... the deal grid, then:
                 [ how to check a deal: four steps, across the page ]
                 [ the questions, across the page, in two columns   ]

   The lowest-price tile is left out while the site has no price record long enough to name a game's low
   (lowest.js), and the wishlist tile grows into its place (the :has() rules), not a hole. The steps and the questions
   sit below the deal grid, above the directory. */
.sales-info:not(:has(.bf-lows)) .bf-wish { grid-column: span 2; }
/* The numbers on one line across the tiles, whatever the words under them run to: each tile's text sits at its foot. */
@media (min-width: 521px) {
  .bf-facts .tile.cal-tile-next { justify-content: flex-start; }
  .bf-facts .cal-big { margin-top: 18px; }
  .bf-facts .cal-next-text { margin-top: auto; }
}
@media (max-width: 1080px) {
  .sales-info:not(:has(.bf-lows)) .bf-wish { grid-column: span 1; min-height: var(--tile-row); }
}
@media (max-width: 800px) {
  .sales-info:not(:has(.bf-lows)) .bf-wish { grid-column: 1 / -1; min-height: 0; }
}

/* The four steps of checking a deal: a number, what to do, and why. */
.bf-guide { grid-column: 1 / -1; container-type: inline-size; }
.bf-guide ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 20px 28px;
  counter-reset: step;
}
.bf-guide li { margin: 0; counter-increment: step; font-size: 13.5px; line-height: 1.55; color: var(--text-dim); }
.bf-guide li::before {
  content: counter(step);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  margin-bottom: 10px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 12.5px;
  font-weight: 800;
}
.bf-guide strong { display: block; margin-bottom: 4px; color: var(--text); font-size: 15px; font-weight: 700; line-height: 1.3; }
@container (max-width: 760px) {
  .bf-guide ul { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@container (max-width: 440px) {
  .bf-guide ul { grid-template-columns: minmax(0, 1fr); }
}

/* The sales calendar on /sales (functions/_salesCalendar.js): a month grid, Monday first. Each week is a grid of
   seven columns: its day numbers on the first row, and a bar for each sale on the rows below it, spanning the
   columns of its days. Drawn for the eye (aria-hidden); the tables below carry every date as text. The bar
   colours are fixed, dark enough for white text in both themes. */
.sales-calendar { max-width: 760px; }
.cal-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 18px; margin: 0 0 14px; }
.cal-button {
  display: inline-block;
  background: var(--accent);
  color: var(--accent-ink) !important;
  text-decoration: none;
  font-weight: 600;
  font-size: 14px;
  padding: 9px 18px;
  border-radius: 999px;
}
.cal-button:hover { opacity: 0.9; }
.cal-link { font-size: 13px; }
/* The upcoming-sales banner under the header. The slot holds its height until the list arrives (index.html).
   The gap above it is the slot's padding, not the banner's margin. As a margin it was 8px, which put the
   banner inside the header's shadow (that reaches about 20px down), and it collapsed through the slot, so
   the page also moved down 8px when the banner arrived. 18px clears the shadow and about matches the 20px
   below; 51px is that plus the banner's 33px, so nothing moves. */
.sales-banner-slot { min-height: 51px; padding-top: 18px; }
.sales-banner-slot[hidden] { display: none; }
.sales-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}
.sb-link {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 4px 4px 14px;
  background: var(--glass);
  border: 1px solid var(--border);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-radius: 999px;
  color: var(--text);
  text-decoration: none;
  font-size: 13px;
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
}
.sb-link:hover { background: var(--bg-elev-2); }
.sb-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sb-items { flex: 1; min-width: 0; display: flex; gap: 8px 18px; overflow: hidden; text-overflow: ellipsis; }
.sb-item { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-dim); }
.sb-item strong { color: var(--text); font-weight: 600; }
/* 4px padding on the link + 25px here = the banner's 33px (the slot's height
   below assumes it). */
.sb-go { flex: none; padding: 4px 12px; border-radius: 999px; background: var(--chip); color: var(--text); font-weight: 600; }
.sb-link:hover .sb-go { background: var(--border); }
.sb-icon { flex: none; fill: none; stroke: var(--accent); stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.sb-close {
  position: relative;
  flex: none;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--text-dim);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.sb-close:hover { background: var(--bg-elev-2); color: var(--text); }
/* 28px to look at, 44px to hit on a phone. Uneven on purpose: 6px to the left
   stays inside the 8px gap, so it never takes a tap meant for the banner. */
.sb-close::after { content: ""; position: absolute; inset: -8px -10px -8px -6px; }
/* Two sales fit a tablet; three were cut mid-word at 768px ("Steam Scream V"). */
@media (max-width: 900px) {
  .sb-item:nth-child(n+3) { display: none; }
}
@media (max-width: 640px) {
  .sales-banner { padding: 0 12px; }
  .sb-item:nth-child(n+2) { display: none; }
  .sb-go-word { display: none; }
}
/* The offer to carry saved games to the new address, and the line that says they arrived (js/app.js, "Handing a visitor's
   saved data over"). Both sit under the header, above the sales banner, and only exist in step 2 of the domain move. */
.handover {
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 1200px;
  margin: 18px auto 0;
  padding: 8px 8px 8px 16px;
  background: var(--glass);
  border: 1px solid var(--border);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-radius: 14px;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.4;
}
.handover-text { flex: 1; min-width: 0; margin: 0; }
.handover-text strong { color: var(--text); font-weight: 600; }
.handover-go { color: var(--accent); font-weight: 600; white-space: nowrap; }
.handover-go:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.handover-close {
  flex: none;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--text-dim);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.handover-close:hover { background: var(--bg-elev-2); color: var(--text); }
@media (max-width: 1240px) {
  .handover { margin-left: 20px; margin-right: 20px; }
}
/* The note that the deal list is old (functions/_staleNote.js), under the sales strip. */
.stale-note { max-width: 1200px; margin: 4px auto 0; padding: 0 20px; }
.stale-note-inner {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--warn-text);
  border-radius: 14px;
  background: var(--bg-elev);
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.45;
}
.stale-note-inner svg { flex: none; margin-top: 1px; fill: none; stroke: var(--warn-text); stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.stale-note-inner p { margin: 0; }
.stale-note-inner strong { color: var(--text); font-weight: 600; }
@media (max-width: 640px) {
  .stale-note { padding: 0 12px; }
}
/* /calendar (functions/calendar.js, functions/_salesCalendar.js): a bento box on the site's tile grid, the same
   columns, gutter and corners as the home page's.

     4 columns   [ on now  on now | next Steam sale | next Epic sale ]
                 [ the month calendar (3 wide)      | coming up       ]
                 [ add it | whose dates these are (3 wide)       ]

   3 columns: the three at the top a tile each, the month and the agenda across the page. 2 columns and a phone: on now across,
   the two sales side by side, then one tile to a row. The colours of the five kinds of event are fixed (dark
   enough for white text in both themes) and are one variable, --k, for the bars, the dots and the swatches. */
.cal-sale { --k: #5b3fd1; }
.cal-fest { --k: #1f7a4d; }
.cal-epic { --k: #0b6fa4; }
.cal-blackfriday { --k: #b4440b; }
.cal-demo { --k: #5c6170; }

.cal-page { max-width: 1200px; margin: 0 auto; padding: 30px 20px 40px; }
.cal-page h1 {
  margin: 0;
  font-family: var(--heading-font);
  font-weight: 800;
  font-size: clamp(32px, 5vw, 48px);
  line-height: 1.04;
  letter-spacing: -0.035em;
}
.cal-lead { margin: 12px 0 0; max-width: 64ch; color: var(--text-dim); font-size: 17px; line-height: 1.5; }
.cal-bento {
  margin-top: 24px;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--tile-gap);
}
.cal-bento .cal-actions { grid-column: 1 / -1; margin: 0; align-items: center; }
.cal-bento .cal-button { display: inline-flex; align-items: center; height: 44px; padding: 0 22px; font-size: 15px; }
.cal-bento .cal-link { display: inline-flex; align-items: center; min-height: 44px; font-size: 14px; }
.cal-bento .tile { justify-content: flex-start; gap: 14px; }
.cal-bento h2.tile-label { margin: 0; align-self: flex-start; }
.cal-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--k, var(--text-dim)); display: inline-block; }

.cal-tile-now { grid-column: span 2; min-height: var(--tile-row); }
.cal-tile-next { grid-column: span 1; min-height: var(--tile-row); justify-content: space-between; }
.cal-tile-month { grid-column: span 3; }
.cal-tile-agenda { grid-column: span 1; container-type: inline-size; }
.cal-tile-take { grid-column: span 1; }
.cal-tile-src { grid-column: span 3; container-type: inline-size; }

/* What is on now: the dark tile of the home page's bento, tinted with the event's own colour. */
.cal-tile-now.on-art {
  justify-content: space-between;
  background: #0b1018;
  background: radial-gradient(120% 100% at 0% 0%, color-mix(in srgb, var(--k) 60%, transparent) 0%, transparent 72%), #0b1018;
}
.cal-tile-now.cal-idle { justify-content: space-between; }
.cal-kindname { font-size: 12.5px; font-weight: 600; color: rgba(255,255,255,0.86); }
.cal-now-body { display: flex; flex-direction: column; gap: 8px; }
.cal-tile-now .tile-title.big { margin: 0; }
.cal-tile-now .tile-note { font-size: 14px; }
.cal-progress { height: 8px; margin-top: 10px; border-radius: 999px; background: rgba(255,255,255,0.18); overflow: hidden; }
.cal-progress span { display: block; height: 100%; border-radius: inherit; background: #fff; }
.cal-also {
  list-style: none;
  margin: 0;
  padding: 12px 0 0;
  border-top: 1px solid rgba(255,255,255,0.16);
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 13px;
  color: rgba(255,255,255,0.9);
}
.cal-also li { display: flex; align-items: center; gap: 8px; }
.cal-also .cal-dot { box-shadow: 0 0 0 2px rgba(255,255,255,0.4); }
.cal-also li span:last-child { margin-left: auto; color: rgba(255,255,255,0.86); }

/* The next Steam and Epic sales: the days to go, as the number to read. */
.cal-tile-next .tile-label { display: inline-flex; align-items: center; gap: 8px; max-width: 100%; height: auto; min-height: 28px; padding-top: 5px; padding-bottom: 5px; line-height: 1.2; white-space: normal; }
.cal-big {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
  font-family: var(--heading-font);
  font-weight: 800;
  line-height: 0.95;
  color: var(--accent);
}
.cal-big span { font-size: clamp(54px, 6vw, 76px); letter-spacing: -0.045em; font-variant-numeric: tabular-nums; }
.cal-big small { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 600; letter-spacing: 0; color: var(--text-dim); }

/* The month. One at a time once the script runs (CALENDAR_PAGE_SCRIPT); before it, or without it, the first three. */
.cal-top { display: flex; flex-direction: column; gap: 12px; }
.cal-nav { display: flex; align-items: center; gap: 8px; max-width: none; margin: 0; }
.cal-nav[hidden] { display: none; }
.cal-title {
  order: -1;
  flex: 1;
  text-align: left;
  font-family: var(--heading-font);
  font-weight: 800;
  font-size: 28px;
  letter-spacing: -0.03em;
}
.cal-prev,
.cal-next {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  background: var(--chip);
  color: var(--text);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.cal-prev:hover:not(:disabled),
.cal-next:hover:not(:disabled) { background: var(--bg-elev-2); }
.cal-prev:disabled,
.cal-next:disabled { opacity: 0.35; cursor: default; }
.cal-prev:focus-visible,
.cal-next:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cal:not(.cal-paged) .cal-month:nth-child(n+4) { display: none; }
.cal-paged .cal-month { display: none; margin: 0; }
.cal-paged .cal-month.cal-on { display: block; }
.cal-paged .cal-month h3 { display: none; }
.cal-month { margin: 0 0 22px; }
.cal-month h3 { margin: 0 0 8px; font-family: var(--heading-font); font-size: 22px; letter-spacing: -0.02em; }
.cal-head,
.cal-week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); column-gap: 0; }
.cal-head span {
  padding: 0 10px 8px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-dim);
}
/* Lines between the days (the first gradient) and a tint on the weekend (the second). */
.cal-week {
  grid-template-rows: 34px;
  row-gap: 3px;
  min-height: 86px;
  padding-bottom: 6px;
  border-top: 1px solid var(--border);
  background-image:
    linear-gradient(to right, var(--border) 1px, transparent 1px),
    linear-gradient(to right, transparent 71.4286%, var(--chip) 71.4286%);
  background-size: calc(100% / 7) 100%, 100% 100%;
  background-repeat: repeat, no-repeat;
}
.cal-day {
  justify-self: start;
  align-self: center;
  min-width: 24px;
  height: 24px;
  margin: 0 6px;
  padding: 0 6px;
  border-radius: 999px;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 24px;
  text-align: center;
  color: var(--text-dim);
}
.cal-today { background: var(--accent); color: var(--accent-ink); font-weight: 800; }
.cal-bar {
  min-width: 0;
  height: 22px;
  margin: 0 3px;
  padding: 0 9px;
  border-radius: 7px;
  background: var(--k);
  font-size: 12.5px;
  font-weight: 600;
  line-height: 22px;
  color: #fff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cal-open-left { margin-left: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; }
.cal-open-right { margin-right: 0; border-top-right-radius: 0; border-bottom-right-radius: 0; }
.cal-legend { display: flex; flex-wrap: wrap; gap: 8px 10px; margin: 6px 0 0; }
.cal-key {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 12px 5px 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--chip);
  font-size: 12.5px;
  color: var(--text);
}
.cal-swatch { width: 10px; height: 10px; border-radius: 3px; background: var(--k); display: inline-block; flex: none; }

/* Coming up: the next dates as a list, the text a screen reader reads and a phone reads best. */
.cal-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.cal-list li { display: flex; align-items: flex-start; gap: 12px; padding: 11px 0; border-top: 1px solid var(--border); }
.cal-list li:first-child { border-top: 0; padding-top: 0; }
.cal-list .cal-dot { margin-top: 6px; }
.cal-li-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cal-li-main b { font-size: 14.5px; font-weight: 600; line-height: 1.3; }
.cal-li-main span { font-size: 12.5px; line-height: 1.4; color: var(--text-dim); }
.cal-list li.is-live .cal-li-main span { color: var(--link); font-weight: 600; }
.cal-tile-agenda .tile-more { margin-top: auto; align-self: flex-start; }
@container (min-width: 520px) {
  .cal-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 32px; }
  .cal-list li:nth-child(2) { border-top: 0; padding-top: 0; }
}

/* How to take it, and whose dates these are. */
.cal-tile-take .tile-title { margin: 0 0 6px; }
.cal-tile-take .tile-note { margin-top: 8px; font-size: 14px; line-height: 1.55; }
.cal-tile-take code {
  padding: 2px 6px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--chip);
  font-size: 12.5px;
  word-break: break-all;
}
.cal-src { margin: 0; display: flex; flex-direction: column; }
.cal-src > div {
  display: grid;
  grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
  gap: 4px 20px;
  padding: 11px 0;
  border-top: 1px solid var(--border);
}
.cal-src > div:first-child { border-top: 0; padding-top: 0; }
.cal-src dt { display: flex; align-items: center; gap: 6px; font-size: 14px; font-weight: 600; }
.cal-src dd { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--text-dim); }
.cal-src dd a { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }
@container (max-width: 480px) {
  .cal-src > div { grid-template-columns: minmax(0, 1fr); }
}

.cal-more { display: flex; flex-wrap: wrap; gap: 8px 10px; margin-top: var(--tile-gap); }
.cal-more a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 18px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-elev);
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
}
.cal-more a:hover { border-color: var(--border-strong); background: var(--bg-elev-2); }

/* Three columns: a tile each for the top three, the rest across the page. */
@media (max-width: 1080px) {
  .cal-bento { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .cal-tile-now, .cal-tile-next { grid-column: span 1; }
  .cal-tile-month, .cal-tile-agenda { grid-column: 1 / -1; }
  .cal-tile-src { grid-column: span 2; }
}
/* Two columns: on now across, the two sales side by side, one tile to a row after them. */
@media (max-width: 800px) {
  .cal-bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cal-tile-now { grid-column: 1 / -1; min-height: 0; }
  .cal-tile-next { grid-column: span 1; min-height: 0; }
  .cal-tile-take, .cal-tile-src { grid-column: 1 / -1; }
}
/* A phone: the two sales a tile each, the number beside the name, so neither is squeezed to half a screen. */
@media (max-width: 520px) {
  .cal-tile-next {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "label label" "big text";
    column-gap: 18px;
    row-gap: 12px;
    align-items: end;
  }
  .cal-tile-next .tile-label { grid-area: label; justify-self: start; }
  .cal-tile-next .cal-big { grid-area: big; flex-direction: column; align-items: flex-start; gap: 4px; }
  .cal-tile-next .cal-next-text { grid-area: text; min-width: 0; }
}
@media (max-width: 640px) {
  .cal-page { padding: 22px 12px 32px; }
  .cal-lead { font-size: 15.5px; }
  .cal-title { font-size: 22px; }
  .cal-tile-now .tile-title.big { font-size: 30px; }
  .cal-big span { font-size: 54px; }
  .cal-head span { padding: 0 2px 6px; font-size: 11px; text-align: center; }
  .cal-week { grid-template-rows: 28px; min-height: 66px; }
  .cal-day { min-width: 22px; height: 22px; margin: 0 1px; padding: 0 4px; font-size: 11.5px; line-height: 22px; }
  .cal-bar { height: 20px; margin: 0 1px; padding: 0 5px; font-size: 11px; line-height: 20px; }
}


/* Long-form pages (/privacy). The site is otherwise a grid of cards and has
   no typographic styles for running text at all. */
.prose {
  max-width: 720px;
  margin: 0 auto;
  padding: 34px 20px 64px;
  line-height: 1.65;
}
.prose h1 {
  font-family: var(--heading-font);
  font-size: 26px;
  margin: 0 0 10px;
}
.prose h2 {
  font-family: var(--heading-font);
  font-size: 16px;
  margin: 30px 0 8px;
}
.prose p, .prose li { color: var(--text-dim); font-size: 14.5px; }
.prose strong { color: var(--text); }
.prose .prose-lead { color: var(--text); font-size: 16px; }
.prose ul { padding-left: 20px; margin: 8px 0; }
.prose li { margin-bottom: 6px; }
.prose a { color: var(--accent); }
.prose .prose-meta { font-size: 12.5px; margin-top: 26px; }

/* The footer: tiles on the page's grid, directly under the directory's. Until
   2026-10-09 it was centred 12px small print with the sign-up as a 760px card
   floating in the middle of a four-column page; until 2026-10-13 a band across
   the page. Now it is the last row of the same bento box:

     4 columns   [ sign-up sign-up  money  money ]      3 columns  [ sign-up sign-up money ]
                 [ the price notes, a strip       ]                 [ the price notes, a strip ]
   then the name and the links, under it. The two notes have a row of their own
   width because the price note is long: one card wide it ran 14 lines and left
   the sign-up beside it half empty. */
.site-footer {
  margin-top: var(--tile-gap);
  color: var(--text-dim);
  font-size: 13px;
}
.footer-inner { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
.footer-tiles {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--tile-gap);
}
.footer-notes { display: contents; }
.footer-notes > .footer-note:first-child { grid-column: span 2; }
.footer-notes > .footer-note:last-child { grid-column: 1 / -1; }
.footer-tiles > .newsletter,
.footer-note {
  min-width: 0;
  padding: var(--tile-pad);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--tile-radius);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
}

/* The sign-up: what it is at the top, the address and the button at the foot,
   both 44px tall. */
.site-footer .newsletter {
  grid-column: span 2;
  margin: 0;
  max-width: none;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: stretch;
  gap: 16px;
}
/* flex: none: its row-layout basis (260px) would be a minimum height in this column. */
.site-footer .newsletter-text { flex: none; gap: 6px; font-size: 14px; line-height: 1.5; }
.site-footer .newsletter-text strong { font-family: var(--heading-font); font-size: 20px; letter-spacing: -0.01em; line-height: 1.25; text-wrap: balance; }
.site-footer .newsletter-fields { flex: none; }
.site-footer .newsletter-fields input[type="email"],
.site-footer .newsletter-fields button { height: 44px; border-radius: 10px; font-size: 14px; }
.site-footer .newsletter-fields input[type="email"] { padding: 0 14px; }
.site-footer .newsletter-fields button { padding: 0 20px; }
.site-footer .newsletter-note { flex: none; min-height: 0; }
.site-footer .newsletter-note:empty { display: none; }
/* Arriving from a post's or an email's link: the address field is what to
   look at, and a ring round the whole tile would be a ring round a card. */
.site-footer .newsletter-spot { box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); }
.site-footer .newsletter-spot input[type="email"] { border-color: var(--accent); box-shadow: 0 0 0 3px var(--tier-hot-glow); }

.footer-label {
  margin: 0 0 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.footer-note p { margin: 0; line-height: 1.6; }
/* The affiliate disclosure: its own heading and the page's text colour, a
   size above the price notes beside it. A disclosure that reads like small
   print is one a regulator, and a reader, would say was technically present
   rather than actually made. */
.footer-note .footer-disclosure { color: var(--text); font-size: 13.5px; }
/* Underlined: inside a sentence, colour alone does not mark a link (flagged
   by PageSpeed on the CheapShark link). */
.footer-note a { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0 24px;
  padding: 10px 0 22px;
}
.footer-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  color: var(--text);
  text-decoration: none;
}
/* 19px, not smaller: bold from 18.66px counts as large text, where the
   purple of ".deals" (4.35:1 on white) passes. */
.footer-brand-name { font-family: var(--heading-font); font-weight: 700; font-size: 19px; letter-spacing: -0.01em; }
.footer-brand-tag { font-size: 13px; color: var(--text-dim); }
.footer-links { display: flex; flex-wrap: wrap; column-gap: 22px; }
/* Padding, not a gap, for a tap target ~38px tall. */
.footer-links a { padding: 10px 0; color: var(--text-dim); text-decoration: none; }
.footer-links a:hover { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }

/* Three columns: the sign-up beside the first note, the prices note a strip under. */
@media (max-width: 1080px) {
  .footer-tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .footer-notes > .footer-note:first-child { grid-column: span 1; }
}
/* Two columns: each across the width. */
@media (max-width: 800px) {
  .footer-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .footer-notes > .footer-note:first-child { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  .footer-inner { padding: 0 12px; }
  .footer-tiles { grid-template-columns: minmax(0, 1fr); }
  .site-footer .newsletter,
  .footer-notes > .footer-note:first-child,
  .footer-notes > .footer-note:last-child { grid-column: auto; }
  .site-footer .newsletter-text strong { font-size: 18px; }
  /* 16px: iOS zooms the page into any field set smaller when it is tapped. */
  .site-footer .newsletter-fields input[type="email"] { font-size: 16px; }
  .footer-bottom { flex-direction: column; align-items: flex-start; }
}

/* The weekly deals email's sign-up, at the top of the footer: a line saying what it is, and the
   address and the button beside it (stacked on a phone). The "website" field is a trap for scripts
   and is out of sight and out of the tab order. */
.newsletter {
  max-width: 760px;
  margin: 0 auto 22px;
  padding: 14px 16px;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 12px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 16px;
  text-align: left;
}
.newsletter-text { flex: 1 1 260px; display: flex; flex-direction: column; gap: 2px; font-size: 12.5px; }
.newsletter-text strong { color: var(--text); font-size: 14px; }
.newsletter-fields { flex: 1 1 280px; display: flex; gap: 8px; }
.newsletter-fields input[type="email"] {
  flex: 1;
  min-width: 0;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  padding: 9px 12px;
  font-size: 13px;
}
.newsletter-fields button {
  background: var(--accent);
  border: none;
  color: var(--accent-ink);
  padding: 9px 16px;
  border-radius: 8px;
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
  white-space: nowrap;
}
.newsletter-fields button:hover { opacity: 0.9; }
.newsletter-fields button:disabled { opacity: 0.5; cursor: default; }
/* "Tell me when a new game is free" (the Free shelf's notification box): the button alone on the right, outlined once it is on. */
.free-alert .newsletter-fields { justify-content: flex-end; }
.free-alert .newsletter-fields button { min-height: 44px; padding: 0 22px; }
.free-alert .newsletter-fields button.is-on { background: transparent; color: var(--text); border: 1px solid var(--border-strong); }
.newsletter-trap { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
.newsletter-note { flex-basis: 100%; min-height: 1em; font-size: 12px; color: var(--text-dim); }
.newsletter-note.success { color: var(--discount-text); }
/* The same sign-up where it is asked for in passing: under the Free tab's cards, and in the popup after a
   game is saved or a price alert is set. Narrower, and without the footer's side margin. */
.newsletter-inline { margin: 14px 0 0; padding: 12px 14px; }
.newsletter-inline[hidden] { display: none; }
.newsletter-inline .newsletter-fields { flex-basis: 100%; }
/* Under the Free list it is a tile of the page's grid, across it: what it is on the left (as the footer's tile says
   it), the address and the button on the right, both 44px tall; one under the other on a phone. */
.steam-shelf[data-shelf="free"] .newsletter-inline {
  max-width: none;
  margin: var(--tile-gap) 0 0;
  padding: var(--tile-pad);
  border-radius: var(--tile-radius);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
  gap: 16px 40px;
}
.steam-shelf[data-shelf="free"] .newsletter-text { flex: 1 1 320px; gap: 6px; font-size: 14px; line-height: 1.5; }
.steam-shelf[data-shelf="free"] .newsletter-text strong { font-family: var(--heading-font); font-size: 20px; letter-spacing: -0.01em; line-height: 1.25; }
.steam-shelf[data-shelf="free"] .newsletter-inline .newsletter-fields { flex: 1 1 340px; max-width: 520px; }
.steam-shelf[data-shelf="free"] .newsletter-fields input[type="email"],
.steam-shelf[data-shelf="free"] .newsletter-fields button { height: 44px; border-radius: 10px; font-size: 14px; }
.steam-shelf[data-shelf="free"] .newsletter-fields input[type="email"] { padding: 0 14px; }
.steam-shelf[data-shelf="free"] .newsletter-fields button { padding: 0 20px; }
.steam-shelf[data-shelf="free"] .newsletter-note:empty { display: none; }
@media (max-width: 640px) {
  /* 16px: iOS zooms the page into any field set smaller when it is tapped. */
  .steam-shelf[data-shelf="free"] .newsletter-fields input[type="email"] { font-size: 16px; }
  .steam-shelf[data-shelf="free"] .newsletter-text strong { font-size: 18px; }
}
/* On /free the page's <h1> names the list: the shelf's own heading stays for screen readers and is not said twice. */
/* The deal grid under a list that is a page of its own (/most-played, /new-releases): its heading. */
.more-deals {
  max-width: 1200px;
  margin: 40px auto 0;
  padding: 0 20px;
  font-family: var(--heading-font);
  font-weight: 800;
  font-size: clamp(22px, 2.6vw, 30px);
  line-height: 1.1;
  letter-spacing: -0.03em;
}
@media (max-width: 640px) {
  .more-deals { margin-top: 32px; }
}
/* The lists that are pages open in the HTML: their grid holds its height while the chart arrives. */
@media (min-width: 801px) {
  .steam-shelves.is-page #mostPlayedRow,
  .steam-shelves.is-page #newReleasesRow { min-height: 560px; }
}
.steam-shelves.is-page .shelf-head {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
.newsletter-dismiss {
  background: none;
  border: none;
  padding: 0;
  color: var(--text-dim);
  font-size: 12px;
  text-decoration: underline;
  cursor: pointer;
}
.newsletter-dismiss[hidden] { display: none; }
/* The footer form, when a post's or an email's link has just brought someone to it. */
.newsletter-spot { box-shadow: 0 0 0 2px var(--accent); }

/* Responsive */
@media (max-width: 640px) {
  .header-inner { flex-wrap: wrap; }
  .search-wrap { order: 3; max-width: none; width: 100%; }
  /* Logo, Saved, currency and theme share the first row at 375px: Saved
     drops its label to just the heart and count, and the gaps tighten. */
  .header-inner { column-gap: 10px; }
  .header-saved { padding: 0 9px; }
  .header-saved-label { display: none; }
  /* Logo and four controls (Saved, currency, theme, wishlist) in one row of 375px: the page's own 12px sides (the tiles', the
     intro's), a smaller logo and the currency without its spare padding. Below about 360px the last one wraps (to the search row's start), as the row once did. */
  .header-inner { padding-left: 12px; padding-right: 12px; column-gap: 5px; }
  .logo { font-size: 19px; gap: 8px; }
  .logo-mark { width: 26px; height: 26px; }
  .header-currency { padding-left: 9px; }
  .header-currency select { padding-right: 20px; }
  /* The four pills stacked into four full-width rows here — about 350px of
     controls above the first deal, on the screen with the least room for it.
     Sort and a Filters button now share one row; the rest open on demand. */
  .filters {
    gap: 10px;
    flex-wrap: wrap;
    align-items: center;
  }
  .filter-group select { min-width: 100px; }

  /* A row of its own, at the right: sharing the Sort row cut the sort's name to "Best D…". */
  .layout-switch { order: 3; margin-left: auto; }
  .filter-group--sort { order: 1; flex: 1 1 auto; min-width: 0; }
  .filter-group--sort select { min-width: 0; width: 100%; }

  .filters-toggle {
    order: 2;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex: 0 0 auto;
    /* Matches .filter-group's pill shape rather than inventing a third
       control style. Height is the pill's, so the row does not grow. */
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text);
    font: inherit;
    font-size: 13px;
    font-weight: 600;
    padding: 9px 14px;
    cursor: pointer;
    transition: border-color 0.18s ease;
  }
  .filters-toggle[aria-expanded="true"] { border-color: var(--accent); }

  /* Count of filters actually narrowing the list, so a collapsed bar never
     hides active state. Sort is excluded — it reorders, it does not filter. */
  /* An explicit display value beats the `hidden` attribute's UA rule, so
     without this the badge stayed as an empty purple dot with no count in it
     whenever no filters were active. */
  .filters-count[hidden] { display: none; }

  .filters-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-ink);
    font-size: 11px;
    font-weight: 700;
  }

  .filters-collapsible {
    order: 4;
    display: none;
    width: 100%;
    flex-direction: column;
    gap: 10px;
  }
  .filters-collapsible.is-open { display: flex; }
  /* Children are stacked inside the wrapper here, so the desktop ordering
     above must not apply. */
  .filters-collapsible > * { order: 0; width: 100%; }
  .filters-collapsible .filter-group { justify-content: space-between; }
  .filters-collapsible .filter-group select { flex: 1; min-width: 0; text-align: right; }

  /* 16px is not a style choice: iOS Safari auto-zooms the page when a focused
     input has a font-size below 16px, which on this layout shoved the header
     off-screen on every search. The taller padding also brings the field and
     its button closer to a comfortable touch target. */
  .search-wrap input { font-size: 16px; padding: 12px 0; }
  .search-wrap button { width: 40px; height: 40px; }
}
