/* =============================================================================
   base.css - Layout, design tokens and shared components.

   Reference: `.docs/steps/1.md` point 2 and `.docs/steps/7.md` point 4 (mobile polish).

   Rules enforced by this file:
     - no hard-coded color: only `var(--color-*)`, `var(--glow-*)`, `var(--shadow-*)` and
       `var(--overlay)` declared in `css/themes.css`;
     - mobile first: base rules target small screens, `@media (min-width: 768px)` refines the
       desktop layout;
     - no horizontal scrolling: `overflow-x: hidden` plus fluid typography (`clamp()`);
     - touch targets are at least 44px high (`--touch-target`);
     - animations use `transform`/`opacity` only and honour `prefers-reduced-motion`.
   ============================================================================= */

/* --- Reset ---------------------------------------------------------------- */

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

html,
body {
  margin: 0;
  padding: 0;
  max-width: 100%;
}

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

/* Horizontal overflow is clipped without turning <body> into a scroll container: an
   `overflow-x: hidden` on <body> is known to break `position: sticky` (the application
   header) on mobile. Browsers without `overflow: clip` fall back to clipping <html>. */
body {
  overflow-x: clip;
}

@supports not (overflow: clip) {
  html {
    overflow-x: hidden;
  }
}

body {
  min-height: 100vh;
  min-height: 100dvh;
  overscroll-behavior: none;
}

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

/* --- Scrollbars ------------------------------------------------------------ */

/* The native bar is hidden everywhere and painted back by `ScrollHint` (`js/utils/ScrollHint.js`)
   while the content moves: the 15px grey column of Windows next to a themed panel reads as a browser
   widget that escaped its window, and the floating pill of macOS covers the edge of the dialog.
   Only the indicator goes away. The wheel, the trackpad, the finger, the keyboard and `Home`/`End`
   keep scrolling exactly as before. `scrollbar-width` is the Firefox spelling, `::-webkit-scrollbar`
   the Chromium and Safari one; a browser knows one or the other, so both are needed. */
html:not([data-native-scrollbars="true"]),
html:not([data-native-scrollbars="true"]) * {
  scrollbar-width: none;
}

/* The `html` case is spelled out on its own: the universal selector of a descendant combinator only
   matches DESCENDANTS, so `html … *` would leave the page's own bar in place, which is exactly the
   one that is on screen the most. */
html:not([data-native-scrollbars="true"])::-webkit-scrollbar,
html:not([data-native-scrollbars="true"]) *::-webkit-scrollbar {
  width: 0;
  height: 0;
}

h1,
h2,
h3,
p {
  margin: 0;
}

button {
  font: inherit;
  color: inherit;
}

[hidden] {
  display: none !important;
}

/* Keeps a label available to a screen reader while removing it from the screen: the header counts
   its boosters and its picks with icons only, and the text of each one must not be lost. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --- Design tokens -------------------------------------------------------- */

:root {
  /* Spacing scale (mobile first) */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;

  /* Radii */
  --radius-sm: 6px;
  --radius-md: 12px;
  --radius-lg: 20px;
  --radius-pill: 999px;

  /* Typography */
  --font-family-base: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  /* The pixel face of a player TITLE (see `css/fonts.css`). It is declared with the monospace
     fallback the character grid of a pixel font needs: if the two subsets are unavailable, a title
     keeps its alignment instead of reflowing into a proportional face. */
  --font-family-title: "Press Start 2P", "Courier New", monospace;
  --font-size-xs: 0.75rem;
  --font-size-sm: 0.875rem;
  --font-size-md: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-xl: clamp(1.25rem, 1.1rem + 1vw, 1.5rem);
  --font-size-2xl: clamp(1.5rem, 1.2rem + 2vw, 2rem);
  --line-height-base: 1.5;

  /* Layout */
  --app-max-width: 1100px;
  --header-height: 3.5rem;
  --nav-height: 4rem;
  --touch-target: 44px;

  /* The card ratio, in one place. Every illustration of the game is a 720x1000 PNG, whatever its
     rarity or its printing, so this single ratio is what EVERY box displaying a card takes
     (`.collection__image`, `.card-reveal__card`, `.stats__history-card`, `.trophy-card__image`, the
     tiles of the overlay, and the foil window of `css/components/card-shine.css`). A box keeping an
     approximate ratio would letterbox the artwork and the foil would then be painted outside it. */
  --card-ratio: 720 / 1000;

  /* Motion */
  --duration-fast: 120ms;
  --duration-base: 220ms;
  --easing-standard: cubic-bezier(0.2, 0, 0, 1);

  /* Layering */
  --z-header: 20;
  --z-nav: 30;
  --z-overlay: 40;
  /* Above every dialog: the indicator of `ScrollHint` follows the box that scrolls, and the
     dialog's own body is one of them. */
  --z-scroll-hint: 50;
}

/* --- App shell ------------------------------------------------------------ */

body {
  display: flex;
  flex-direction: column;
  font-family: var(--font-family-base);
  font-size: var(--font-size-md);
  line-height: var(--line-height-base);
  color: var(--color-text);
  background-color: var(--color-bg);
}

.app-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  min-height: var(--header-height);
  padding: var(--space-2) var(--space-4);
  padding-top: calc(var(--space-2) + env(safe-area-inset-top, 0px));
  /* The identity of the header is anchored to the left edge of the bar (it hangs a few pixels below
     it, see `.app-header__identity`), so the brand and the counters start after it. */
  padding-left: calc(var(--space-4) + 3.75rem);
  background-color: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  box-shadow: var(--shadow-sm);
}

.app-header__brand {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
}

.app-header__title {
  font-size: var(--font-size-lg);
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* The pseudo of the player, in front of the name of the game (`"<pseudo> - MineCards"`). It shares
   the header line with the tagline and the counters, so a long name is cut instead of pushing them:
   the title of the game stays readable, which is the one thing this corner is for. The separator is
   drawn here rather than written in the markup, so a header without a pseudo (no account yet) does
   not start with a dangling dash. */
.app-header__player {
  display: inline-block;
  max-width: 12rem;
  vertical-align: bottom;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-header__player[hidden] {
  display: none;
}

.app-header__player:not([hidden]) + .app-header__brand-name::before {
  content: " - ";
}

.app-header__tagline {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Identity of the header: the avatar of the player, the level on its corner, and that level's
   progress as a ring around it. It is a BUTTON (it opens the profile), so it has to lose the box a
   button normally carries: the picture alone is the target. */
.app-header__identity {
  /* Pinned to the right edge of the bar rather than laid out in the row of counters: it is taller
     than a chip, and a flex item would either stretch the bar (which then just gets taller) or
     stay inside it. Anchored here it sits on the same line as the counters and hangs a few pixels
     BELOW the bar, over the top of the page - the slot the counters reserve for it is
     `padding-right` on `.app-header__stats`. */
  position: absolute;
  left: var(--space-4);
  /* A few pixels lower than the bar, no more: on a phone the top of the picture then lands on the
     top of the counters, and on a wide screen it is a couple of pixels under it - which is what
     "slightly out of the bar" means. The ring adds its own three pixels. */
  bottom: -0.25rem;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: none;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-appearance: none;
  appearance: none;
}

/* The circle the picture sits in. `--level-progress` is the percentage of the CURRENT level, written
   by `App.refreshHeaderIdentity()`; the ring reads it below. */
.app-header__avatar {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  background-color: var(--color-surface-alt);
  --level-progress: 0;
}

/* The progress ring, painted BEHIND the picture: a full circle of the track colour and, on top of
   it, the filled share of the level. It is a `conic-gradient` cut into a band by a radial mask, so
   the run starts at twelve o'clock and its end is visible as a hard edge at any value - which a
   border or an `outline` could never show. The band sits just outside the picture (the inset), so
   the artwork is never covered, and the colours come from the palette: the ring follows the theme
   like every other accent of the game. */
.app-header__avatar::before {
  content: "";
  position: absolute;
  inset: -0.1875rem;
  border-radius: 50%;
  background: conic-gradient(
    var(--color-accent) calc(var(--level-progress, 0) * 1%),
    var(--color-border) 0
  );
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 0.1875rem), #000 calc(100% - 0.1875rem));
  mask: radial-gradient(farthest-side, transparent calc(100% - 0.1875rem), #000 calc(100% - 0.1875rem));
}

.app-header__avatar-image {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  /* The icons are pixel art (several ship as 6x6 or 8x8): a smooth upscale would turn them into a
     blur, so the browser repeats the pixels instead (same rule as the profile avatar). */
  image-rendering: pixelated;
}

.app-header__avatar-initial {
  font-size: var(--font-size-sm);
  font-weight: 700;
  color: var(--color-accent);
}

/* The level, written on the lower right corner of the picture: it is the one number of the identity
   and it must stay readable on any artwork, so it carries its own pill and a ring of the surface
   colour that separates it from the avatar. */
.app-header__avatar-level {
  position: absolute;
  /* Barely off the picture: the circle already overhangs the bar, and a badge hanging further out
     would read as a floating pill instead of a corner. */
  right: -0.1rem;
  bottom: -0.1rem;
  z-index: 1;
  min-width: 1.05rem;
  padding: 0 0.15rem;
  border-radius: var(--radius-pill);
  background-color: var(--color-accent);
  color: var(--color-accent-contrast);
  font-size: 0.625rem;
  font-weight: 700;
  line-height: 1.15;
  text-align: center;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 0 0 2px var(--color-surface);
}

/* The router marks the button like any other destination of the bar, so "you are on the profile"
   reads on the avatar too: the halo is the same one the keyboard focus uses. */
.app-header__identity.is-active .app-header__avatar,
.app-header__identity:hover .app-header__avatar,
.app-header__identity:focus-visible .app-header__avatar {
  box-shadow: var(--glow-accent);
}

.app-header__stats {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
  flex-wrap: wrap;
}

.app-header__stat {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  min-height: calc(var(--touch-target) - 12px);
  background-color: var(--color-surface-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

.app-header__stat-value {
  font-size: var(--font-size-sm);
  font-weight: 700;
  color: var(--color-text);
}

/* The bonus counter is a button: it opens the per-extension inventory dialog.

   `font-family` alone, never the `font: inherit` shorthand: the shorthand resets `font-size` too, and
   that is what made the two CLICKABLE counters (bonus and binders) render bigger than the `<span>`
   ones - their label at the body size instead of `--font-size-xs`, on a box three pixels taller.
   A `<button>` does not inherit the typography of its container the way a `<span>` does, so the
   family always comes from here (and from the `button` rule of this file). */
.app-header__stat--action {
  font-family: inherit;
  color: inherit;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.app-header__stat--action:hover,
.app-header__stat--action:focus-visible {
  border-color: var(--color-accent);
  color: var(--color-text);
}

/* The one counter of the header that carries no number (the daily rewards): it keeps the height and
   the shape of the others but takes the width of its icon alone, so the five read numbers keep the
   row to themselves. */
.app-header__stat.app-header__stat--icon {
  position: relative;
  flex: 0 0 auto;
  padding: var(--space-1) var(--space-2);
}

/* "Something is waiting for you today": a dot on the corner of the button, never a number (the
   chapter has one reward a day, so a counter would always say "1"). */
.app-header__stat-dot {
  position: absolute;
  top: 0.2rem;
  right: 0.2rem;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background-color: var(--color-warning);
  box-shadow: 0 0 0 1px var(--color-surface), 0 0 8px var(--color-warning);
  animation: header-dot-pulse 2s var(--easing-standard) infinite;
}

@keyframes header-dot-pulse {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.25);
  }
}

/* The counters of the header are ICONS only (each label is `.sr-only`): the icon, then the value.
   The size is in `rem`, not in `em`: two of the five counters are `<button>`s, whose typography the
   browser does not inherit from the container the way a `<span>` does, so an `em` size would make
   those two icons a pixel bigger than the other three. */
.app-header__stat-icon {
  flex: 0 0 auto;
  width: 1.05rem;
  height: 1.05rem;
  object-fit: contain;
}

/* One counter (the Mine Pick picks) draws its icon INLINE, with the very same sparkle as the
   bottom bar: it then follows the text colour like `.app-nav__item svg`, instead of carrying the
   baked colours of a PNG. */
svg.app-header__stat-icon {
  fill: none;
  /* The other counters of the header are drawn glyphs; the Mine Pick sparkle and the gift follow
     them, so the row reads as one set of icons instead of mixing a drawing with a tinted one.
     `--icon-ink` is what the palette says that ink is: black on the three light themes, white on
     the two dark ones, where a black stroke would simply disappear (`css/themes.css`). */
  color: var(--icon-ink);
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* A wide screen shows the NAME of each counter next to its icon: four icons and four numbers ask the
   player to memorise a legend, and there is room for a word once the header leaves the phone layout.
   Only a short label appears ("Quotidiens", "Pioches"): the whole label lives in the `.sr-only`
   span above, which stays the accessible name - this one is `aria-hidden`, so a screen reader never
   hears a truncated version of it.
   The breakpoint is the one where the counters and the brand stop fitting on the same row: the
   tagline (pure decoration, already hidden on a phone) then steps aside instead of wrapping the
   header onto a second line. */
.app-header__stat-label {
  display: none;
  color: var(--color-text-muted);
  /* Spelled out rather than inherited: the label is the one part of a counter whose size can be
     stolen by the element around it (a `<button>` resets the font, see `.app-header__stat--action`). */
  font-size: var(--font-size-xs);
}

@media (min-width: 1024px) {
  .app-header__stat-label {
    display: inline;
  }

  .app-header__tagline {
    display: none;
  }

  /* Every element of the header row, the counters and the clock alike, is the SAME box: one row of
     a grid whose columns are all `1fr` takes the width of the WIDEST of them - the longest label of
     the current language, or the clock of the moment - and stretches them to one height. A flex row
     cannot: it sizes each chip to its own content, which is why "Quotidiens 3" used to be half as
     wide again as "Bonus 0", and the clock, which counts nothing at all, was the narrowest of all.
     The row keeps its place in the header line: pushed to the right of the brand, on one line. */
  .app-header__stats {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    align-items: stretch;
  }
}


/* Bonus inventory dialog (opened from the header counter). */
.bonus-inventory__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.bonus-inventory__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  background-color: var(--color-surface-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.bonus-inventory__name {
  color: var(--color-text);
}

.bonus-inventory__count {
  font-weight: 700;
  color: var(--color-text);
}

.bonus-inventory__total {
  margin: var(--space-3) 0 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  text-align: center;
}

.bonus-inventory__empty {
  margin: 0;
  color: var(--color-text-muted);
  text-align: center;
}

.app-header__clock {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  min-height: calc(var(--touch-target) - 12px);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

body[data-connection="offline"] .app-header__clock {
  color: var(--color-warning);
  border-style: solid;
}

/* Mobile header: only the brand and the counters are kept. The tagline and the UTC clock are
   dropped (the clock is still readable in the settings, where it is not on the way), the title
   sits on its own centered row and is one step smaller, which leaves the counters the whole width
   of the header. */
@media (max-width: 767px) {
  /* A phone shows no brand at all: the name of the game is in the browser tab and in the installed
     app, and the bar is worth more to the counters and to the identity than to a title repeated on
     every screen. */
  .app-header__brand {
    display: none;
  }

  /* Two counters step aside on a phone so the row holds the avatar as well: the binders are one tap
     away in the collection, and the Mine Pick picks are on the Mine page. The numbers are not lost,
     they are simply not competing with the identity for the width of the bar. */
  .app-header__stat--desktop-only {
    display: none;
  }

  .app-header {
    padding-left: calc(var(--space-4) + 3.5rem);
  }

  .app-header__avatar {
    width: 2.75rem;
    height: 2.75rem;
  }

  .app-header__avatar-level {
    font-size: 0.5625rem;
  }

  .app-header__tagline,
  .app-header__clock {
    display: none;
  }

  /* The five counters of a phone, ALL ON ONE ROW under the title: three chips on the first row and
     two on the second used to eat the whole first screenful, and the header of the game is read at
     a glance or not at all. The clock is hidden here, so the row holds the counters only.

     Each chip is a fifth of the width (`flex: 1 1 0`, never a fixed `calc()`: the five share exactly
     what is left, whatever the padding of the header), and `nowrap` forbids the row from ever
     splitting again at an in-between width. The icon and the value shrink with the chip, and the
     value is capped at "9999+" by `App.setCount` - five glyphs is what a fifth of a 360 px phone
     can hold. */
  .app-header__stats {
    width: 100%;
    margin-left: 0;
    justify-content: center;
    flex-wrap: nowrap;
    gap: var(--space-1);
  }

  .app-header__stat {
    flex: 1 1 0;
    min-width: 0;
    padding: var(--space-1);
    gap: var(--space-1);
    /* A number that could not fit is clipped inside its pill rather than pushed over the next one.
       It only ever happens with five digits, which `setCount` already caps. */
    overflow: hidden;
  }

  .app-header__stat-icon {
    width: 0.9rem;
    height: 0.9rem;
  }

  .app-header__stat-value {
    font-size: var(--font-size-xs);
  }
}

.app-main {
  flex: 1 1 auto;
  width: 100%;
  max-width: var(--app-max-width);
  margin: 0 auto;
  padding: var(--space-4);
  padding-bottom: calc(var(--nav-height) + var(--space-5) + env(safe-area-inset-bottom, 0px));
}

.app-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: var(--z-nav);
  display: grid;
  /* One column per button, whatever their number: the bottom bar must never wrap onto a second row
     (the info page added a 7th destination). On a wide screen the columns share the row equally,
     which is what gives the bar its regular rhythm; on a phone they are sized by their own label
     instead, see the `480px` block below. */
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  padding-bottom: calc(var(--space-1) + env(safe-area-inset-bottom, 0px));
  background-color: var(--color-surface);
  border-top: 1px solid var(--color-border);
  box-shadow: var(--shadow-md);
}

/* --- Onboarding: the app chrome is hidden -------------------------------- */

body.is-onboarding .app-header,
body.is-onboarding .app-nav {
  display: none;
}

body.is-onboarding .app-main {
  padding-bottom: var(--space-5);
}

/* --- Focus mode: the RITUAL screens hide the chrome too ------------------- */

/* `js/utils/FocusMode.js` turns this on while a booster is opened (from the moment the pack is
   drawn) and while the card of a Mine Pick pack is chosen: the brand, the counters, the bottom bar
   and the "Comment ça marche ?" pill all step aside, so the only things left on the screen are the
   pack and its cards. The title of the view goes with them - "Ouverture" says nothing about what is
   happening in the scene, and on a phone every line of height is a card that fits.

   The same rules as `is-onboarding` (which hides the same two blocks), extended to the two elements
   that belong to the VIEW rather than to the shell: a focus screen is one where a page header and a
   help pill would be read by a player who is in the middle of a gesture. */
body.is-focus .app-header,
body.is-focus .app-nav,
body.is-focus .view-help,
body.is-focus .view__header {
  display: none;
}

/* The bottom padding of the main area exists to clear the fixed bar: with the bar gone, it would be
   dead space under the ritual. The safe area is kept, a phone with a notch still has one. */
body.is-focus .app-main {
  padding-top: var(--space-4);
  padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
}

/* The scene of the opening sits in the MIDDLE of the screen, both ways: with the header and the
   bottom bar gone, a pack pinned to the top of the page left the bottom of the screen empty, which
   is the opposite of focus.

   `.app-main` already takes the whole height of the window (it is the flexible child of the
   `body` column), so the view only has to take that height too and centre its own column. The
   height is written as `100dvh` minus the two paddings above, exactly like the two other full
   height blocks of the stylesheet.

   `safe center` is what makes it hold on a phone: a scene taller than the window - a spotlight card
   and its two buttons, once the browser bar is counted - must keep its top REACHABLE rather than
   overflow above the fold with no way to scroll back to it. A browser that does not know `safe`
   drops the declaration and keeps the initial `flex-start`, which is the safe behaviour by
   definition. Only the vertical axis is set: the horizontal centring of the scene is already the
   business of its own children (`.booster-3d` is `90%` wide with `margin-inline: auto`, the
   spotlight also centres its column, and the summary is a grid of equal columns).

   Scoped to the OPENING view: the MinePick stage keeps its panel hanging from the top of the screen,
   where its list of cards and its result line are read in the order they are written. */
body.is-focus [data-view="opening"].is-active {
  display: flex;
  flex-direction: column;
  min-height: calc(100dvh - (2 * var(--space-4)) - env(safe-area-inset-bottom, 0px));
  justify-content: safe center;
}

/* Everything that was placed relative to the bottom bar follows it out of the screen - the toast
   stack, which would otherwise hover 4.5rem above the bottom of an empty page. The notification
   banner needs nothing: `NotificationCenter` measures the bottom of the header to place it, and a
   hidden header measures zero. */
body.is-focus {
  --nav-height: 0px;
}


/* --- Bottom navigation (thumb friendly) ---------------------------------- */

.app-nav__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: var(--touch-target);
  padding: var(--space-1);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background-color: transparent;
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  font-weight: 600;
  line-height: 1.1;
  cursor: pointer;
  touch-action: manipulation;
  transition: color var(--duration-fast) var(--easing-standard),
    background-color var(--duration-fast) var(--easing-standard);
}

.app-nav__item span {
  min-width: 0;
  max-width: 100%;
  text-align: center;
  /* A label is one word of the bar, never two: it is allowed to be tight, not to be cut in the
     middle. The bar is wide enough for the longest one (see below), and `nowrap` keeps a translation
     that grew from breaking the row instead of slipping under its neighbour. */
  overflow-wrap: normal;
  white-space: nowrap;
}

/* PHONE: seven destinations in ~360 px. The equal split of the wide screen gives each column 45 px,
   which is less than the 50 px of "Collection" - and a label wider than its column is not clipped by
   default, it simply overflows and lands on the label next to it. So on a phone the columns are sized
   by their CONTENT and the leftover space is spread around them: "Collection" takes the 50 px it
   needs, "Pick" the 20 px, and every label stays whole. The columns keep the thumb-sized height of
   `.app-nav__item`, and the gaps between them are the usual 4 px plus the share of `space-evenly`.
   The same rule names the breakpoint: the sum of the seven labels at `--font-size-xs` is ~220 px, so
   from 480 px down there is no reason to squeeze them into equal columns. */
@media (max-width: 480px) {
  .app-nav {
    grid-auto-columns: max-content;
    justify-content: space-evenly;
  }

  .app-nav__item {
    font-size: 0.6875rem;
  }
}

/* Very narrow (a 320 px phone, the outer screen of a foldable): the seven labels in a row need ~322
   px, and there is no smaller type that stays readable - so the labels step aside and the icons keep
   the bar. The text is not removed: it is clipped the way `.sr-only` does it, so a screen reader still
   announces "Collection" and still hears it as the name of the button. Tap targets are back to an
   equal share of the row. */
@media (max-width: 339px) {
  .app-nav {
    grid-auto-columns: minmax(0, 1fr);
    justify-content: normal;
  }

  .app-nav__item {
    position: relative;
  }

  /* Every label of the bar, but not the badge of the settings: that one is a dot, not a word, and
     clipping it would hide the only sign that a version is waiting. */
  .app-nav__item span:not(.app-nav__badge) {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* "A version you have not read is waiting in the settings": the same dot as the daily reward one of
   the header, on the corner of the icon. Positioned against the button, so it follows the bar's
   padding whatever the width of the label is. */
.app-nav__item {
  position: relative;
}

.app-nav__badge {
  position: absolute;
  top: 0.25rem;
  /* The icon is 22 px wide and centred in the button: its upper right corner is 11 px right of the
     middle, which is where the dot is pinned - whatever the width of the label under it. */
  right: calc(50% - 0.7rem);
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  background-color: var(--color-accent);
  box-shadow: 0 0 0 2px var(--color-surface);
}

.app-nav__item svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.app-nav__item.is-active {
  color: var(--color-accent);
  background-color: var(--color-surface-alt);
  border-color: var(--color-border);
}

/* The main action ("Ouvrir"): a primary button in the middle of the bar, lifted above its
   neighbours so the one thing the app is for reads as the obvious target. */
.app-nav__item--primary {
  background-color: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-accent-contrast);
  transform: translateY(-6px);
  box-shadow: var(--shadow-md);
}

.app-nav__item--primary:hover,
.app-nav__item--primary.is-active {
  background-color: var(--color-accent-hover);
  border-color: var(--color-accent-hover);
  color: var(--color-accent-contrast);
}

/* --- Views --------------------------------------------------------------- */

.view {
  display: none;
}

.view.is-active {
  display: block;
  animation: view-enter var(--duration-base) var(--easing-standard);
}

@keyframes view-enter {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.view__header {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-bottom: var(--space-4);
}

/* The heading of a view: the title and its subtitle as one block, so a view header that carries an
   action of its own (the binders button of the collection) can push that block and the action apart
   without the two lines of text separating. */
.view__heading {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

/* A view header with an action at its reading end. The action is pinned to the TOP of the header
   (`flex-start`), which is what puts it "at the top right of the page" whatever the height of the
   subtitle next to it. */
.view__header--split {
  flex-direction: row;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

.view__title {
  font-size: var(--font-size-xl);
  font-weight: 700;
}

.view__subtitle {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* The actions of a view header (the statistics button of the profile, the back button of the
   statistics page): a discreet row under the subtitle. */
.view__header-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* --- "How does it work?" popup (`js/UI/components/ViewHelp.js`) ---------- */

/* One pill under the header of every view, and the small panel it opens: two sentences on what the
   page does, then the way to the information page. The pill is a dashed outline, quieter than a
   real action - it explains the page, it does not do anything to it - and it sits at the reading
   start of the header, where the eye already is when it has just read the title. */
.view-help {
  position: relative;
  align-self: flex-start;
  margin-top: var(--space-2);
}

.view-help__button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 2rem;
  padding: 0 var(--space-3);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-pill);
  background-color: transparent;
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  font-weight: 600;
  cursor: pointer;
  touch-action: manipulation;
  transition: border-color var(--duration-fast) var(--easing-standard),
    color var(--duration-fast) var(--easing-standard);
}

.view-help__button:hover,
.view-help__button[aria-expanded="true"] {
  border-style: solid;
  border-color: var(--color-accent);
  color: var(--color-text);
}

.view-help__button:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* The icon is drawn, not loaded: it follows the colour of the pill like a glyph would. */
.view-help__icon {
  flex: 0 0 auto;
  width: 0.95rem;
  height: 0.95rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
}

/* The panel floats over the view instead of pushing it down: the explanation is read once, the
   layout of the page must not jump when it opens. Widths are capped in `rem` AND against the
   viewport, so the same block fits a phone without ever overflowing it. */
.view-help__panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  z-index: var(--z-header);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  width: min(22rem, calc(100vw - (2 * var(--space-4) + 2px)));
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
  box-shadow: var(--shadow-md);
}

.view-help__title {
  font-size: var(--font-size-sm);
  font-weight: 700;
}

.view-help__text {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

.view-help__more {
  margin-top: var(--space-1);
}

/* The icon of a `.btn` (an inline SVG path, never a picture). */
.btn__icon {
  flex: 0 0 auto;
  width: 1.15em;
  height: 1.15em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* --- Switch --------------------------------------------------------------- */

/* A real switch (`role="switch"` on a `<button>`, `aria-checked` carrying the state): the
   track/thumb pair shows it, the adjacent text is written by the view so it always matches the
   active language. Used by the sound preference of the settings and by the "Effets" switch of the
   card details (`js/UI/components/CardEffectsToggle.js`, where the row itself carries `.switch`),
   which is why it lives here and not in a view stylesheet: a control shared by several screens
   belongs to the shared layer. The host is free to place and size it
   (`.settings__panel .switch`, `.card-effects`), never to redraw the track or the thumb. */
.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0;
  border: none;
  background: none;
  color: var(--color-text);
  font: inherit;
  cursor: pointer;
}

.switch:disabled {
  opacity: 0.6;
  cursor: default;
}

.switch__track {
  position: relative;
  flex: 0 0 auto;
  width: 2.75rem;
  height: 1.5rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background-color: var(--color-surface-alt);
  transition: background-color 160ms ease, border-color 160ms ease;
}

.switch__thumb {
  position: absolute;
  top: 50%;
  left: 0.2rem;
  width: 1.05rem;
  height: 1.05rem;
  border-radius: 50%;
  background-color: var(--color-text-muted);
  transform: translateY(-50%);
  transition: left 160ms ease, background-color 160ms ease;
}

.switch[aria-checked="true"] .switch__track {
  border-color: var(--color-accent);
  background-color: var(--color-accent);
}

.switch[aria-checked="true"] .switch__thumb {
  left: calc(100% - 1.25rem);
  background-color: var(--color-accent-contrast);
}

/* The caption of the switch is written by the view (`SettingsView`, `CollectionView`), never in the
   markup. It is muted while the switch is off and plain while it is on: someone who cannot read the
   position of the thumb still gets the state from the colour of the word. */
.switch__state {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  white-space: nowrap;
}

.switch[aria-checked="true"] .switch__state {
  color: var(--color-text);
}

.switch:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
  border-radius: var(--radius-md);
}

@media (prefers-reduced-motion: reduce) {
  .switch__track,
  .switch__thumb {
    transition: none;
  }
}

/* --- Panels and placeholders -------------------------------------------- */

.panel {
  padding: var(--space-4);
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

.placeholder {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--touch-target);
  padding: var(--space-2) var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background-color: var(--color-surface-alt);
  color: var(--color-text);
  font-size: var(--font-size-md);
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  touch-action: manipulation;
  transition: background-color var(--duration-fast) var(--easing-standard),
    border-color var(--duration-fast) var(--easing-standard),
    transform var(--duration-fast) var(--easing-standard);
}

.btn:active:not(:disabled) {
  transform: scale(0.98);
}

.btn--primary {
  background-color: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-accent-contrast);
}

.btn--primary:hover:not(:disabled) {
  background-color: var(--color-accent-hover);
  border-color: var(--color-accent-hover);
}

.btn--ghost {
  background-color: transparent;
  border-color: var(--color-border);
  color: var(--color-text);
}

.btn--ghost:hover:not(:disabled) {
  background-color: var(--color-surface-alt);
  border-color: var(--color-accent);
}

.btn--danger {
  background-color: transparent;
  border-color: var(--color-error);
  color: var(--color-error);
}

.btn--danger:hover:not(:disabled) {
  border-color: var(--color-accent);
}

.btn--block {
  width: 100%;
}

/* A small action keeps a real touch target but reads as secondary to the page it sits in. */
.btn--small {
  min-height: 2.25rem;
  padding: 0 var(--space-3);
  font-size: var(--font-size-xs);
}

.btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.btn:focus-visible,
.app-nav__item:focus-visible,
.lang-switch__option:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* --- Inline feedback ----------------------------------------------------- */

.feedback {
  margin-top: var(--space-3);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

.feedback[data-tone="error"] {
  color: var(--color-error);
}

/* --- Boot overlay and fatal error --------------------------------------- */

.app-boot {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: grid;
  place-items: center;
  background-color: var(--color-bg);
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}

.app-fatal {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 40rem;
  margin: var(--space-5) auto;
  padding: var(--space-4);
  background-color: var(--color-surface);
  border: 1px solid var(--color-error);
  border-radius: var(--radius-md);
}

.app-fatal h2 {
  font-size: var(--font-size-lg);
  color: var(--color-error);
}

.app-fatal pre {
  margin: 0;
  padding: var(--space-2);
  background-color: var(--color-surface-alt);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  white-space: pre-wrap;
  word-break: break-word;
}

/* The panel used to be a dead end: the buttons give the player the one fix that works (a clean
   reload) and the risky way out (a partial game rather than none). */
.app-fatal__hint {
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

.app-fatal__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: flex-end;
}

/* --- Mobile polish (step 7 point 4) -------------------------------------- */

/* Applied globally instead of per component, so every present and future control inherits the
   same touch behaviour:
     - `touch-action: manipulation` removes the ~300ms double-tap-zoom delay of mobile browsers
       (the components that need a different gesture, such as the 3D booster or the swipeable
       card stage, keep their own `touch-action` declaration);
     - `-webkit-tap-highlight-color: transparent` removes the grey flash some mobile browsers
       paint over a tapped control, which is unreadable on the dark and classic themes. */
button,
a,
input,
select,
textarea,
summary,
[role="button"] {
  touch-action: manipulation;
}

button,
a,
[role="button"] {
  -webkit-tap-highlight-color: transparent;
}

/* --- Desktop refinements ------------------------------------------------- */

@media (min-width: 768px) {
  :root {
    --header-height: 4rem;
    --nav-height: 4.5rem;
    --font-size-md: 1.0625rem;
  }

  .app-main {
    padding: var(--space-6);
    padding-bottom: calc(var(--nav-height) + var(--space-6));
  }

  .app-nav__item {
    font-size: var(--font-size-sm);
    flex-direction: row;
    gap: var(--space-2);
  }
}

@media (min-width: 1024px) {
  .app-nav {
    max-width: var(--app-max-width);
    margin: 0 auto;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    border-left: 1px solid var(--color-border);
    border-right: 1px solid var(--color-border);
  }
}

/* --- Card detail: drag-to-tilt ------------------------------------------ */

/* A card shown in a detail surface (collection, profile favourites, trader offer, opening reveal)
   can be dragged with the mouse or a finger. `CardTilt` caps the rotation at
   `cardDetail.maxRotationDeg` and sets `--card-tilt-thickness` from the configuration; the
   stacked shadows below turn that value into a slight visible edge, so the card reads as an
   object instead of a flat picture. */
.card-tilt {
  --card-tilt-thickness: 6px;
  border-radius: var(--radius-md);
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
  transform-origin: center center;
  transform-style: preserve-3d;
  transition: transform 0.28s cubic-bezier(0.2, 0.7, 0.3, 1), box-shadow 0.28s ease;
  will-change: transform;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.08),
    0 2px 0 rgba(0, 0, 0, 0.28),
    0 var(--card-tilt-thickness) 0 rgba(0, 0, 0, 0.32),
    0 calc(var(--card-tilt-thickness) + 8px) 18px rgba(0, 0, 0, 0.4);
}

.card-tilt.is-dragging {
  transition: none;
  cursor: grabbing;
}

/* --- Scroll indicator ----------------------------------------------------- */

/* The layer `ScrollHint` moves. It spans the viewport so the bar can be placed anywhere, and it is
   `pointer-events: none` because it is a hint and never a control: it must not swallow the click of
   the panel it floats over. Only `opacity` is animated, which the compositor handles without
   repainting the card list behind it. */
.scroll-hint {
  position: fixed;
  inset: 0;
  z-index: var(--z-scroll-hint);
  opacity: 0;
  transition: opacity var(--duration-base) var(--easing-standard);
  pointer-events: none;
}

.scroll-hint.is-visible {
  opacity: 1;
}

.scroll-hint__bar {
  position: absolute;
  border-radius: var(--radius-pill);
  background-color: var(--color-text-muted);
  opacity: 0.55;
}

/* The preference of the settings screen. `ScrollHint.setNativeBars()` writes the attribute on
   `<html>`, which stops the two `scrollbar` rules at the top of this file from matching: the
   browser paints its own bar again, and this layer steps aside instead of drawing a second one on
   top of it. A player who never touches the switch keeps the thin, themed bar. */
html[data-native-scrollbars="true"] .scroll-hint {
  display: none;
}

/* --- Accessibility: reduced motion -------------------------------------- */

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