/* ===========================================================================
   Offline practice mode.

   `body.qzr-offline-session` tracks the LIVE restricted state
   (QzrOffline.restrictToDownloaded(): offline forced, or network down) and is
   owned by QzrOffline.syncOfflineBodyClass() — every writer, qzr-auth.js
   included, goes through it so the class can never disagree with the offline
   widgets it renders alongside.

   The banner is not decoration: in that state most of the app cannot work
   (nothing server-backed), and without an explicit signal every failure would
   read as a bug. It also tells the user their results are not being recorded,
   which they otherwise have no way of knowing.
   See docs/architecture/offline-practice-mode.md.
   =========================================================================== */

/* Text only, no icon glyph. This opened with "\f6ff" (plane-slash), which is
   not in Font Awesome 5 Free — and at font-weight 600 it would not have
   selected the Solid face even if it were. Both roads led to a tofu box in
   front of the message. An icon font inside a pseudo-element cannot fail
   gracefully, so the banner does without one. */
/* Fixed, and the shell is offset by the banner's MEASURED height in
   qzr-offline-ui.js (syncBannerOffset).
   Two dead ends are worth recording so they are not retried:
   - `padding-top` on <body> does nothing here. Something forces body padding
     with !important: even an INLINE 80px computes to 0. That is why the
     original 34px compensation never applied and the banner covered the
     topbar.
   - in-flow (static/sticky) does not work either, because <body> is
     `display: flex; flex-direction: row`, so a ::before becomes a flex ITEM
     and lands beside the content rather than above it.
   Hence: out of flow (fixed keeps it from being a flex item at all) plus a
   measured offset on `.cover-container`, the real shell — padding there does
   move the content. Measured rather than hardcoded because the text wraps at
   narrow widths: observed 44.6px and 73px on the same device. */
body.qzr-offline-session::before {
  content: "Mode hors connexion · seuls tes contenus téléchargés sont disponibles";
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-weight: 600;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10000;
  padding: 8px 14px;
  text-align: center;
  font-size: var(--qz-fs-xs);
  line-height: 1.3;
  /* --qz-warning-bg / --qz-warning already carry their own dark-mode and
     per-theme overrides in vnext-tokens.css, so no media query is needed here. */
  background: var(--qz-warning-bg);
  color: var(--qz-warning-ink);
  box-shadow: 0 1px 0 rgba(0, 0, 0, .08);
  pointer-events: none;
}

/* No padding here on purpose — the offset lives on `.cover-container` and is
   applied from JS (syncBannerOffset), because body padding is ignored. */

/* ===========================================================================
   Offline is a SMALLER app, not the same app with dead controls.

   Anything that cannot work without a server is removed rather than greyed
   out. Greying tells the user "this exists, and you may not have it", which is
   noise when nine controls in ten are unusable; hiding leaves a short, honest
   surface of things that genuinely work. Everything comes back untouched the
   moment `qzr-offline-session` is removed.
   =========================================================================== */
body.qzr-offline-session [data-qzr-online-only] {
  display: none !important;
}

/* Le panneau du menu du haut separe desormais CHAQUE element a partir de
   Reglages. Hors ligne, Reglages et Profil disparaissent avec les autres
   entrees en ligne — et leurs separateurs restaient, en laissant deux traits
   colles (mesure : 2 paires). `display:none` ne rapproche pas les freres au
   sens du selecteur `+`, l'element masque reste STRUCTURELLEMENT entre les
   deux traits : c'est `:has()` qui permet de viser le separateur par ce qui le
   SUIT, et donc de retirer celui qui n'introduit plus rien. */
body.qzr-offline-session .qzv-menu__sep:has(+ [data-qzr-online-only]) {
  display: none !important;
}

/* The bottom tab bar routes almost entirely to server-backed pages, and the
   dashboard now carries direct entry points to the two that work. */
body.qzr-offline-session #qzr-mobile-bottom-nav {
  display: none !important;
}

/* Dashboard: the mission card advertises a run the device may not hold, and
   "Afficher plus" unfolds quêtes/chests/activity — all server-backed. The
   offline quick links injected by qzr-offline-ui.js replace both. */
body.qzr-offline-session .qzv-mission-card,
body.qzr-offline-session .qzv-more-toggle,
body.qzr-offline-session .qzv-more-body {
  display: none !important;
}

/* --- Offline dashboard quick links ---------------------------------------- */
.qzv-offline-links {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin: 0 0 12px;
}
.qzv-offline-link {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding: 14px;
  border-radius: var(--qz-r-card, 16px);
  background: var(--qz-surface);
  border: 1px solid var(--qz-line);
  color: inherit;
  text-decoration: none;
}
.qzv-offline-link:active { transform: scale(var(--qz-press-scale)); }
.qzv-offline-link__icon {
  font-size: 1.15rem;
  color: var(--qz-accent);
  margin-bottom: 2px;
}
.qzv-offline-link__title {
  font-weight: 700;
  font-size: var(--qz-fs-sm, .88rem);
}
.qzv-offline-link__meta {
  font-size: var(--qz-fs-xs, .78rem);
  opacity: .7;
}
/* Nothing downloaded in that bucket — still reachable, but honest about it. */
.qzv-offline-link--empty { opacity: .6; }

/* An offline step whose quiz was never downloaded. It is a real <button
   disabled>, so it cannot be a silent dead tap; this only makes the reason
   legible. Distinct from the normal progression lock — the user has not failed
   to unlock it, they simply do not have it on the device. */
.category-path-step--offline-missing .qzr-path-step__lock {
  display: block;
  margin-top: 4px;
  font-style: normal;
  font-size: var(--qz-fs-xs);
  color: var(--qz-warning-ink);
}

/* SPECIFICITY NOTE — ui-visual-language.css styles these steps through ID
   selectors (#categories-detail .category-path-step, #categories-detail
   .qzr-path-step-locked), i.e. specificity 1,1,0. Class-only rules lose to that
   no matter the load order, which is why the first attempt here had no effect.
   Beat it with the ID plus the element+class (1,2,0) rather than !important. */
#categories-detail button.category-path-step--offline-missing,
#categories-detail button.category-path-step--offline-missing:hover,
#categories-detail button.category-path-step--offline-missing:focus {
  cursor: not-allowed;
  opacity: .55;
}

/* ===========================================================================
   Studio, rebuilt from IndexedDB when the server is unreachable.
   Rendered by studio.js renderOfflineList(). Only downloaded quizzes, and only
   the play action — edit / publish / duplicate need a server, so they are
   absent rather than present and broken.

   `!important` throughout, deliberately and against the usual house rule: this
   surface is styled through ID selectors elsewhere, which outrank any
   class-only rule here whatever the load order. Chaining IDs to win would
   couple these cards to markup they do not own. Scoped tightly to
   .studio-offline-* so the blast radius is exactly this list.

   Tokens are used WITHOUT fallbacks on purpose. A fallback hides a typo: an
   invented token silently renders the fallback and never picks up dark mode or
   any of the eight themes, which is how a previous pass shipped broken. Bare
   var() fails visibly instead. Spacing is in px because the token set has no
   spacing scale — the --qz-N names are the green colour ramp.
   =========================================================================== */
.studio-offline-note {
  margin: 0 0 14px !important;
  font-size: var(--qz-fs-xs) !important;
  font-weight: 600 !important;
  color: var(--qz-warning-ink) !important;
}
.studio-offline-grid {
  display: flex !important;
  flex-direction: column !important;
  gap: 12px !important;
}

/* The card is not interactive as a whole — only the button is.
 *
 * One fixed height for every card, so the list reads as an even rhythm however
 * long the titles are. It is derived from the very tokens the card is built
 * from rather than being a measured magic number, so it stays correct if the
 * type scale moves: two title lines + the body gap + one meta line, plus the
 * card's own vertical padding. The title is clamped to exactly those two lines
 * (below) so a long one can never overflow the box it is sized for.
 */
.studio-offline-card {
  --qz-offline-card-pad-y: 18px;
  --qz-offline-card-body-gap: 5px;
  --qz-offline-card-title-lh: 1.3;
  --qz-offline-card-meta-lh: 1.35;

  box-sizing: border-box !important;    /* the height below INCLUDES the padding */
  display: flex !important;
  align-items: center !important;
  justify-content: space-between !important;
  gap: 14px !important;
  padding: var(--qz-offline-card-pad-y) 18px !important;
  height: calc(
      2 * var(--qz-offline-card-pad-y)
    + 2 * var(--qz-offline-card-title-lh) * var(--qz-fs-sm)
    + var(--qz-offline-card-body-gap)
    + var(--qz-offline-card-meta-lh) * var(--qz-fs-xs)
  ) !important;
  border-radius: var(--qz-r-card) !important;
  background: var(--qz-surface) !important;
  border: 1px solid var(--qz-line) !important;
  box-shadow: var(--qz-shadow-sm) !important;
}
.studio-offline-card__body {
  display: flex !important;
  flex-direction: column !important;
  gap: var(--qz-offline-card-body-gap) !important;
  min-width: 0 !important;              /* long titles wrap instead of overflowing */
}
.studio-offline-card__title {
  font-weight: 700 !important;
  font-size: var(--qz-fs-sm) !important;
  /* --qz-text, NOT --qz-ink: ink is the mascot's constant visor colour and has
     no dark-theme override, so it stayed near-black on the dark surface and the
     titles disappeared. --qz-text is the one that actually flips. */
  color: var(--qz-text) !important;
  line-height: var(--qz-offline-card-title-lh) !important;
  /* Clamped to the two lines the fixed height above budgets for. */
  display: -webkit-box !important;
  -webkit-line-clamp: 2 !important;
  line-clamp: 2 !important;
  -webkit-box-orient: vertical !important;
  overflow: hidden !important;
}
.studio-offline-card__meta {
  font-size: var(--qz-fs-xs) !important;
  line-height: var(--qz-offline-card-meta-lh) !important;
  color: var(--qz-muted) !important;
}

/* The only hit area on the card. */
.studio-offline-card__play {
  flex: 0 0 auto !important;
  display: inline-flex !important;
  align-items: center !important;
  justify-content: center !important;
  gap: 8px !important;
  min-height: 44px !important;          /* comfortable tap target */
  min-width: 104px !important;          /* stable width when the spinner swaps in */
  padding: 11px 18px !important;
  border: 0 !important;
  border-radius: var(--qz-r-pill) !important;
  background: var(--qz-accent) !important;
  color: var(--qz-on-accent) !important;
  font-weight: 700 !important;
  font-size: var(--qz-fs-xs) !important;
  cursor: pointer !important;
}
.studio-offline-card__play:active { transform: scale(var(--qz-press-scale)) !important; }
.studio-offline-card__play[disabled],
.studio-offline-card__play.qzr-launch-busy {
  opacity: .75 !important;
  cursor: default !important;
}

/* ---------------------------------------------------------------------------
   The banner is a global state indicator, but it must not sit over the two
   full-screen surfaces: the pre-game showcase (a Bootstrap modal, hence
   body.modal-open) and the running quiz (body.realtime-active, set by the
   router). Both already state the offline condition themselves — the showcase
   carries "Entraînement hors ligne", the result screen "non comptabilisé" — so
   the strip is redundant there and costs vertical space where it is scarcest.

   The shell offset must go with it, or the padding syncBannerOffset applied
   leaves a gap where the banner used to be. That padding is an INLINE style,
   so only !important can override it.
   --------------------------------------------------------------------------- */
body.qzr-offline-session.modal-open::before,
body.qzr-offline-session.realtime-active::before {
  display: none !important;
}
body.qzr-offline-session.modal-open .cover-container,
body.qzr-offline-session.realtime-active .cover-container {
  padding-top: 0 !important;
}
