/* THE SHELL'S SKIN. Mobile first — every rule here is the phone, and the one media query at the
   bottom is the only place a wider screen is considered.

   ⚠ THE COLOUR TOKENS BELOW ARE ONLY THE FIRST-PAINT FALLBACK (French). `core/theme.js` overwrites
   every one of them on the root element as soon as the language is known, from the same palette
   the storefront tiles and the channel art use. Never hard-code a colour outside this block — a
   literal hex in a rule is a colour that will not follow the language.

   ⚠ MODULES DO NOT STYLE THE SHELL AND THE SHELL DOES NOT STYLE MODULES. A module gets a bare
   mount element and these tokens; anything beyond that it ships itself. That is what lets a module
   be lifted into the native rewrite without dragging a stylesheet behind it. */

:root {
  --bg: #0a1020; --bg-2: #0e1a30; --card: #142442; --card-2: #1a2f56; --line: #203a68;
  --brand: #0055A4; --on-brand: #ffffff; --brand-edge: transparent;
  --accent: #FFD23F; --accent-ink: #1a1205;
  --ink: #eef3ff; --ink-dim: #9db0d6; --ink-faint: #64789f;
  --ok: #2fbf71; --no: #d64550; --ok-ink: #06210f;
  --stage: #060c18; --scrim: rgba(4,9,20,.62);
  --r: 14px; --r-sm: 9px; --top-h: 54px;
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* ⚠ `hidden` is only a UA `display:none`, and ANY author rule that sets display beats it —
   `.sheet { display: grid }` had the modal scrim covering the whole app on first paint, dimmed and
   unclickable, with nothing in the console to say so. Every state here toggles with `el.hidden`. */
[hidden] { display: none !important; }

html, body { height: 100%; overflow-x: hidden; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 15px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  overscroll-behavior-y: none;
}
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
h1, h2, h3 { margin: 0; font-weight: 700; letter-spacing: -.01em; }

/* ---- boot ------------------------------------------------------------------------------- */
.boot { position: fixed; inset: 0; display: grid; place-content: center; gap: 18px; justify-items: center; }
.boot-mark { width: 42px; height: 42px; color: var(--accent); }
.boot-dots { display: flex; gap: 6px; }
.boot-dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--ink-faint);
  animation: bd 1s infinite ease-in-out; }
.boot-dots i:nth-child(2) { animation-delay: .15s; }
.boot-dots i:nth-child(3) { animation-delay: .3s; }
@keyframes bd { 0%, 60%, 100% { opacity: .25 } 30% { opacity: 1 } }

/* ---- frame ------------------------------------------------------------------------------ */
.app { display: flex; flex-direction: column; height: 100%; }
.topbar {
  display: flex; align-items: center; gap: 8px; flex: none;
  height: calc(var(--top-h) + var(--safe-t)); padding: var(--safe-t) 12px 0;
  background: var(--bg); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 20;
}
.brand { display: flex; align-items: center; gap: 8px; font-weight: 700; }
/* The one place the SPINE colour is used as a fill — small, and always with `on` on top of it, so
   Germany's near-black and Italy's near-white both stay legible. */
.brand-mark { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 7px;
  background: var(--brand); color: var(--on-brand); padding: 4px;
  box-shadow: inset 0 0 0 1.5px var(--brand-edge, transparent); }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.chip { padding: 5px 10px; border-radius: 999px; background: var(--card);
  font-size: 12px; font-weight: 700; color: var(--ink-dim); }
.avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--card-2);
  display: grid; place-items: center; font-size: 12px; font-weight: 800; color: var(--ink-dim);
  overflow: hidden; }
.avatar.on { background: var(--accent); color: var(--accent-ink); }
.avatar img { width: 100%; height: 100%; object-fit: cover; }

.view { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 14px 12px calc(var(--safe-b) + 48px); }

/* ---- library ---------------------------------------------------------------------------- */
/* ---- the filter row -------------------------------------------------------------------------
   ⚠ IT STICKS. With one grid of twenty-four the controls scroll away in a few centimetres, and a
   filter you have to scroll back up to change is a filter nobody uses twice. */
/* ⚠ top: 0, NOT THE HEADER'S HEIGHT. `.view` is itself the scroll container, so a sticky child
   offsets from ITS top edge — which is already below the header. Using the header height pushed
   the whole row down by 54px and parked it on top of the first line of cards. */
.filters { position: sticky; top: 0; z-index: 10;
  display: flex; flex-wrap: wrap; align-items: center; gap: 7px;
  margin: -14px -12px 16px; padding: 12px;
  background: var(--bg); border-bottom: 1px solid var(--line); }

.chipf { display: inline-flex; align-items: center; gap: 6px; padding: 7px 12px;
  border-radius: 999px; background: var(--card); border: 1px solid var(--line);
  font-size: 12.5px; font-weight: 600; color: var(--ink-dim); }
.chipf i { font-style: normal; font-size: 10.5px; font-weight: 800; color: var(--ink-faint);
  font-variant-numeric: tabular-nums; }
.chipf.on { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.chipf.on i { color: var(--accent-ink); opacity: .65; }
/* ⚠ A ZERO CHIP IS SHOWN, NOT HIDDEN. Removing it would make the row reflow every time the other
   axis changed, and a control that moves under your finger is worse than one that is dim.
   ⚠ AND THE MODIFIER IS `zero`, NOT `empty`: `.empty` is already the app's empty-STATE block, with
   44px of padding, so a chip wearing that name grew into a 108px circle. A generic utility class
   name is a namespace, and a modifier must not walk into it. */
.chipf.zero { opacity: .45; }
.fsep { width: 1px; align-self: stretch; margin: 2px 4px; background: var(--line); }

/* ⚠ minmax(0,1fr) NOT 1fr. A bare 1fr floors at min-content, and a card holding one long word
   pushed the second column off the right edge of the phone. */
.grid { display: grid; gap: 12px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.card {
  position: relative; display: flex; flex-direction: column; gap: 6px;
  padding: 13px; border-radius: var(--r); background: var(--card);
  /* ⚠ AN EDGE, NOT JUST A FILL. Filled blocks on a near-coloured ground merge into each other at a
     glance; a hairline is what makes twelve of them read as twelve. */
  border: 1px solid var(--line); text-align: left; min-height: 122px;
  transition: transform .12s ease, background .12s ease;
}
.card:active { transform: scale(.975); }
.card .ico { font-size: 26px; line-height: 1; }
.card .nm { font-weight: 700; font-size: 14px; }
.card .bl { font-size: 11.5px; color: var(--ink-dim); line-height: 1.35; }
.card .foot { margin-top: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
/* ⚠ COMING SOON IS DIMMED, NOT GREYED THE SAME WAY AS PREMIUM. A member has to be able to tell at a
   glance which cards they could open right now by paying, and which nobody can open yet. */
.card.soon { background: var(--bg-2); opacity: .62; }
.card.soon .ico { filter: grayscale(1); }

/* THREE STATES, AND THEY MUST LOOK DIFFERENT. Open wears no badge at all — a shelf where every
   card shouts is a shelf nobody reads. Premium is the accent, because it is an invitation. Coming
   soon is grey, because it is an apology. */
.pill { padding: 3px 8px; border-radius: 999px; font-size: 9.5px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .05em; }
.pill.soon { background: color-mix(in srgb, var(--ink-faint) 18%, transparent); color: var(--ink-faint); }
/* ⚠ THE GENRE IS AN OUTLINE, NOT A FILL. It states a fact; Premium is an invitation and Coming soon
   is an apology, and both of those need to win the eye. A third filled pill in the same row would
   have made the foot of every card a wall of colour saying nothing in particular. */
.pill.genre { background: none; border: 1px solid var(--line); color: var(--ink-faint);
  font-weight: 700; }

/* Best score and rank, on the card. `i` is filled in after the standings land. */
.card .stat { margin-left: auto; display: flex; align-items: baseline; gap: 5px;
  font-size: 10px; color: var(--ink-faint); }
/* ⚠ THE RANK IS THE LOUD HALF NOW. The score used to be first and dim with the rank beside it;
   the pair is a rank out of a field, and the number that means something is the first one. */
.card .stat b { font-size: 13px; color: var(--accent); font-weight: 800;
  font-variant-numeric: tabular-nums; }
.card .stat i { font-style: normal; font-size: 11px; color: var(--ink-faint); font-weight: 700; }


/* ---- play ------------------------------------------------------------------------------- */
.play { display: flex; flex-direction: column; gap: 10px; }
.play-bar { display: flex; align-items: center; gap: 10px; }
.play-bar .back { font-size: 20px; padding: 2px 6px; }
.play-bar .ttl { font-weight: 700; }
.play-bar .rank { margin-left: auto; font-weight: 800; font-size: 12px; color: var(--ink-faint);
  font-variant-numeric: tabular-nums; }
.play-bar .rank:not(:empty) + .sc { margin-left: 10px; }
.play-bar .rank:empty + .sc { margin-left: auto; }
.play-bar .sc { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 700;
  color: var(--accent); }
/* ⚠ THE STAGE TAKES THE ROOM IT HAS. It was 330px tall on every screen, so a desktop drew a small
   square in the middle of a large div — the thing JC saw as the game "playing at half the space".
   A game measures this element, so its height IS the game's size. */
.stage {
  position: relative; border-radius: var(--r); background: var(--stage);
  border: 1px solid var(--line); overflow: hidden;
  min-height: min(62vh, 560px);
  display: grid; place-items: center;
}
.stage canvas { display: block; max-width: 100%; touch-action: none; }
/* ⚠ URGENT ONLY AT THE END. A countdown that is red for three minutes is a countdown somebody
   plays badly under; the last thirty seconds are the only part worth flinching at. */
/* A board that could not be read says so, in the colour of a problem. */
.boardwhy { margin: 0; color: var(--no); font-size: 13px; line-height: 1.5; }
.boardwhy small { color: var(--ink-faint); word-break: break-word; }
.prev-strip.soon { background: color-mix(in srgb, var(--no) 14%, transparent); color: var(--no); }
/* ⚠ A MEMBER'S STRIP IS THE SAME ROW SAYING THE OPPOSITE THING, so it is the same row — quieter,
   because a number that is climbing is good news and good news does not need a coloured bar. */
.prev-strip.open { background: color-mix(in srgb, var(--ok) 10%, transparent); color: var(--ink-dim); }
/* ⚠ THE BUTTON IS QUIET UNTIL THE CLOCK IS NOT. It sits beside the countdown all the way through —
   an ask nobody can act on is an ask that only delivers bad news — but it borrows the row's colour
   rather than shouting over the game somebody is in the middle of playing. */
.strip-cta { margin-left: auto; font: inherit; font-weight: 700; color: inherit; cursor: pointer;
  background: color-mix(in srgb, currentColor 13%, transparent); border: 0;
  border-radius: 999px; padding: 4px 11px; white-space: nowrap; }
.strip-cta:hover { background: color-mix(in srgb, currentColor 22%, transparent); }
.prev-strip { display: flex; align-items: center; gap: 8px; padding: 8px 11px;
  border-radius: var(--r-sm); background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent); font-size: 11.5px; font-weight: 600; }

/* ---- generic ---------------------------------------------------------------------------- */
/* ⚠ STATES ITS OWN COLOUR AND KILLS THE UNDERLINE, because a .btn is sometimes an <a>. The
   `button` reset above only reaches buttons, so the YouTube link on the door came out as default
   browser blue, underlined, in the middle of a dark card. */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 12px 18px; border-radius: 999px; background: var(--card-2); font-weight: 700;
  font-size: 14px; color: var(--ink); text-decoration: none; }
.btn.pri { background: var(--accent); color: var(--accent-ink); }
.btn.wide { width: 100%; }
.btn:disabled { opacity: .5; }
.row { display: flex; align-items: center; gap: 10px; padding: 13px 12px;
  border-radius: var(--r-sm); background: var(--card); width: 100%; text-align: left; }
.row .g { flex: 1; min-width: 0; }
.row .g b { display: block; font-size: 13.5px; }
.row .g small { color: var(--ink-dim); font-size: 11.5px; }
.stack { display: grid; gap: 8px; }
.muted { color: var(--ink-dim); font-size: 12.5px; }
.empty { text-align: center; padding: 44px 20px; color: var(--ink-faint); }
.tiles { display: flex; flex-wrap: wrap; gap: 7px; }
.tile { padding: 9px 12px; border-radius: var(--r-sm); background: var(--card-2);
  font-weight: 700; font-size: 14px; }
.tile.on { background: var(--accent); color: var(--accent-ink); }

/* ---- sheet (account menu, paywall, dialogs) ---------------------------------------------- */
.sheet { position: fixed; inset: 0; z-index: 40; display: grid; align-items: end;
  background: var(--scrim); backdrop-filter: blur(2px); }
/* ⚠ AND IT SCROLLS RATHER THAN OVERFLOWING. The wall is the tallest sheet in the app — a picture,
   three rows and two buttons — and on a short phone in landscape the primary button was the part
   that went off the bottom. */
.sheet-card { background: var(--bg-2); border-radius: 20px 20px 0 0; padding: 20px 16px;
  padding-bottom: calc(20px + var(--safe-b)); animation: up .2s ease;
  max-height: 92vh; overflow-y: auto; }
@keyframes up { from { transform: translateY(14px); opacity: .4 } }
.sheet-card h3 { font-size: 18px; margin-bottom: 6px; }

/* ---- toasts / dev ------------------------------------------------------------------------ */
.toasts { position: fixed; left: 12px; right: 12px; bottom: calc(var(--safe-b) + 14px);
  z-index: 50; display: grid; gap: 7px; pointer-events: none; }
.toast { padding: 11px 14px; border-radius: var(--r-sm); background: var(--card-2);
  font-size: 13px; box-shadow: 0 6px 20px rgba(0,0,0,.4); animation: up .18s ease; }
.toast.bad { background: var(--no); }
.toast.good { background: var(--ok); color: var(--ok-ink); }

/* Sits at the very bottom now that nothing else does. A dev affordance that occludes real chrome
   makes every screenshot a lie about what the member sees. */
.devbar { position: fixed; bottom: calc(var(--safe-b) + 8px); left: 50%; transform: translateX(-50%);
  z-index: 60; display: flex; gap: 4px; padding: 4px; border-radius: 999px;
  background: rgba(0,0,0,.78); border: 1px solid var(--line); font-size: 10px; }
.devbar button { padding: 4px 9px; border-radius: 999px; font-weight: 700; color: var(--ink-dim); }
.devbar button.on { background: var(--accent); color: var(--accent-ink); }

/* ---- wider screens ------------------------------------------------------------------------
   No sidebar. The phone layout simply centres and the grid gains columns — one layout to reason
   about, and the native rewrite inherits one set of decisions rather than two. */
@media (min-width: 560px) { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 900px) {
  .view { padding: 22px 26px 60px; max-width: 1040px; margin: 0 auto; width: 100%; }
  .topbar { padding-left: max(26px, calc((100vw - 1040px) / 2)); padding-right: max(26px, calc((100vw - 1040px) / 2)); }
  .grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .sheet { place-items: center; }
  .sheet-card { border-radius: var(--r); width: min(430px, 90vw); padding-bottom: 20px; }
  .toasts { left: auto; right: 20px; bottom: 20px; width: 320px; }
}

/* ---- ranking ------------------------------------------------------------------------------- */
.rankline { margin: 0 0 14px; font-size: 12.5px; color: var(--accent); font-weight: 700; }
.brow { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--r-sm);
  background: var(--card); }
.brow.me { background: var(--accent); color: var(--accent-ink); }
.brow .pos { width: 26px; font-weight: 800; font-variant-numeric: tabular-nums; font-size: 12px; }
.brow .who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13px; font-weight: 600; }
.brow .pts { font-variant-numeric: tabular-nums; font-weight: 800; font-size: 13px; }
.scopes { display: flex; gap: 6px; margin-bottom: 12px; }

/* ---- the door ------------------------------------------------------------------------------
   ⚠ THIS IS THE ONLY SCREEN THAT IS ALLOWED TO BE BIG. Everywhere else the shell keeps out of the
   way of a game; here there is nothing else, and a welcome that whispers is not a welcome. */
.land { display: grid; gap: 26px; padding-bottom: 20px; }

.land-hero { position: relative; border-radius: var(--r); overflow: hidden;
  min-height: 210px; display: flex; align-items: flex-end;
  background: linear-gradient(180deg, var(--bg-2), var(--bg)); border: 1px solid var(--line); }
.land-hero .scene { position: absolute; inset: 0; width: 100%; height: 100%; }
/* ⚠ THE WORDS NEED THEIR OWN GROUND. A title sitting straight on a drawing is legible until the
   drawing changes — and this one changes with the language. The scrim is part of the hero, not a
   property of any particular scene. */
.land-hero::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 30%, var(--bg) 100%); opacity: .92; }
.land-words { position: relative; z-index: 1; padding: 18px; }
.land-words h1 { font-size: 30px; letter-spacing: -.02em; line-height: 1.05; }
.land-words p { margin: 8px 0 0; font-size: 14px; color: var(--ink-dim); max-width: 30ch; }

/* The line that says what is free and what is not. Set apart from the pitch above it, because it
   is the sentence somebody is actually looking for. */
.land-free { margin: 12px 0 0; font-size: 13px; line-height: 1.55; color: var(--ink);
  padding-top: 12px; border-top: 1px solid var(--line); }

.land-pitch { padding: 16px; border-radius: var(--r); background: var(--card);
  border: 1px solid var(--line); }
.land-pitch h2 { font-size: 15px; margin-bottom: 8px; }
.land-pitch p { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--ink-dim); }

.land-cta { display: grid; gap: 10px; justify-items: center; }
.land-why { margin: 0; text-align: center; font-size: 11.5px; color: var(--ink-faint);
  max-width: 34ch; }
.land-foot { margin: 0; text-align: center; font-size: 11px; color: var(--ink-faint); }

@media (min-width: 700px) {
  .land-hero { min-height: 280px; }
  .land-words { padding: 26px; }
  .land-words h1 { font-size: 40px; }
  .land-cta .btn { min-width: 260px; width: auto; }
}

/* ---- a game's lobby --------------------------------------------------------------------------
   ⚠ THE INTRO SCREEN IS PART OF THE GAME, NOT THE SHELL. It is mounted inside the stage by the
   module, because what belongs on it — the levels, the word for what you are catching — is the
   module's own. What it borrows from here is only the furniture: a hero, a row of numbers, and the
   same leaderboard rows the ranking screen uses, so a board looks like a board everywhere.

   If a second game wants this shape it moves to modules/_shared/lobby.js. One game is not a
   pattern; two is. */
.lobby { width: 100%; max-width: 460px; padding: 18px; display: grid; gap: 16px;
  align-content: start; }
/* The panel the game comes from. Full width, because a comic panel shrunk to a thumbnail is a
   decoration; at this size the balloon is readable and the balloon is the point. */
.bet { margin: 0 0 14px; }
.bet img { width: 100%; display: block; border-radius: var(--r-sm); border: 1px solid var(--line); }
.bet figcaption { margin: 7px 2px 0; font-size: 12px; line-height: 1.45; color: var(--ink-dim); }
.bet figcaption b { color: var(--accent); font-weight: 600; }
.lobby-hero { display: flex; align-items: center; gap: 14px; }
.lobby-face { image-rendering: pixelated; flex: none; }
.lobby-hero h2 { font-size: 26px; line-height: 1; letter-spacing: -.02em; }
.lobby-hero p { margin: 6px 0 0; font-size: 12.5px; line-height: 1.45; }

.svs { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 8px; }
.sv { display: grid; gap: 2px; justify-items: center; padding: 10px 6px;
  border-radius: var(--r-sm); background: var(--card); border: 1px solid var(--line); }
.sv b { font-size: 18px; font-variant-numeric: tabular-nums; }
.sv small { font-size: 10px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-faint); font-weight: 700; }

/* ⚠ THE BOARD ROW IS TWO LINES HERE AND ONE LINE EVERYWHERE ELSE. Scoped to the lobby on purpose:
   the ranking screen lists a person's standing across every game, where a level from one of them
   would be a column that is empty for most rows. */
.lobby-board .who { display: flex; flex-direction: column; gap: 1px; line-height: 1.25; }
.lobby-board .who > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lobby-board .who small { font-size: 10.5px; color: var(--ink-faint); font-weight: 600; }
.lobby-board .brow.me small { color: inherit; opacity: .72; }

.lobby-board .lbl { margin: 0 0 8px; font-size: 10px; font-weight: 800; letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-faint); }

/* ---- a game's footer legend ------------------------------------------------------------------
   ⚠ TWO WORDS PER ICON, AND NO MORE. Nothing on a pixel field is labelled, so a flask, a stake and
   a boar all read as scenery until something says what they do — and the only place anybody will
   read that is under the field they are about to play. A paragraph here would be a paragraph
   nobody finishes. */
.pitchwrap { display: grid; gap: 8px; justify-items: center; width: 100%; }

/* ⚠ LIVES LIVE OUTSIDE THE PITCH. Drawn in the corner of the field they were the same sprite as the
   boar you chase, and a counter inside the play area is one more thing a player has to learn is not
   part of the play. Above the rectangle, labelled, it is a fact about the game rather than a thing
   in it. */
.lifebar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: 6px; min-height: 20px; max-width: 100%; }
.lifebar .lb-l { display: flex; align-items: center; gap: 6px; justify-self: start; }
.lifebar .lb-c { justify-self: center; font-size: 11.5px; letter-spacing: .01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lifebar small { font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-faint); }
.lifebar #lifepips { display: flex; gap: 3px; }
.lifebar canvas { image-rendering: pixelated; width: 18px; height: 18px; }
/* ⚠ max-width, or it does not wrap. In a centred grid an item is sized to its content and
   simply overflows the stage instead of breaking onto a second row — five labels ran off
   the right-hand edge of a phone. */
/* The game's own voice: "niveau impeccable", "non, Obélix". It used to be a balloon dropped over
   the middle of the board — the one rectangle a player is actually reading. It keeps its line
   whether or not it has anything to say, so a message never resizes the field under the run. */
.cryline { min-height: 19px; line-height: 19px; margin: 3px 0 2px; text-align: center;
  font-size: 13px; font-weight: 900; letter-spacing: .02em; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; color: var(--ink);
  opacity: 0; transition: opacity .16s ease; }
.cryline.on { opacity: 1; }
.cryline.gold { color: var(--accent); }

/* THE SHELF. ⚠ A LOCKED BADGE IS DIMMED, NOT HIDDEN: the ones you have not got are the reason to
   play, and a grid that fills up as you earn them is the whole shape of an achievement screen. */
.badges { display: grid; gap: 10px; }
.bdg { display: flex; align-items: center; gap: 12px; padding: 10px 12px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-sm); }
.bdg img { image-rendering: pixelated; flex: none; }
.bdg.off img { filter: grayscale(1) brightness(.62); opacity: .5; }
.bdg .g { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.bdg b { font-size: 14px; }
.bdg small { font-size: 11.5px; color: var(--ink-dim); line-height: 1.35; }
.bdg small.n { color: var(--ink-faint); }
/* ⚠ THE BAR IS THE NEXT RUNG, NOT THE WHOLE LADDER. "18 / 25" is a thing somebody can finish
   tonight; "18 / 2000" is a thing nobody starts. */
.bdg .bar { height: 4px; border-radius: 3px; background: var(--bg-2); overflow: hidden;
  margin: 3px 0 1px; }
.bdg .bar i { display: block; height: 100%; background: var(--accent); border-radius: 3px; }

.legend { max-width: 100%; display: flex; flex-wrap: wrap; align-items: center;
  justify-content: center;
  gap: 6px 14px; font-size: 11px; line-height: 1; color: var(--ink-dim); }
.legend i { display: inline-flex; align-items: center; gap: 5px; font-style: normal;
  white-space: nowrap; }
.legend canvas { image-rendering: pixelated; width: 18px; height: 18px; flex: none; }
.legend .lg-key { font: inherit; color: var(--ink-dim); background: var(--bg-2);
  border: 1px solid var(--line); border-radius: 999px; padding: 4px 10px; cursor: pointer; }
.legend .lg-key:hover { color: var(--ink); border-color: var(--ink-faint); }

/* ---- the wall ---------------------------------------------------------------------------------
   ⚠ THE ONE SCREEN IN THE APP THAT ASKS FOR MONEY, and the only one allowed to move. Everything
   else here holds still on purpose — an app that animates at you is an app that is selling. This
   one IS selling, once, at the single moment somebody has just proved to themselves that they want
   more of it, so it gets a cauldron that bubbles and a fire that flickers and nothing else does. */
.upsell { text-align: center; }
.upsell h3 { font-size: 20px; margin: 10px 0 4px; }
.upsell .muted { margin: 0 0 14px; font-size: 13px; line-height: 1.5; }
.upsell .stack { margin-top: 16px; }

.wall-score { margin: 0 0 10px; font-size: 15px; font-weight: 800; color: var(--accent);
  font-variant-numeric: tabular-nums; }
.wall-foot { margin: 12px 0 0; font-size: 10.5px; line-height: 1.45; color: var(--ink-faint); }

/* Three claims, all of them checkable. A fourth would be the one nobody believes. */
.perks { list-style: none; display: grid; gap: 8px; text-align: left; margin: 0; padding: 0; }
.perks li { display: flex; align-items: center; gap: 11px; padding: 9px 12px;
  border-radius: var(--r-sm); background: var(--card); border: 1px solid var(--line);
  font-size: 12.5px; line-height: 1.35; }
.perks b { flex: none; display: grid; place-items: center; width: 28px; height: 28px;
  border-radius: 8px; background: color-mix(in srgb, var(--ok) 16%, transparent);
  color: var(--ok); font-size: 15px; font-weight: 800; line-height: 1; }

.cdrn { display: block; width: 132px; height: 110px; margin: 2px auto 0; overflow: visible; }
/* ⚠ IRON IS IRON IN EITHER THEME, so it is a literal colour and not a token. `--ink` made it a
   white bathtub in the dark theme; `--silhouette` made it brand-blue, which reads as plastic. A
   fixed dark grey is right on a light ground on its own, and on a dark one the rim highlight is
   what separates it from the card. */
.cdrn .pot { fill: #333a46; }
.cdrn .rim { fill: #3d4553; stroke: color-mix(in srgb, var(--ink) 22%, transparent); stroke-width: 1.5; }
.cdrn .feet { stroke: #333a46; stroke-width: 7; stroke-linecap: round; }
.cdrn .brew { fill: var(--ok); }
.cdrn .shine { fill: #ffffff; opacity: .3; }
.cdrn .bubbles circle { fill: color-mix(in srgb, var(--ok) 55%, white); }
.cdrn .steam circle { fill: var(--ok); opacity: 0; }
.cdrn .fire path { fill: var(--accent); transform-origin: 70px 108px; }

/* ⚠ THE BUBBLES ARE STAGGERED, NOT SYNCHRONISED. Three circles rising in lockstep read as one
   object moving; a third of a second apart they read as a liquid. */
.cdrn .bubbles circle { animation: bub 2.4s ease-in infinite; }
.cdrn .bubbles circle:nth-child(2) { animation-delay: .8s; }
.cdrn .bubbles circle:nth-child(3) { animation-delay: 1.6s; }
.cdrn .steam circle { animation: steam 3.2s ease-out infinite; }
.cdrn .steam circle:nth-child(2) { animation-delay: 1.1s; }
.cdrn .steam circle:nth-child(3) { animation-delay: 2.2s; }
.cdrn .fire path { animation: flick .5s ease-in-out infinite alternate; }

@keyframes bub  { 0% { transform: translateY(6px) scale(.4); opacity: 0 }
                  35% { opacity: .95 }
                  100% { transform: translateY(-16px) scale(1); opacity: 0 } }
@keyframes steam { 0% { transform: translateY(10px) scale(.6); opacity: 0 }
                   30% { opacity: .28 }
                   100% { transform: translateY(-22px) scale(1.5); opacity: 0 } }
@keyframes flick { from { transform: scaleY(.82) scaleX(1.06) } to { transform: scaleY(1.12) } }

/* ⚠ AND IT ALL STOPS FOR ANYONE WHO ASKED IT TO. A payment screen is exactly the wrong place to
   ignore a person's accessibility setting. */
@media (prefers-reduced-motion: reduce) {
  .cdrn .bubbles circle, .cdrn .steam circle, .cdrn .fire path { animation: none; }
  .cdrn .steam circle { opacity: .18; }
}

.credit { margin: -2px 2px 2px; font-size: 10.5px; line-height: 1.45; color: var(--ink-faint); }

/* ⚠ SIZED IN `em`, SO IT OBEYS WHOEVER DREW IT. An icon that is a fixed 22px is an icon that is
   wrong on the 34px empty stage and wrong again the next time a card changes size; at 1em it is
   exactly as big as the emoji it sits beside would have been.
   And `pixelated`, because it IS pixel art: sixteen pixels smoothed up to twenty-six is a blur of
   a helmet rather than a helmet. */
.ico-img { width: 1em; height: 1em; image-rendering: pixelated; vertical-align: -0.1em; }

/* ⚠ THE SCOPE SWITCH IS TWO WORDS IN THE HEADING, not a control above it. It is the smallest thing
   on the screen that changes what the biggest thing on the screen means, and it earns exactly that
   much room.
   ⚠ AND THE GUARD THAT ADDED THIS LOOKED FOR ".scope", WHICH MATCHED THE EXISTING ".scopes" RULE
   above and skipped the whole block while reporting success. Substring tests are not membership
   tests. */
.lobby-board .lbl { display: flex; align-items: center; gap: 8px; }
.lobby-board .scope { font: inherit; font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase;
  font-weight: 800; color: var(--ink-faint); background: none; border: 0; padding: 2px 0;
  cursor: pointer; border-bottom: 2px solid transparent; }
.lobby-board .scope.on { color: var(--accent); border-bottom-color: var(--accent); }
.lobby-board .scope:not(.on):hover { color: var(--ink-dim); }

/* THE ACCOUNT SHEET'S ONE LINE OF FACTS. Three rows of a single number each read as a dashboard;
   this is a sentence, and the numbers are the only part of it that is loud. */
.acct-stats { margin: 2px 0 0; color: var(--ink-dim); font-size: 12.5px; }
.acct-stats b { color: var(--ink); font-size: 14px; font-variant-numeric: tabular-nums; }
.acct-stats .dot { opacity: .45; padding: 0 2px; }

/* ACTIVATION. ⚠ THE FIELDS ARE FULL WIDTH AND THE LABELS SIT ABOVE THEM, because this form is
   filled in on a phone by somebody who has just come from the YouTube app. */
.act-form { display: grid; gap: 14px; }
.fld { display: grid; gap: 6px; border: 0; padding: 0; margin: 0; }
.fld > span { font-size: 12.5px; font-weight: 700; color: var(--ink-dim); }
.fld input, .fld textarea {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 15px;
  color: var(--ink); background: var(--card-2); border: 1px solid transparent;
  border-radius: var(--r-sm); padding: 11px 12px; resize: vertical; }
.fld input:focus, .fld textarea:focus { outline: none; border-color: var(--accent); }
.fld input::placeholder, .fld textarea::placeholder { color: var(--ink-faint); }
/* The level is a radio wearing a tile, so it matches every other choice in the app.
   ⚠ THE RADIO ITSELF IS HIDDEN, NOT STYLED. `accent-color` painted the checked dot in the accent
   — the same colour the checked TILE turns — so the selected option showed a yellow dot on yellow
   and the unselected one showed a bright ring on dark. It read exactly backwards. The fill is the
   answer, as it is for every other tile in the app; the input stays in the DOM for the form and for
   the keyboard, and `:focus-visible` is what puts it back on screen for anybody arriving by tab. */
.tile.pick { cursor: pointer; display: inline-flex; align-items: center; font-size: 13px;
  white-space: nowrap; }
.tile.pick input { position: absolute; opacity: 0; pointer-events: none; }
.tile.pick:has(input:checked) { background: var(--accent); color: var(--accent-ink); }
.tile.pick:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.act-state { border-left: 3px solid var(--accent); }
.act-state.ok { border-left-color: #3f9c46; }

/* ── the gallery (views/gallery.js) ─────────────────────────────────────────────────────────── */
.gal-banner { display: flex; flex-direction: column; gap: 3px; width: 100%; text-align: left; margin: 0 0 12px; padding: 14px 16px;
  border: 0; border-radius: var(--r-md, 14px); cursor: pointer; color: var(--accent-ink);
  background: linear-gradient(120deg, var(--accent), color-mix(in srgb, var(--accent) 70%, #fff)); }
.gal-banner b { font-size: 16px; }
.gal-banner span { font-size: 12.5px; opacity: .8; }
.gal-head { margin: 4px 0 12px; }
.gal-head h2 { margin: 6px 0 4px; font-size: 20px; }
.gal-head h2 b { color: var(--accent); margin-right: 6px; }
.gal-head p, .gal-by { color: var(--ink-dim); font-size: 13px; margin: 0 0 10px; }
.gal-back { padding: 6px 12px; font-size: 12.5px; }
.gal-albums { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
.gal-album { display: flex; flex-direction: column; gap: 4px; text-align: left; padding: 0 0 10px; border: 1px solid var(--line);
  border-radius: 12px; overflow: hidden; background: var(--bg-2); color: inherit; cursor: pointer; }
.gal-album img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; display: block; }
.gal-album .nm { padding: 0 10px; font-weight: 700; font-size: 14px; }
.gal-album .nm b { color: var(--accent); }
.gal-album .bl { padding: 0 10px; font-size: 11.5px; color: var(--ink-dim); }
.gal-grid { display: grid; gap: 8px; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.gal-item { position: relative; padding: 0; border: 0; border-radius: 10px; overflow: hidden; cursor: pointer; background: #000; line-height: 0; }
.gal-item img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.gal-moves { position: absolute; right: 6px; bottom: 6px; line-height: 1.3; font-size: 10.5px; font-weight: 800; padding: 2px 7px;
  border-radius: 999px; background: var(--accent); color: var(--accent-ink); }
.gal-view img, .gal-view video { width: 100%; border-radius: 10px; display: block; background: #000; }
.gal-cap { font-size: 12.5px; color: var(--ink-dim); margin: 8px 0; }
.gal-dls { display: flex; flex-direction: column; gap: 8px; }
.gal-dls .btn { width: 100%; justify-content: flex-start; font-size: 13px; text-decoration: none; }
.gal-lock { opacity: .7; }

/* the gallery on the village's home: a section of album cards (views/library.js) */
.home-gal { margin: 0 0 18px; }
.home-h { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin: 4px 0 10px; font-size: 17px; }
.home-h span { font-size: 12.5px; font-weight: 400; opacity: .75; }
.home-gal .gal-albums { margin: 0; }
.home-gal .gal-album .pill { margin-left: 6px; vertical-align: middle; }

/* the film's music, under its paintings (views/gallery.js) */
.gal-music { margin: 22px 0 8px; }
.gal-music h3 { margin: 0 0 4px; font-size: 16px; }
.gal-track { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-top: 1px solid var(--line); }
.gal-track .nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; }
.gal-track .btn { flex: none; padding: 6px 12px; font-size: 13px; }
