/* ============================================================================
   mr-design.css - the MapRaiders design system. One file, no JavaScript.

   Owner A3. Read by A3 (web/, battle/web/) and by A7 (the static pages under
   docs/). It is linked, never copied: a copy drifts within a week and then the
   visitor feels he changed companies on the way from the landing page into the
   game.

   Served from web/public/, which Vite copies verbatim into dist/:
       https://mapraiders.com/play/mr-design.css
   A0 has been asked for an nginx alias so the same file also answers at
       https://mapraiders.com/mr-design.css
   Until that exists, link the /play/ path. Same file either way.

   WHY EVERY NAME STARTS WITH --mr-
   Three stylesheets already meet on one page and two of them own short names:
   art/template/card.css sets --ink, --ground, --accent and --hair on :root, and
   web/src/index.css sets --bg, --accent, --text. --hair is a colour in one and a
   stroke width in the other. battle/web/build_cardpack.mjs documents that
   collision and works around it by moving card.css's :root onto .card. This file
   does not need that workaround because it claims no short name at all.

   NO DARK MODE, ON PURPOSE. The 992 pages under docs/ are light, printed cards
   are light, and paper does not change theme. One look, measured once.

   Structure:
     1  Fonts
     2  Tokens: brand, ink, camps, rarity, space, type, motion
     3  Base: reset, typography, focus, reduced motion
     4  Layout: page, section, grid, panel
     5  Navigation and footer
     6  Buttons, including the locked "coming soon" state
     7  Badges, pills, prices, odds tables
     8  The card frame and its placeholder
     9  The slider: scroll-snap, swipeable, keyboard operable
    10  Trade surfaces: shop, marketplace, arena
    11  Forms
    12  Utilities
   ========================================================================== */

/* ---------------------------------------------------------------------------
   1  Fonts

   Same two families the landing page already loads, so a visitor crossing from
   docs/ into /play/ does not watch the type change. Loaded here so that one
   <link> to this file is enough for a static page.
   --------------------------------------------------------------------------- */

/* SELBST AUSGELIEFERT, NICHT NACHGELADEN. Hier stand ein @import auf fonts.googleapis.com, und
   dieser eine Import schickte die IP jedes Besuchers an einen Dritten: auf den 15 Seiten, die
   diese Datei verlinken, UND im Spielclient unter /play/, dort ausdruecklich VOR dem Login.
   (Die Zahl stand hier zuerst als 787, und die Herkunft gehoert zur Lehre: der Kopf DIESER Datei
   behauptete 992, A9 meldete 787, und beides ging ungeprueft durch mehrere Haende, meine
   eingeschlossen. Gemessen hat es dann A7 an docs/ selbst: 15 Seiten binden mr-design.css ein,
   746 haben eigene link-Zeilen, 0 haben beides, 36 laden gar keine fremde Schrift, 17 Familien. 746 weitere Seiten laden ihre Schriften
   ueber eigene <link>-Zeilen aus 17 Familien und sind von dieser Umstellung NICHT gedeckt.)
   Es gibt also keinen Zeitpunkt, an dem eine Einwilligung haette vorliegen koennen, und ein
   Zustimmungsbanner fuer eine Schriftart waere ohnehin absurd. Das LG Muenchen I hat 2022 zu
   genau dieser Konstellation entschieden (Az. 3 O 17493/20).
   Die Regel dahinter ist groesser als die Schriften: keine Besucher-IP an einen Dritten ohne
   Einwilligung. Ich hatte sie beim Kartenbild der Startseite von mir aus angewendet und hier
   uebersehen, weil ein @import wie Gestaltung aussieht und nicht wie ein Datenabfluss.
   Die Dateien liegen unter /assets/schriften/, erzeugt von web/tools/schriften_holen.mjs.
   Der 300er-Schnitt von Outfit faellt dabei weg, er wird nirgends benutzt. */
@import url('/assets/schriften/schriften.css');

/* ---------------------------------------------------------------------------
   2  Tokens
   --------------------------------------------------------------------------- */

:root {
  /* -- Brand. Standing rule: white, blue, amber, off-white. No purple. ------ */
  --mr-blue: #1558f0;
  --mr-blue-bright: #4b7bff;
  --mr-blue-deep: #0b3aa8;
  --mr-blue-wash: rgba(21, 88, 240, 0.08);
  --mr-blue-edge: rgba(21, 88, 240, 0.24);
  /* Der Fokusring, als eigenes Token: eine dunkle Seite setzt ihn auf --mr-blue-bright um. */
  --mr-focus: var(--mr-blue);
  --mr-amber: #f5a623;
  --mr-amber-wash: rgba(245, 166, 35, 0.14);

  /* -- Ground and ink ------------------------------------------------------ */
  --mr-paper: #f6f4f1; /* the page */
  --mr-surface: #ffffff; /* panels, cards, rows */
  --mr-surface-2: #efede8; /* a surface on a surface */
  --mr-ink: #141210; /* body text */
  --mr-ink-2: #4a4643; /* secondary text, still passes AA on paper */
  /* Measured, not chosen by eye: #7a7470 passed on paper (4.61) and failed on every tinted
     surface (3.94 on --mr-surface-2), and captions sit on tinted surfaces all over this file.
     This value is 4.5 or better on paper, white and surface-2. */
  --mr-ink-3: #6f6960; /* captions, quiet labels */
  --mr-line: #ddd8d2; /* hairlines between rows */
  --mr-line-strong: #c0bab4; /* borders that must be seen */

  /* -- Semantic ------------------------------------------------------------ */
  --mr-good: #1b9e5a;
  --mr-warn: #b4690e;
  --mr-bad: #d7263d;
  /* The locked grey. Never red: nothing failed, it is simply not open yet, and a red control
     reads as an error the visitor thinks he caused.
     It is DARKER than a normal disabled grey on purpose. WCAG exempts inactive controls from
     contrast, and taking that exemption would be wrong here: our locked button is not a dead
     control, it is a sentence the visitor is meant to read, carrying the price, the "coming soon"
     and the reason. Measured: #8a8378 gave 3.21 against the locked ground and failed; this value
     gives 4.64 on --mr-surface-2, 4.95 on paper, 5.43 on white. */
  --mr-locked: #6f6960;

  /* -- The camps -----------------------------------------------------------
     Values from agents/A0_koordination/LAGER_ENTWURF_v1.md, set v1.2 (08.09.),
     which A6 measured as CIEDE2000 pairs against the card frame. Under dE 20 a
     pair reads as one colour at 24 px; the weakest pair in v1.2 is 24,3.

     Do not count the entries here. How many camps there are is A8's answer, not
     a number this file may imply: "eight camps" currently means two different
     eights in two contracts. A page renders the camps a list gives it.

     The KEY is the catalog's own `welt` value (battle/catalog, KATALOG.md §2),
     because that is what A1 delivers today. The names from the camp draft
     (Antike new, Strasse folded into Unterwelt, Zucht -> Natur, Abgrund ->
     Hoelle) are aliases further down. When A8's camp bible fixes the ids this
     is one block to rename, not a search across 992 pages.                  */
  --mr-camp-unterwelt: #7a1b2e; /* Bordeaux. Still red, dark enough to leave  */
  --mr-camp-unterwelt-accent: #c0161c; /*  the ember to Hoelle (dE 30 not 19) */
  --mr-camp-kapital: #8c5a2b; /* Tabak. Moved off gold so Himmel keeps bronze */
  --mr-camp-kapital-accent: #b8860b; /* gold, as an impression under 10 %     */
  --mr-camp-antike: #7a7a3c; /* Oliv, bronze-age green                        */
  --mr-camp-antike-accent: #6e1e3a; /* Bordeaux, under 10 %. Open with A0.     */
  /* Called "Purpur" here until 08.09., which was a bad word for it in a house that has a standing
     rule against purple. The value never was purple (hue 340, wine red); only the label was. */
  --mr-camp-ordnung: #1558f0; /* Brand blue (v1.2). The frame gave its blue up  */
  --mr-camp-ordnung-accent: #141210; /*  so the state could have it. See note. */
  --mr-camp-stahl: #0e6b73; /* Petrol                                         */
  --mr-camp-stahl-accent: #19c3e6; /* signal cyan                             */
  --mr-camp-natur: #6db33f; /* Giftgruen                                      */
  --mr-camp-natur-accent: #2f6b1c;
  --mr-camp-himmel: #7eb6e6; /* Sky blue (v1.2). Bronze was dE 12 from Tabak   */
  --mr-camp-himmel-accent: #efe6d2; /* Ivory                                  */
  --mr-camp-hoelle: #ff4500; /* Ember carries the colour (v1.2), black is only */
  --mr-camp-hoelle-accent: #141414; /*  the ground it sits on                  */
  /* Fund / Archive is deliberately colourless: paper and ink plus the archive mark (D22, A0
     08.09.). It is the one camp that must not read as a colour, because a Fund card is legal in
     every deck. The swatch stays visible through its ink rim, not through a fill. */
  --mr-camp-fund: var(--mr-paper);
  --mr-camp-fund-accent: var(--mr-ink);

  /* THE INK ON A CAMP TILE, AND WHY IT CANNOT BE ONE COLOUR.
     A camp tile is a filled surface with a label on it, and the camps span from Bordeaux to sky
     blue. Measured against the whole set: white text fails on Natur (2.57), Himmel (2.16),
     Hoelle (3.44) and Fund (1.1); ink fails on Unterwelt (1.8), Kapital (3.21), Ordnung (3.28)
     and Stahl (3.0). One fixed choice makes four camps unreadable either way, and picking by eye
     picks wrong, because a saturated mid-tone looks darker than it measures.
     So every camp carries its own pair, computed once. Antike is the only one where neither
     worked on the full colour (white 4.49, ink 4.16), so its TILE is 4 percent darker than its
     mark; the mark keeps the exact camp colour. Same principle the printed card already uses,
     where a mechanic pill decides its own ink (card.js, inkOn).
     Lowest ratio in the set after this: 4.81. */
  --mr-camp-unterwelt-tile: #7a1b2e; /* 10.38 */
  --mr-camp-unterwelt-on: #ffffff;
  --mr-camp-kapital-tile: #8c5a2b; /*  5.81 */
  --mr-camp-kapital-on: #ffffff;
  --mr-camp-antike-tile: #75753a; /*  4.81 */
  --mr-camp-antike-on: #ffffff;
  --mr-camp-ordnung-tile: #0b3aa8; /*  9.61, deep blue: the tile must not be the action blue */
  --mr-camp-ordnung-on: #ffffff;
  --mr-camp-stahl-tile: #0e6b73; /*  6.23 */
  --mr-camp-stahl-on: #ffffff;
  --mr-camp-natur-tile: #6db33f; /*  7.27 */
  --mr-camp-natur-on: #141210;
  --mr-camp-himmel-tile: #7eb6e6; /*  8.65 */
  --mr-camp-himmel-on: #141210;
  --mr-camp-hoelle-tile: #ff4500; /*  5.43 */
  --mr-camp-hoelle-on: #141210;
  --mr-camp-fund-tile: #f6f4f1; /* 17.02 */
  --mr-camp-fund-on: #141210;

  /* ONE COLLISION THAT IS DELIBERATE AND HAS TO BE HANDLED, NOT HIDDEN.
     Since v1.2 the camp Ordnung IS the brand blue. On a card that is fine, because the frame gave
     its blue up. On the website it is not: a filled blue surface next to a blue button reads as a
     button.

     This rule used to say "never next to an action", and that phrasing was worthless. The primary
     button lives in the sticky nav, so it is on screen at every scroll position on every page.
     "Next to an action" is therefore ALWAYS true, and a rule that needs a judgement call per page
     is a rule 992 generated pages will break. It was already broken in the first draft that used
     it.
     So the collision is removed in the token instead of avoided by discipline: the Ordnung TILE is
     the deep blue, the Ordnung MARK keeps the exact camp colour. Same construction Antike already
     uses, and it improves the tile on the way (white 5.70 on the brand blue, 9.61 on the deep).
     Measured pairs for the whole set are in LAGER_ENTWURF_v1.md section "Farbsatz v1.2".
     Unterwelt and Hoelle can still turn once (decided red against ember), so they stay
     variables like everything else here. */

  /* -- Rarity. Mirrors web/src/theme.ts, no purple in the ladder. ---------- */
  --mr-rar-c: #7a7470;
  --mr-rar-uc: #1b9e5a;
  --mr-rar-r: #1558f0;
  --mr-rar-e: #0e7490;
  --mr-rar-l: #f5a623;
  --mr-rar-s: #7a1b2e;

  /* -- Space. One 4 px ladder; every gap on the site is one of these. ------ */
  --mr-s1: 4px;
  --mr-s2: 8px;
  --mr-s3: 12px;
  --mr-s4: 16px;
  --mr-s5: 24px;
  --mr-s6: 32px;
  --mr-s7: 48px;
  --mr-s8: 64px;
  --mr-s9: 96px;

  /* -- Radius and shadow --------------------------------------------------- */
  --mr-r1: 6px;
  --mr-r2: 10px;
  --mr-r3: 16px;
  --mr-r4: 24px;
  --mr-r-pill: 999px;
  --mr-shadow-1: 0 1px 2px rgba(20, 18, 16, 0.06), 0 2px 8px rgba(20, 18, 16, 0.05);
  --mr-shadow-2: 0 2px 6px rgba(20, 18, 16, 0.07), 0 12px 28px rgba(20, 18, 16, 0.09);
  --mr-shadow-lift: 0 6px 14px rgba(20, 18, 16, 0.1), 0 22px 48px rgba(20, 18, 16, 0.12);

  /* -- Type ---------------------------------------------------------------- */
  --mr-font: 'Outfit', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --mr-font-alt: 'Space Grotesk', ui-monospace, 'Segoe UI', sans-serif;
  --mr-font-num: 'Space Grotesk', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Fluid steps. clamp() so a phone and a 27 inch screen both read well and
     nothing needs a media query for size alone. */
  --mr-t-hero: clamp(2.25rem, 1.35rem + 4.2vw, 4.5rem);
  --mr-t-h1: clamp(1.85rem, 1.25rem + 2.6vw, 3rem);
  --mr-t-h2: clamp(1.45rem, 1.15rem + 1.4vw, 2.125rem);
  --mr-t-h3: clamp(1.15rem, 1.03rem + 0.55vw, 1.5rem);
  --mr-t-body: clamp(1rem, 0.97rem + 0.15vw, 1.0625rem);
  --mr-t-small: 0.9375rem;
  --mr-t-cap: 0.8125rem;
  --mr-t-micro: 0.6875rem;

  /* -- Motion. Everything is switched off under reduced-motion, section 3. -- */
  --mr-fast: 120ms;
  --mr-base: 220ms;
  --mr-slow: 420ms;
  --mr-ease: cubic-bezier(0.22, 0.61, 0.36, 1);

  /* -- Layout -------------------------------------------------------------- */
  --mr-page: 1200px;
  --mr-page-narrow: 760px;
  --mr-gutter: clamp(var(--mr-s4), 1rem + 2vw, var(--mr-s8));
  --mr-nav-h: 64px;

  /* -- Stacking. Named, so nobody invents z-index: 99999 again. ------------ */
  --mr-z-base: 1;
  --mr-z-sticky: 100;
  --mr-z-nav: 500;
  --mr-z-overlay: 800;
  --mr-z-modal: 900;
  --mr-z-toast: 1000;
}

/* Camp aliases from the camp draft. Same values, second name, so a page can
   write data-camp="natur" today and data-camp="zucht" keeps working. A8's camp
   bible decides which name survives; then one of the two blocks is deleted. */
:root {
  --mr-camp-zucht-tile: var(--mr-camp-natur-tile);
  --mr-camp-zucht-on: var(--mr-camp-natur-on);
  --mr-camp-abgrund-tile: var(--mr-camp-hoelle-tile);
  --mr-camp-abgrund-on: var(--mr-camp-hoelle-on);
  --mr-camp-strasse-tile: var(--mr-camp-unterwelt-tile);
  --mr-camp-strasse-on: var(--mr-camp-unterwelt-on);
  --mr-camp-zucht: var(--mr-camp-natur);
  --mr-camp-zucht-accent: var(--mr-camp-natur-accent);
  --mr-camp-abgrund: var(--mr-camp-hoelle);
  --mr-camp-abgrund-accent: var(--mr-camp-hoelle-accent);
  --mr-camp-strasse: var(--mr-camp-unterwelt); /* folded in, decision of 07.09. */
  --mr-camp-strasse-accent: var(--mr-camp-unterwelt-accent);
}

/* ---------------------------------------------------------------------------
   3  Base
   --------------------------------------------------------------------------- */

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

/* Scoped to .mr so this file can be dropped onto an existing page without
   taking over elements it was not asked about. A7 puts class="mr" on <body>. */
.mr {
  margin: 0;
  background: var(--mr-paper);
  color: var(--mr-ink);
  font-family: var(--mr-font);
  font-size: var(--mr-t-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

.mr h1,
.mr h2,
.mr h3,
.mr h4 {
  margin: 0 0 var(--mr-s3);
  line-height: 1.14;
  letter-spacing: -0.02em;
  font-weight: 700;
  text-wrap: balance;
}

.mr h1 {
  font-size: var(--mr-t-h1);
}
.mr h2 {
  font-size: var(--mr-t-h2);
}
.mr h3 {
  font-size: var(--mr-t-h3);
  letter-spacing: -0.01em;
}

.mr p {
  margin: 0 0 var(--mr-s4);
  max-width: 68ch; /* measure, not container width: 140 characters is unreadable */
  text-wrap: pretty;
}

/* Link colour belongs to links in PROSE, not to every anchor on the page.
 *
 * `.mr a` looks harmless and is not: it scores 0,1,1, which beats every single-class component
 * below it. An <a class="mr-btn"> then gets blue text on a blue ground and the button reads as
 * an empty rectangle; the nav links, the camp chips and the card tiles lose their colours the
 * same way. Found on the first full page that used buttons as links, not in review.
 *
 * So: prose links are coloured, components colour themselves. A component that wants the link
 * colour asks for it. */
/* "A link in prose" is an anchor WITHOUT A CLASS. A component always carries one, so :not([class])
   keeps this rule out of every nav list, footer column and camp index, all of which are <li>. The
   first version said `.mr li a` and turned the whole navigation blue. A prose link that does need
   a class asks for .mr-link. */
.mr :is(p, li, td, dd, blockquote) a:not([class]),
.mr-lede a:not([class]),
.mr-link {
  color: var(--mr-blue);
  text-decoration-color: var(--mr-blue-edge);
  text-underline-offset: 3px;
}

.mr :is(p, li, td, dd, blockquote) a:not([class]):hover,
.mr-lede a:not([class]):hover,
.mr-link:hover {
  color: var(--mr-blue-deep);
  text-decoration-color: currentColor;
}

/* Every other anchor inherits its surroundings until a component says otherwise. This exists
 * only to kill the browser default, whose visited state is PURPLE, and the standing brand rule
 * is that there is no purple on this site.
 *
 * :where() on both halves so the rule carries zero specificity. Written as `.mr a` it scores
 * 0,1,1 and beats .mr-btn, .mr-nav__link and every other single-class component below it, which
 * is exactly the bug this is replacing: a button that inherited the page's ink instead of its
 * own white. A reset must never outrank the thing it is resetting for. */
:where(.mr) :where(a) {
  color: inherit;
}

.mr img,
.mr svg,
.mr video {
  display: block;
  max-width: 100%;
}

.mr hr {
  border: 0;
  border-top: 1px solid var(--mr-line);
  margin: var(--mr-s6) 0;
}

.mr code,
.mr kbd {
  font-family: var(--mr-font-num);
  font-size: 0.92em;
  /* A card number, a class list, a URL or a long German compound has no break opportunity and
     would push the page sideways on a phone. Breaking mid-token is ugly; a sideways-scrolling
     page is broken. */
  overflow-wrap: anywhere;
}

/* Same reason, one level up: card names and camp names arrive from a catalog nobody clamps. */
.mr p,
.mr li,
.mr td,
.mr th,
.mr dd {
  overflow-wrap: break-word;
}

/* One focus ring for the whole site. :focus-visible only, so a mouse click does
   not draw it but a Tab key always does. The offset keeps it off the glyphs.

   Its own token, because one blue cannot serve both grounds. The brand blue measures 5.19 on
   paper and 3.17 on a dark panel: still over the 3:1 a non-text element needs, but only just, and
   the one place a keyboard visitor depends on. A dark page therefore sets --mr-focus to the
   bright blue (4.79 there). Not a violation that was fixed, a margin that was too thin. */
.mr :focus-visible {
  outline: 3px solid var(--mr-focus);
  outline-offset: 2px;
  border-radius: var(--mr-r1);
}

/* Skip link. Required for the seven-section landing page: without it a keyboard
   visitor tabs through the whole navigation on every single page. */
.mr-skip {
  position: absolute;
  left: var(--mr-s4);
  top: calc(var(--mr-s4) * -4);
  z-index: var(--mr-z-toast);
  padding: var(--mr-s3) var(--mr-s4);
  background: var(--mr-surface);
  border: 2px solid var(--mr-blue);
  border-radius: var(--mr-r2);
  font-weight: 600;
  transition: top var(--mr-fast) var(--mr-ease);
}

.mr-skip:focus {
  top: var(--mr-s4);
}

.mr-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Anything that moves, stops moving when the visitor asked for that. Nothing on
   this site conveys meaning through motion alone, so switching it off costs no
   information. */
@media (prefers-reduced-motion: reduce) {
  .mr *,
  .mr *::before,
  .mr *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------------------------------------------------------------------------
   4  Layout
   --------------------------------------------------------------------------- */

.mr-page {
  width: 100%;
  max-width: var(--mr-page);
  margin-inline: auto;
  padding-inline: var(--mr-gutter);
}

.mr-page--narrow {
  max-width: var(--mr-page-narrow);
}

.mr-section {
  padding-block: clamp(var(--mr-s7), 3rem + 4vw, var(--mr-s9));
}

.mr-section--tight {
  padding-block: var(--mr-s6);
}

/* Alternating grounds give the seven sections their own rhythm without any
   section needing its own stylesheet. */
.mr-section--surface {
  background: var(--mr-surface);
  border-block: 1px solid var(--mr-line);
}

.mr-section--ink {
  background: var(--mr-ink);
  color: var(--mr-paper);
}

.mr-section--ink h1,
.mr-section--ink h2,
.mr-section--ink h3 {
  color: #fff;
}

.mr-section--ink a {
  color: var(--mr-blue-bright);
}

.mr-eyebrow {
  display: block;
  margin-bottom: var(--mr-s2);
  font-family: var(--mr-font-alt);
  font-size: var(--mr-t-cap);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--mr-blue);
}

.mr-section--ink .mr-eyebrow {
  color: var(--mr-amber);
}

.mr-lede {
  font-size: clamp(1.0625rem, 1rem + 0.5vw, 1.25rem);
  color: var(--mr-ink-2);
  max-width: 60ch;
}

.mr-section--ink .mr-lede {
  color: rgba(246, 244, 241, 0.78);
}

/* auto-fit grid: the column count follows the width, no breakpoints needed. */
.mr-grid {
  display: grid;
  gap: var(--mr-s5);
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
}

.mr-grid--tight {
  gap: var(--mr-s3);
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
}

.mr-grid--wide {
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
}

.mr-panel {
  background: var(--mr-surface);
  border: 1px solid var(--mr-line);
  border-radius: var(--mr-r3);
  padding: var(--mr-s5);
  box-shadow: var(--mr-shadow-1);
}

/* A NOTE THAT STOPS THE READER, without a colour that cannot be read.
   Used where a surface has to say why it is not doing its job: the buyability list has expired,
   an edition is closed, a number on the page is a placeholder. It is not a decoration, so it
   carries role="status" at the call site.

   The warn colour never carries the text. Measured on the light theme: --mr-warn as body text is
   3.85 on paper and 3.42 on its own tinted ground, both under 4.5. So the ground is a 10 percent
   wash and the ink stays page ink (15.13), and the colour does its work as the left edge, where
   contrast is a visibility question and not a reading one. Same rule as the rarity mark.
   color-mix, not a fixed hex, so a page that overrides --mr-surface (the dark draft does) gets a
   note that follows it instead of a beige box on black. */
.mr-note {
  border: 1px solid color-mix(in srgb, var(--mr-warn) 30%, var(--mr-line));
  border-left: 3px solid var(--mr-warn);
  border-radius: var(--mr-r2);
  background: color-mix(in srgb, var(--mr-warn) 10%, var(--mr-surface));
  color: var(--mr-ink);
  padding: var(--mr-s4) var(--mr-s5);
  margin: var(--mr-s5) 0;
  font-size: var(--mr-t-small);
  line-height: 1.55;
}

.mr-note--warn {
  /* the default already is the warning; the modifier exists so a call site reads unambiguously
     and so a second kind (info, done) can be added without renaming anything */
}

.mr-note--info {
  border-color: color-mix(in srgb, var(--mr-blue) 30%, var(--mr-line));
  border-left-color: var(--mr-blue);
  background: color-mix(in srgb, var(--mr-blue) 8%, var(--mr-surface));
}

.mr-panel--flat {
  box-shadow: none;
}

.mr-panel--pad-lg {
  padding: var(--mr-s6);
}

/* A panel that is a link. Lifts on hover, but only where hover really exists;
   on a touch screen :hover sticks after a tap and the card stays raised. */
@media (hover: hover) {
  .mr-panel--link {
    transition: transform var(--mr-base) var(--mr-ease), box-shadow var(--mr-base) var(--mr-ease);
  }
  .mr-panel--link:hover {
    transform: translateY(-3px);
    box-shadow: var(--mr-shadow-2);
  }
}

/* ---------------------------------------------------------------------------
   5  Navigation and footer
   --------------------------------------------------------------------------- */

.mr-nav {
  position: sticky;
  top: 0;
  z-index: var(--mr-z-nav);
  min-height: var(--mr-nav-h);
  display: flex;
  align-items: center;
  gap: var(--mr-s4);
  padding-inline: var(--mr-gutter);
  background: rgba(246, 244, 241, 0.88);
  backdrop-filter: saturate(1.4) blur(10px);
  border-bottom: 1px solid var(--mr-line);
}

.mr-nav__brand {
  font-family: var(--mr-font-alt);
  font-weight: 700;
  font-size: 1.0625rem;
  letter-spacing: 0.01em;
  color: var(--mr-ink);
  text-decoration: none;
  margin-right: auto;
  white-space: nowrap;
  /* The brand is the one thing in the bar that must not shrink: min-width:0 below lets every
     flex item collapse, and a collapsed nowrap brand does not clip, it overprints the links. */
  flex: none;
}

.mr-nav__brand b {
  color: var(--mr-blue);
}

/* A flex item is min-width:auto by default, which means it refuses to shrink below its content.
   Without this, the <nav> holding the scrolling link list grows to the full width of its links,
   the list never scrolls, and the whole page scrolls sideways instead. Measured at 390 px: 186 px
   of horizontal page scroll before this line, 0 after. */
.mr-nav > * {
  min-width: 0;
}

/* Die Energieanzeige darf NICHT schrumpfen. `min-width: 0` daruber laesst jedes Kind der Leiste
   zusammenfallen, und die Anzeige stand als letztes Kind rechts: auf einer Aufnahme bei 390 px
   war sie am Rand abgeschnitten ("790 ENERGI"), und "Arena" fehlte ganz. Zusammen brauchte die
   Leiste dort rund 460 px. */
.mr-nav > .mr-badge--energie {
  flex: none;
}

/* Unter 560 px tritt das Wort zurueck und der Blitz nimmt seinen Platz. Das spart die rund
   70 px, die "Arena" gefehlt haben. Das Wort steht weiter im aria-label des Elements, es geht
   also nur fuer das Auge weg, nicht fuer eine Vorlesehilfe. */
@media (max-width: 560px) {
  .mr-nav {
    gap: var(--mr-s2);
  }
  .mr-nav__link {
    padding-inline: var(--mr-s2);
  }
  .mr-badge--energie .mr-badge__unit {
    display: none;
  }
  .mr-badge--energie::before {
    content: '\26A1';
    margin-right: 0.25em;
  }
}

.mr-nav__links {
  display: flex;
  align-items: center;
  gap: var(--mr-s1);
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-x: auto;
  scrollbar-width: none;
}

.mr-nav__links::-webkit-scrollbar {
  display: none;
}

.mr-nav__link {
  display: inline-block;
  padding: var(--mr-s2) var(--mr-s3);
  border-radius: var(--mr-r2);
  color: var(--mr-ink-2);
  font-weight: 500;
  font-size: var(--mr-t-small);
  text-decoration: none;
  white-space: nowrap;
}

.mr-nav__link:hover {
  background: var(--mr-blue-wash);
  color: var(--mr-blue-deep);
}

.mr-nav__link[aria-current='page'] {
  color: var(--mr-ink);
  background: var(--mr-surface-2);
  font-weight: 600;
}

.mr-foot {
  background: var(--mr-ink);
  color: rgba(246, 244, 241, 0.72);
  padding-block: var(--mr-s7) var(--mr-s5);
  font-size: var(--mr-t-small);
}

.mr-foot a {
  color: rgba(246, 244, 241, 0.86);
  text-decoration: none;
}

.mr-foot a:hover {
  color: #fff;
  text-decoration: underline;
}

.mr-foot__cols {
  display: grid;
  gap: var(--mr-s5);
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
}

.mr-foot__legal {
  margin-top: var(--mr-s6);
  padding-top: var(--mr-s4);
  border-top: 1px solid rgba(246, 244, 241, 0.14);
  font-size: var(--mr-t-cap);
  color: rgba(246, 244, 241, 0.55);
}

/* ---------------------------------------------------------------------------
   6  Buttons

   .mr-btn is a <button> or an <a>. The locked state below is the one that
   matters for this project: The owner decided on 07.09. that everything carrying
   real money or money value is built and visible, and its button says
   "coming soon" and cannot be pressed. So that is a first-class button state
   here, not a disabled afterthought, and it looks deliberate rather than broken.
   --------------------------------------------------------------------------- */

.mr-btn {
  --_bg: var(--mr-blue);
  --_fg: #fff;
  --_bd: transparent;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--mr-s2);
  min-height: 44px; /* touch target floor, the game is played on a phone */
  padding: var(--mr-s3) var(--mr-s5);
  border: 1px solid var(--_bd);
  border-radius: var(--mr-r2);
  background: var(--_bg);
  color: var(--_fg);
  font-family: inherit;
  font-size: var(--mr-t-body);
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--mr-fast) var(--mr-ease), transform var(--mr-fast) var(--mr-ease),
    box-shadow var(--mr-fast) var(--mr-ease);
}

.mr-btn:hover {
  --_bg: var(--mr-blue-deep);
}

.mr-btn:active {
  transform: translateY(1px);
}

.mr-btn--ghost {
  --_bg: transparent;
  --_fg: var(--mr-ink);
  --_bd: var(--mr-line-strong);
}

.mr-btn--ghost:hover {
  --_bg: var(--mr-surface);
  --_fg: var(--mr-blue-deep);
  --_bd: var(--mr-blue);
}

.mr-btn--quiet {
  --_bg: transparent;
  --_fg: var(--mr-ink-2);
  --_bd: transparent;
  min-height: 36px;
  padding: var(--mr-s2) var(--mr-s3);
  font-weight: 500;
}

.mr-btn--quiet:hover {
  --_bg: var(--mr-surface-2);
  --_fg: var(--mr-ink);
}

.mr-btn--amber {
  --_bg: var(--mr-amber);
  --_fg: #2a1c05;
}

.mr-btn--amber:hover {
  --_bg: #d98f14;
}

.mr-btn--danger {
  --_bg: var(--mr-bad);
}

.mr-btn--danger:hover {
  --_bg: #ac1c30;
}

.mr-btn--lg {
  min-height: 54px;
  padding: var(--mr-s4) var(--mr-s6);
  font-size: 1.0625rem;
}

.mr-btn--block {
  display: flex;
  width: 100%;
}

.mr-btn[disabled],
.mr-btn[aria-disabled='true'] {
  cursor: not-allowed;
}

/* The locked button. Use on the real-money and money-value actions:
       <button class="mr-btn mr-btn--locked" disabled
               aria-describedby="why-locked">Mit Echtgeld kaufen</button>
   and put the reason in a <p id="why-locked"> next to it. Rule from the brief:
   the button is fully built, visible, unpressable, and it says why.

   Grey, not red. Nothing failed here, it is simply not open yet, and a red
   button reads as an error the visitor thinks he caused. */
.mr-btn--locked {
  --_bg: var(--mr-surface-2);
  --_fg: var(--mr-locked);
  --_bd: var(--mr-line);
  cursor: not-allowed;
}

.mr-btn--locked:hover,
.mr-btn--locked:active {
  --_bg: var(--mr-surface-2);
  transform: none;
}

/* The badge is a flex item, not an absolutely positioned overlay with reserved padding. .mr-btn
   is already inline-flex, so ::after joins the row on its own and a two-line label no longer
   pushes the badge out of its reserved gap. */
.mr-btn--locked::after {
  content: 'coming soon';
  flex: none;
  /* The word is settable, because "coming soon" is only true for the money lock. A button that
     is shut because the player is on tier II and this needs tier III says so; labelling that
     "coming soon" tells him to wait for us when he should be building. Set data-lock to change
     the word. */
  padding: 3px var(--mr-s2);
  border-radius: var(--mr-r-pill);
  background: var(--mr-surface);
  border: 1px solid var(--mr-line);
  font-family: var(--mr-font-alt);
  font-size: var(--mr-t-micro);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mr-locked);
}

.mr-btn--locked[data-lock]::after {
  content: attr(data-lock);
}

/* A whole region can be locked, not only its button: the shop's real-money
   column, an arena stake box. Keeps the content readable; it is meant to be
   read, that is the point of building it before it opens. */
.mr-locked-zone {
  position: relative;
  border: 1px dashed var(--mr-line-strong);
  border-radius: var(--mr-r3);
  padding: var(--mr-s5);
  background: repeating-linear-gradient(
    135deg,
    var(--mr-surface),
    var(--mr-surface) 14px,
    var(--mr-surface-2) 14px,
    var(--mr-surface-2) 28px
  );
}

.mr-locked-zone > * {
  opacity: 0.72;
}

.mr-locked-zone__note {
  opacity: 1;
  margin: 0 0 var(--mr-s4);
  padding: var(--mr-s3) var(--mr-s4);
  border-radius: var(--mr-r2);
  background: var(--mr-surface);
  border: 1px solid var(--mr-line);
  font-size: var(--mr-t-small);
  color: var(--mr-ink-2);
}

/* ---------------------------------------------------------------------------
   7  Badges, pills, prices, odds
   --------------------------------------------------------------------------- */

.mr-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--mr-s1);
  padding: 2px var(--mr-s2);
  border-radius: var(--mr-r-pill);
  border: 1px solid var(--mr-line);
  background: var(--mr-surface);
  font-family: var(--mr-font-alt);
  font-size: var(--mr-t-micro);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--mr-ink-2);
  white-space: nowrap;
}

.mr-badge--blue {
  border-color: var(--mr-blue-edge);
  background: var(--mr-blue-wash);
  color: var(--mr-blue-deep);
}

.mr-badge--amber {
  border-color: rgba(245, 166, 35, 0.4);
  background: var(--mr-amber-wash);
  color: #8a5c05;
}

.mr-badge--good {
  border-color: rgba(27, 158, 90, 0.35);
  background: rgba(27, 158, 90, 0.1);
  color: #14713f;
}

.mr-badge--locked {
  border-style: dashed;
  color: var(--mr-locked);
}

/* Camp badge. Drive it from data, never from a per-camp class: the page writes
   data-camp="unterwelt" and the colour arrives on its own. That is what makes
   an A8 rename a one-block edit. */
.mr-camp {
  display: inline-flex;
  align-items: center;
  gap: var(--mr-s2);
  padding: 3px var(--mr-s3) 3px var(--mr-s2);
  border-radius: var(--mr-r-pill);
  border: 1px solid var(--mr-line);
  background: var(--mr-surface);
  font-size: var(--mr-t-cap);
  font-weight: 600;
  color: var(--mr-ink);
  text-decoration: none;
  white-space: nowrap;
}

.mr-camp::before {
  content: '';
  width: 12px;
  height: 12px;
  border-radius: 3px;
  background: var(--_camp, var(--mr-ink-3));
  box-shadow: inset 0 0 0 2px var(--_camp-accent, transparent);
  flex: none;
}

.mr-camp:hover {
  border-color: var(--_camp, var(--mr-line-strong));
}

[data-camp='unterwelt'] {
  --_camp: var(--mr-camp-unterwelt);
  --_camp-accent: var(--mr-camp-unterwelt-accent);
  --_tile: var(--mr-camp-unterwelt-tile);
  --_on: var(--mr-camp-unterwelt-on);
}
[data-camp='strasse'] {
  --_camp: var(--mr-camp-strasse);
  --_camp-accent: var(--mr-camp-strasse-accent);
  --_tile: var(--mr-camp-strasse-tile);
  --_on: var(--mr-camp-strasse-on);
}
[data-camp='kapital'] {
  --_camp: var(--mr-camp-kapital);
  --_camp-accent: var(--mr-camp-kapital-accent);
  --_tile: var(--mr-camp-kapital-tile);
  --_on: var(--mr-camp-kapital-on);
}
[data-camp='antike'] {
  --_camp: var(--mr-camp-antike);
  --_camp-accent: var(--mr-camp-antike-accent);
  --_tile: var(--mr-camp-antike-tile);
  --_on: var(--mr-camp-antike-on);
}
[data-camp='ordnung'] {
  --_camp: var(--mr-camp-ordnung);
  --_camp-accent: var(--mr-camp-ordnung-accent);
  --_tile: var(--mr-camp-ordnung-tile);
  --_on: var(--mr-camp-ordnung-on);
}
[data-camp='stahl'] {
  --_camp: var(--mr-camp-stahl);
  --_camp-accent: var(--mr-camp-stahl-accent);
  --_tile: var(--mr-camp-stahl-tile);
  --_on: var(--mr-camp-stahl-on);
}
[data-camp='natur'],
[data-camp='zucht'] {
  --_camp: var(--mr-camp-natur);
  --_camp-accent: var(--mr-camp-natur-accent);
  --_tile: var(--mr-camp-natur-tile);
  --_on: var(--mr-camp-natur-on);
}
[data-camp='himmel'] {
  --_camp: var(--mr-camp-himmel);
  --_camp-accent: var(--mr-camp-himmel-accent);
  --_tile: var(--mr-camp-himmel-tile);
  --_on: var(--mr-camp-himmel-on);
}
[data-camp='hoelle'],
[data-camp='abgrund'] {
  --_camp: var(--mr-camp-hoelle);
  --_camp-accent: var(--mr-camp-hoelle-accent);
  --_tile: var(--mr-camp-hoelle-tile);
  --_on: var(--mr-camp-hoelle-on);
}
[data-camp='fund'] {
  --_camp: var(--mr-camp-fund);
  --_camp-accent: var(--mr-camp-fund-accent);
  --_tile: var(--mr-camp-fund-tile);
  --_on: var(--mr-camp-fund-on);
}

/* Rarity, same mechanism. Keys are the catalog's own letters (KATALOG.md §2). */
[data-rarity='C'] {
  --_rar: var(--mr-rar-c);
}
[data-rarity='UC'] {
  --_rar: var(--mr-rar-uc);
}
[data-rarity='R'] {
  --_rar: var(--mr-rar-r);
}
[data-rarity='E'] {
  --_rar: var(--mr-rar-e);
}
[data-rarity='L'] {
  --_rar: var(--mr-rar-l);
}
[data-rarity='S'] {
  --_rar: var(--mr-rar-s);
}

/* The mark carries the rarity colour, the label stays ink.
   Colour as TEXT colour fails here and it is not close: Legendary amber on white measured 2.03
   and Uncommon green 3.45, at 11 px. It is also the rule the printed card already follows, where
   the pill is filled with the mechanic colour and its own content decides its ink. A rarity is an
   identity, and an identity belongs on a mark, not on the letters. */
.mr-rarity {
  display: inline-flex;
  align-items: center;
  gap: var(--mr-s1);
  font-family: var(--mr-font-alt);
  font-size: var(--mr-t-micro);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mr-ink-2);
}

.mr-rarity::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 2px;
  transform: rotate(45deg);
  background: var(--_rar, var(--mr-ink-3));
  flex: none;
}

/* Price. Tabular figures so a column of prices lines up on the decimal. */
.mr-price {
  font-family: var(--mr-font-num);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.mr-price--lg {
  font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2rem);
}

.mr-price__cur {
  font-size: 0.62em;
  font-weight: 600;
  color: var(--mr-ink-3);
  margin-left: 0.2em;
}

/* Soft currency vs hard currency read differently at a glance, on purpose:
   the visitor must never be one careless tap away from thinking he spent euros. */
.mr-price--soft {
  color: var(--mr-ink);
}

.mr-price--hard {
  color: var(--mr-locked); /* while the euro button is locked it is not loud */
}

/* The odds table. The brief: probabilities stand openly on the page, also where
   no law demands it. So it gets a real component, not a footnote. */
.mr-odds {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--mr-t-small);
}

.mr-odds caption {
  caption-side: top;
  text-align: left;
  padding-bottom: var(--mr-s2);
  font-size: var(--mr-t-cap);
  color: var(--mr-ink-2);
}

.mr-odds th,
.mr-odds td {
  padding: var(--mr-s2) var(--mr-s3);
  border-bottom: 1px solid var(--mr-line);
  text-align: left;
}

.mr-odds th {
  font-family: var(--mr-font-alt);
  font-size: var(--mr-t-cap);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--mr-ink-3);
}

.mr-odds td:last-child,
.mr-odds th:last-child {
  text-align: right;
  font-family: var(--mr-font-num);
  font-variant-numeric: tabular-nums;
}

/* A bar behind the number, so the shape of the distribution is visible without
   reading six percentages. Width comes from a --p variable on the row. */
.mr-odds tbody tr {
  background: linear-gradient(
    to right,
    color-mix(in srgb, var(--_rar, var(--mr-blue)) 14%, transparent) calc(var(--p, 0) * 1%),
    transparent calc(var(--p, 0) * 1%)
  );
}

/* ---------------------------------------------------------------------------
   8  The card frame

   The real card is drawn by battle/art/template/card.js at 744 x 1039 (63 x 88
   mm at 300 dpi) and A3 only scales it. This block is the frame AROUND it:
   the slot, the aspect ratio, the placeholder for the 262 of 270 cards that
   have no artwork yet, and the caption. It never draws a card face; two truths
   about how a card looks is exactly the drift the print template exists to
   prevent.
   --------------------------------------------------------------------------- */

.mr-card {
  display: block;
  position: relative;
  aspect-ratio: 744 / 1039;
  border-radius: var(--mr-r2);
  background: var(--mr-surface);
  border: 1px solid var(--mr-line);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  box-shadow: var(--mr-shadow-1);
}

@media (hover: hover) {
  .mr-card {
    transition: transform var(--mr-base) var(--mr-ease), box-shadow var(--mr-base) var(--mr-ease);
  }
  a.mr-card:hover,
  .mr-card--link:hover {
    transform: translateY(-4px);
    box-shadow: var(--mr-shadow-lift);
  }
}

/* Putting the print card into the slot.
 *
 * card.css says it in its own header: every zone is a percentage of the trimmed card and every
 * type size is written against --card-w, "so the same rules hold at any output resolution".
 * So the card is not scaled, it is told how wide it is, and it lays itself out. Two variables.
 *
 * Why not transform: scale(). Because it does not work and because it should not be wanted.
 * It does not work: a scale factor is a number, and CSS cannot divide a length by a length to
 * get one, so scale(calc(100cqw / 744)) is invalid and silently becomes no transform at all.
 * It should not be wanted: a scaled card is a picture of text. A7 is building pages for Google
 * out of these, and text under a transform stays selectable but a card sized this way keeps
 * real line breaks, real hyphenation and real font metrics at the size it is actually read.
 *
 * --pt is 4.167px at --card-w 744px, that is one point at 300 dpi, so it follows as
 * card-w / 178.545. --hair already follows --card-w on its own inside card.css.  */
.mr-card {
  container-type: inline-size;
}

.mr-card .card {
  --card-w: 100cqw;
  --card-h: calc(100cqw * 1039 / 744);
  --pt: calc(100cqw / 178.545);
  box-shadow: none; /* card.css draws a hairline for the proof sheet; the slot has a border */
}

/* Below roughly 150 px the printed rules text is no longer readable at any font size, and
   pretending otherwise gives a grey smear. Under that width the card shows its head and its
   name and hides the prose, which is what a thumbnail is for. */
@container (max-width: 150px) {
  .mr-card .rules,
  .mr-card .alarm,
  .mr-card .footer {
    display: none;
  }
}

/* The placeholder. 262 of 270 cards have no picture today (ASSET_MANIFEST.json
   counts 8). The rule from the brief: a frame with the name, never a broken
   image reference. It carries the camp colour, so a wall of placeholders still
   reads as a collection and not as a page that failed to load. */
.mr-card--placeholder {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--mr-s4);
  background: linear-gradient(
    160deg,
    color-mix(in srgb, var(--_camp, var(--mr-ink-3)) 16%, var(--mr-surface)),
    var(--mr-surface) 62%
  );
  border-color: color-mix(in srgb, var(--_camp, var(--mr-line-strong)) 40%, var(--mr-line));
}

.mr-card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--mr-s2);
}

.mr-card__cost {
  display: grid;
  place-items: center;
  width: 2em;
  height: 2em;
  border-radius: 50%;
  background: var(--mr-ink);
  color: var(--mr-paper);
  font-family: var(--mr-font-num);
  font-weight: 700;
  font-size: var(--mr-t-small);
}

.mr-card__name {
  font-weight: 700;
  font-size: var(--mr-t-small);
  line-height: 1.24;
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.mr-card__type {
  font-family: var(--mr-font-alt);
  font-size: var(--mr-t-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mr-ink-2);
}

.mr-card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--mr-s2);
  font-size: var(--mr-t-cap);
}

.mr-card__id {
  font-family: var(--mr-font-num);
  font-size: var(--mr-t-micro);
  color: var(--mr-ink-3);
  letter-spacing: 0.04em;
}

/* "Not owned" in the collection and in the gap view after a sandbox match. */
.mr-card--missing {
  filter: grayscale(1);
  opacity: 0.5;
}

.mr-card__ribbon {
  position: absolute;
  top: var(--mr-s3);
  right: 0;
  padding: 2px var(--mr-s3);
  border-radius: var(--mr-r-pill) 0 0 var(--mr-r-pill);
  background: var(--mr-ink);
  color: var(--mr-paper);
  font-family: var(--mr-font-alt);
  font-size: var(--mr-t-micro);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Serial number from item_instances. It is the one thing a printed card cannot
   carry and the one thing a collector looks for first. */
.mr-card__serial {
  position: absolute;
  left: var(--mr-s3);
  bottom: var(--mr-s3);
  padding: 2px var(--mr-s2);
  border-radius: var(--mr-r1);
  background: rgba(20, 18, 16, 0.72);
  color: #fff;
  font-family: var(--mr-font-num);
  font-size: var(--mr-t-micro);
  letter-spacing: 0.06em;
}

.mr-card-caption {
  margin-top: var(--mr-s2);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--mr-s2);
  font-size: var(--mr-t-cap);
  color: var(--mr-ink-2);
}

/* ---------------------------------------------------------------------------
   9  The slider

   Hand-made, no library, as the brief demands. It is a scroll container with
   scroll-snap: swipeable on a phone because that is what a scroll container is,
   keyboard operable because it is focusable and arrow keys scroll a focused
   scroll container, and screen-reader sane because it is a plain list.

   Zero JavaScript is required. The arrow buttons and the dots are anchor links
   to the slide ids, so they work with scripting off as well.
   --------------------------------------------------------------------------- */

.mr-slider {
  position: relative;
}

.mr-slider__track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: var(--mr-slide-w, min(84%, 460px));
  gap: var(--mr-s4);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: thin;
  padding-block: var(--mr-s2) var(--mr-s4);
  /* The inline padding lets the first and last slide sit centred instead of
     glued to the viewport edge, and scroll-padding makes snapping agree. */
  padding-inline: var(--mr-s1);
  scroll-padding-inline: var(--mr-s1);
}

.mr-slider__track > * {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.mr-slider__track--wide {
  --mr-slide-w: min(92%, 720px);
}

.mr-slider__track--cards {
  --mr-slide-w: clamp(150px, 34vw, 210px);
}

/* The running card band of section 4 of the landing page: same track, no snap,
   because a band is meant to drift rather than click into place. */
.mr-slider__track--band {
  scroll-snap-type: none;
}

/* THE FIVE DOORS. Decision of 08.09.: Grenzlauf, Lager, Karten, Arena and Shop are supposed to run
   large across the whole page and be swipeable, each one an entrance to its own page, instead of
   sitting small in the navigation next to the wordmark. So a door is a big target, not a link:
   the whole tile is the anchor, it is at least 56 px tall on the smallest phone, and the title
   carries the weight rather than an icon.

   Colour: page ink on the page surface, the accent only on the arrow and the top edge. The camp
   tiles taught this three times today (rarity, warning, empty slot), so it is not re-learned here:
   a coloured surface under text costs a measured pair per colour, an edge costs nothing. */
.mr-door {
  display: flex;
  flex-direction: column;
  gap: var(--mr-s2);
  min-height: 168px;
  padding: var(--mr-s5);
  border: 1px solid var(--mr-line);
  border-top: 3px solid var(--mr-blue);
  border-radius: var(--mr-r3);
  background: var(--mr-surface);
  color: var(--mr-ink);
  text-decoration: none;
  transition: border-color var(--mr-fast) var(--mr-ease), transform var(--mr-fast) var(--mr-ease);
}

.mr-door:hover {
  border-color: var(--mr-blue);
  transform: translateY(-2px);
}

.mr-door__title {
  font-size: clamp(1.35rem, 1.1rem + 1vw, 1.85rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.mr-door__line {
  color: var(--mr-ink-2);
  font-size: var(--mr-t-small);
  line-height: 1.5;
}

/* The "what is behind this" line, pushed to the bottom so five doors of different text length
   still line up at the foot. */
.mr-door__more {
  margin-top: auto;
  padding-top: var(--mr-s3);
  color: var(--mr-blue);
  font-size: var(--mr-t-cap);
  font-weight: 600;
  letter-spacing: 0.02em;
}

.mr-door__more::after {
  content: ' →';
}

/* Full width, edge to edge, because that is the whole point of the instruction. The track keeps
   its own inline padding so the first and last door are not glued to the screen edge. */
.mr-doors {
  --mr-slide-w: min(78%, 340px);
  padding-inline: max(var(--mr-gutter), calc((100vw - var(--mr-page)) / 2));
}

@media (min-width: 1100px) {
  /* Wide enough for all five at once: a slider that does not need sliding should not slide. */
  .mr-doors .mr-slider__track {
    grid-auto-flow: row;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    overflow-x: visible;
  }
}

.mr-slider__nav {
  display: flex;
  align-items: center;
  gap: var(--mr-s2);
  margin-top: var(--mr-s2);
}

.mr-slider__dots {
  display: flex;
  gap: var(--mr-s2);
  margin: 0 auto 0 0;
  padding: 0;
  list-style: none;
}

.mr-slider__dot {
  display: block;
  width: 30px;
  height: 30px; /* the visible dot is small, the target is not */
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: transparent;
}

.mr-slider__dot::before {
  content: '';
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--mr-line-strong);
  transition: background var(--mr-fast) var(--mr-ease), transform var(--mr-fast) var(--mr-ease);
}

.mr-slider__dot:hover::before {
  background: var(--mr-blue-bright);
}

.mr-slider__dot[aria-current='true']::before {
  background: var(--mr-blue);
  transform: scale(1.3);
}

.mr-slider__arrow {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  flex: none;
  border-radius: 50%;
  border: 1px solid var(--mr-line-strong);
  background: var(--mr-surface);
  color: var(--mr-ink);
  font-size: 1.125rem;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
}

.mr-slider__arrow:hover {
  border-color: var(--mr-blue);
  color: var(--mr-blue);
}

/* A slide. Nothing forces this shape, but the three-image explanation of the
   border walk uses it and so does the camp slider. */
.mr-slide {
  display: flex;
  flex-direction: column;
  gap: var(--mr-s3);
  background: var(--mr-surface);
  border: 1px solid var(--mr-line);
  border-radius: var(--mr-r3);
  padding: var(--mr-s5);
  box-shadow: var(--mr-shadow-1);
  scroll-margin-inline: var(--mr-s4);
}

.mr-slide__figure {
  aspect-ratio: 4 / 3;
  border-radius: var(--mr-r2);
  background: var(--mr-surface-2);
  border: 1px solid var(--mr-line);
  overflow: hidden;
}

.mr-slide__figure img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.mr-slide__step {
  font-family: var(--mr-font-num);
  font-size: var(--mr-t-cap);
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--mr-blue);
}

/* The camp slide takes its ground AND its ink from the camp, because one fixed ink makes four
   of the camps unreadable. See the --mr-camp-*-tile / -on block in section 2 for the numbers. */
.mr-slide--camp {
  background: linear-gradient(
    155deg,
    color-mix(in srgb, var(--_tile, var(--mr-blue)) 88%, #000),
    var(--_tile, var(--mr-blue))
  );
  border-color: transparent;
  color: var(--_on, #fff);
  text-decoration: none;
}

.mr-slide--camp h3,
.mr-slide--camp .mr-slide__step {
  color: inherit;
}

/* The dimmed body text keeps the tile's own ink and only loses a little weight, so it dims
   towards the ink it already has instead of towards white on a light camp. */
.mr-slide--camp p {
  color: inherit;
  opacity: 0.88;
}

/* ---------------------------------------------------------------------------
   10  Trade surfaces: shop, marketplace, arena
   --------------------------------------------------------------------------- */

/* A pack. The brief forbids the slot-machine look, so: no glow, no shimmer, no
   countdown. A pack is a product tile with an honest odds table under it. */
.mr-pack {
  display: flex;
  flex-direction: column;
  gap: var(--mr-s3);
  background: var(--mr-surface);
  border: 1px solid var(--mr-line);
  border-radius: var(--mr-r3);
  padding: var(--mr-s5);
  box-shadow: var(--mr-shadow-1);
}

.mr-pack__art {
  aspect-ratio: 3 / 4;
  border-radius: var(--mr-r2);
  background: linear-gradient(160deg, var(--mr-surface-2), var(--mr-surface));
  border: 1px solid var(--mr-line);
  display: grid;
  place-items: center;
  color: var(--mr-ink-3);
  font-family: var(--mr-font-alt);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-size: var(--mr-t-cap);
}

.mr-pack__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--mr-s2);
}

.mr-pack__count {
  font-size: var(--mr-t-small);
  color: var(--mr-ink-2);
}

.mr-pack__actions {
  display: grid;
  gap: var(--mr-s2);
  margin-top: auto;
}

/* One market row: offer, seller, price, action. A table on a wide screen, a
   stack of rows on a phone, from one rule set. */
.mr-offer {
  display: grid;
  gap: var(--mr-s3);
  grid-template-columns: 64px 1fr auto;
  align-items: center;
  padding: var(--mr-s3);
  border-bottom: 1px solid var(--mr-line);
  background: var(--mr-surface);
}

.mr-offer:first-child {
  border-top-left-radius: var(--mr-r3);
  border-top-right-radius: var(--mr-r3);
}

.mr-offer:last-child {
  border-bottom: 0;
  border-bottom-left-radius: var(--mr-r3);
  border-bottom-right-radius: var(--mr-r3);
}

.mr-offer__thumb {
  aspect-ratio: 744 / 1039;
  border-radius: var(--mr-r1);
  border: 1px solid var(--mr-line);
  background: var(--mr-surface-2);
  overflow: hidden;
}

.mr-offer__title {
  font-weight: 600;
  line-height: 1.3;
}

.mr-offer__sub {
  font-size: var(--mr-t-cap);
  color: var(--mr-ink-3);
}

.mr-offer__right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--mr-s2);
}

@media (max-width: 560px) {
  .mr-offer {
    grid-template-columns: 52px 1fr;
  }
  .mr-offer__right {
    grid-column: 1 / -1;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
}

/* The fee line. 3 to 5 percent on every trade is our cleanest income, so it is
   written openly next to the price rather than discovered after the sale. */
.mr-fee {
  font-size: var(--mr-t-cap);
  color: var(--mr-ink-3);
}

/* Arena board: what is running, who leads. */
.mr-board {
  width: 100%;
  border-collapse: collapse;
}

.mr-board th,
.mr-board td {
  padding: var(--mr-s3);
  text-align: left;
  border-bottom: 1px solid var(--mr-line);
}

.mr-board th {
  font-family: var(--mr-font-alt);
  font-size: var(--mr-t-cap);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--mr-ink-3);
  font-weight: 600;
}

.mr-board tbody tr:hover {
  background: var(--mr-blue-wash);
}

.mr-board__rank {
  font-family: var(--mr-font-num);
  font-variant-numeric: tabular-nums;
  color: var(--mr-ink-3);
  width: 3em;
}

/* Wide content scrolls inside its own box. The page body never scrolls sideways. */
.mr-scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* The grace period notice. A freshly taken territory is extraterritorial for 24
   to 48 hours, and the brief says that must be visible, not buried in a FAQ. */
.mr-grace {
  display: flex;
  align-items: flex-start;
  gap: var(--mr-s3);
  padding: var(--mr-s3) var(--mr-s4);
  border-radius: var(--mr-r2);
  border: 1px solid rgba(245, 166, 35, 0.42);
  background: var(--mr-amber-wash);
  font-size: var(--mr-t-small);
  color: #6b4705;
}

.mr-grace__clock {
  font-family: var(--mr-font-num);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   11  Forms
   --------------------------------------------------------------------------- */

.mr-field {
  display: block;
  margin-bottom: var(--mr-s4);
}

.mr-label {
  display: block;
  margin-bottom: var(--mr-s1);
  font-size: var(--mr-t-cap);
  font-weight: 600;
  color: var(--mr-ink-2);
}

.mr-input,
.mr-select {
  width: 100%;
  min-height: 44px;
  padding: var(--mr-s2) var(--mr-s3);
  border: 1px solid var(--mr-line-strong);
  border-radius: var(--mr-r2);
  background: var(--mr-surface);
  color: var(--mr-ink);
  font-family: inherit;
  font-size: 1rem; /* never below 16 px: iOS zooms the page on focus otherwise */
}

.mr-input:focus,
.mr-select:focus {
  border-color: var(--mr-blue);
}

.mr-hint {
  margin-top: var(--mr-s1);
  font-size: var(--mr-t-cap);
  color: var(--mr-ink-3);
}

.mr-error {
  margin-top: var(--mr-s1);
  font-size: var(--mr-t-cap);
  color: var(--mr-bad);
}

/* The country switch. It sits in the shop from the first day, because real
   money random packs are forbidden in some EU countries and the switch decides
   what a visitor is even shown. */
.mr-country {
  display: inline-flex;
  align-items: center;
  gap: var(--mr-s2);
  font-size: var(--mr-t-small);
  color: var(--mr-ink-2);
}

.mr-country .mr-select {
  width: auto;
  min-height: 36px;
  padding-block: var(--mr-s1);
}

/* Segmented control: the four modes of the game table, the shop's currency
   tabs. Radio inputs underneath, so it works with the keyboard and with JS off. */
.mr-seg {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  border-radius: var(--mr-r-pill);
  background: var(--mr-surface-2);
  border: 1px solid var(--mr-line);
}

.mr-seg__opt {
  padding: var(--mr-s2) var(--mr-s4);
  border-radius: var(--mr-r-pill);
  font-size: var(--mr-t-small);
  font-weight: 600;
  color: var(--mr-ink-2);
  cursor: pointer;
  white-space: nowrap;
}

.mr-seg__opt:hover {
  color: var(--mr-ink);
}

.mr-seg input:checked + .mr-seg__opt,
.mr-seg__opt[aria-selected='true'] {
  background: var(--mr-surface);
  color: var(--mr-blue-deep);
  box-shadow: var(--mr-shadow-1);
}

/* ---------------------------------------------------------------------------
   12  Utilities. Deliberately few: a design system with 300 helpers is a second
   framework nobody agreed to.
   --------------------------------------------------------------------------- */

/* Flex column with a gap, not `> * + * { margin-top }`.
   The margin version silently does nothing to inline children, and the first real use of it was a
   footer column of links, which are inline: the three links came out as "KarteSpieltischSammlung"
   on one line. A flex container blockifies its children, so a stack stacks whatever you put in it,
   which is the only thing a utility called stack is for. */
.mr-stack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--mr-s4);
}
.mr-stack--tight {
  gap: var(--mr-s2);
}
.mr-stack--loose {
  gap: var(--mr-s6);
}

.mr-row {
  display: flex;
  align-items: center;
  gap: var(--mr-s3);
  flex-wrap: wrap;
}

.mr-row--end {
  justify-content: flex-end;
}
.mr-row--between {
  justify-content: space-between;
}

.mr-muted {
  color: var(--mr-ink-2);
}
.mr-dim {
  color: var(--mr-ink-3);
}
.mr-num {
  font-family: var(--mr-font-num);
  font-variant-numeric: tabular-nums;
}
.mr-center {
  text-align: center;
}
.mr-nowrap {
  white-space: nowrap;
}
.mr-hide {
  display: none !important;
}

/* Hidden because the page runs inside the app's WebView. The brief: the sandbox
   gap view shows the shop route on the web and must not show it in the app.
   The WebView sets data-embed="app" on <html>, and every shop link marked with
   this class disappears. One switch, one class, no second build. */
[data-embed='app'] .mr-web-only {
  display: none !important;
}

@media print {
  .mr-nav,
  .mr-foot,
  .mr-slider__nav {
    display: none !important;
  }
  .mr {
    background: #fff;
  }
}
