/* ─── Reading fonts (self-hosted; see web/public/fonts/) ───────
   font-display: swap → System renders immediately; the woff2 file
   takes over once loaded. Missing files degrade silently. */
@font-face {
  font-family: 'Inter';
  font-style: normal; font-weight: 100 900; font-display: swap;
  src: url('/public/fonts/inter-variable.woff2') format('woff2-variations');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal; font-weight: 100 800; font-display: swap;
  src: url('/public/fonts/jetbrainsmono-variable.woff2') format('woff2-variations');
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('/public/fonts/atkinson-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  font-style: normal; font-weight: 700; font-display: swap;
  src: url('/public/fonts/atkinson-700.woff2') format('woff2');
}
@font-face {
  font-family: 'Lexend';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('/public/fonts/lexend-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Lexend';
  font-style: normal; font-weight: 700; font-display: swap;
  src: url('/public/fonts/lexend-700.woff2') format('woff2');
}
@font-face {
  font-family: 'OpenDyslexic';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('/public/fonts/opendyslexic-400.woff2') format('woff2');
}
@font-face {
  font-family: 'OpenDyslexic';
  font-style: normal; font-weight: 700; font-display: swap;
  src: url('/public/fonts/opendyslexic-700.woff2') format('woff2');
}

html, body { margin: 0; padding: 0; height: 100%; background: #0A0A0A; color: #F2EFE8; font-family: 'Inter', ui-sans-serif, system-ui, sans-serif; }
/* height is declared twice on purpose — old browsers ignore the dvh line
   and keep 100vh; modern mobile uses 100dvh so the collapsing address bar
   can't clip the BottomTabBar. Cascades to the shells via their height:100%. */
#root { width: 100vw; height: 100vh; height: 100dvh; overflow: hidden; isolation: isolate; }
#boot-loader { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 12px; color: #6a6e73; font: 12px/1.4 'JetBrains Mono', monospace; letter-spacing: 1px; text-transform: uppercase; }
#boot-loader.hidden { display: none; }
/* Offline boot fallback — full-viewport, revealed by boot.js (removes [hidden])
   when React never mounts. The id+attr selector beats the flex display. */
#boot-offline { position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 10px; padding: 24px; background: #0A0A0A; color: #F2EFE8; text-align: center; font: 14px/1.5 'Inter', ui-sans-serif, system-ui, sans-serif; }
#boot-offline[hidden] { display: none; }
#boot-offline .boot-offline-sub { color: #9aa0a6; font-size: 12px; max-width: 320px; }
#boot-loader .pulse { width: 8px; height: 8px; border-radius: 50%; background: #B8924A; animation: pulse 1.4s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }

/* ─── Shared overlay / skeleton keyframes ──────────────────────
   Referenced from inline `animation:` props (CSP allows element-level
   style attrs). All auto-gated by the reduced-motion blocks at the
   bottom of this file — they zero animation-duration globally.
   - fadeIn        backdrop scrims (MobileDrawer/Overlay)
   - sheetUp       bottom-sheet entrance (Sheet)
   - drawerInRight right-anchored drawer entrance (MobileDrawer)
   - shimmer       Skeleton loading sweep */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes sheetUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes drawerInRight { from { transform: translateX(100%); } to { transform: translateX(0); } }
@keyframes shimmer { from { background-position: 150% 0; } to { background-position: -150% 0; } }
/* countBump — quick scale pop on the deck-tile ×N badge when the count
   changes (replayed via a changing React key). Auto-gated by reduce-motion. */
@keyframes countBump { 0% { transform: scale(1); } 40% { transform: scale(1.35); } 100% { transform: scale(1); } }
/* joystickPop — the CardJoystick action pad emanating from a tapped tile
   (scale-up + fade, rooted at the tap point via transform-origin). Auto-gated
   by reduce-motion below, so it lands instantly when motion is reduced. */
@keyframes joystickPop { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; } }

/* ─── vs-Mode duel event animations ────────────────────────────
   Small, quick, non-disruptive mount/change feedback for the duel board.
   Every element that uses these ALSO carries className="duel-anim", so two
   independent switches suppress them: the board's ⚙ Animations toggle (the
   [data-duel-anims="off"] kill-switch below) and reduce-motion (OS media query
   or the app's data-reduce-motion, via the global blocks at the bottom of this
   file). The kill-switch is scoped to .duel-anim so board-FEEL transitions
   (table tilt, hover-lift) are untouched — only the event animations stop.
   - duelCardIn  a card / chain link / pile-top mounting (stable React keys make
                 it fire only on genuinely-new elements — no diff engine needed).
   - duelPulse   a life-total tick, replayed via a changing key={life}.
   No animation-delay is used, so reduce-motion (which zeros duration, not delay)
   snaps cleanly to the final state with no hidden/flashing frame. */
@keyframes duelCardIn { from { opacity: 0; transform: translateY(6px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes duelPulse { 0% { transform: scale(1); } 45% { transform: scale(1.18); } 100% { transform: scale(1); } }
/* ⚙ Animations = off: stop only the duel's .duel-anim event animations; the
   board's tilt / hover transitions (not .duel-anim) keep working. */
[data-duel-anims="off"] .duel-anim { animation: none !important; }

/* ─── Skip-link ────────────────────────────────────────────────
   Off-screen until a keyboard user lands on it; pops into view
   on focus so they can jump past the chrome to the primary
   content. The selector matches the `<main id="content">`
   wrappers in shell/Shell.tsx. */
.skip-link {
  position: absolute; left: -9999px; top: 0;
  padding: 10px 14px; z-index: 10000;
  background: var(--accent, #B8924A); color: #0A0A0A;
  font: 600 13px/1 'Inter', ui-sans-serif, system-ui, sans-serif;
  text-decoration: none; border-radius: 0 0 4px 0;
}
.skip-link:focus { left: 0; outline: 2px solid #0A0A0A; outline-offset: -4px; }

/* ─── Skip-link target focus ───────────────────────────────────
   <main id="content"> has tabIndex={-1} so the skip link can
   land focus on it. Show a focus ring (clipped *inside* the box
   because <main> has overflow:hidden) so keyboard users see the
   jump succeed. Pointer focus stays quiet via :focus-visible. */
main#content:focus-visible {
  outline: 2px solid var(--focus-ring, var(--accent, #B8924A));
  outline-offset: -4px;
}

/* ─── Focus-visible ring ───────────────────────────────────────
   Every interactive atom (Btn, Chip, Switch, Input) opts in via
   className="fab-focus". `:focus-visible` keeps mouse clicks
   quiet — the ring only appears for keyboard / a11y nav.
   `--focus-ring` is set by tokens.tsx::applyAppearance — it
   tracks the accent on dark themes and swaps to near-black on
   light theme (saturated accents fail WCAG 3:1 on white). */
.fab-focus:focus-visible {
  outline: 2px solid var(--focus-ring, var(--accent, #B8924A));
  outline-offset: 2px;
  border-radius: 3px;
}

/* ─── Screen-reader-only utility ───────────────────────────────
   Visually hidden but exposed to AT. Used for the live-region
   announcer in shell/Shell.tsx and for icon-only button labels. */
.sr-only {
  position: absolute !important;
  width: 1px !important; height: 1px !important;
  padding: 0 !important; margin: -1px !important;
  overflow: hidden !important; clip: rect(0,0,0,0) !important;
  white-space: nowrap !important; border: 0 !important;
}

/* ─── Press feedback + scroll lock ─────────────────────────────
   Kill the grey iOS tap-flash and the 300 ms double-tap delay on every
   tappable element. `.fab-tap` / `.fab-tap-soft` add a spring-in on
   :active — opt-in per atom in Stage 3 (never on elements that already
   use `transform` for layout). The transition that animates the scale
   lives on the element (T.motion.tappable); the reduced-motion blocks
   below zero its duration, so the press becomes an instant state change. */
button, [role="button"], a, .fab-tap, .fab-tap-soft {
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.fab-tap:active { transform: scale(0.96); }
.fab-tap-soft:active { transform: scale(0.98); }

/* Background scroll-lock for open overlays. Toggled (ref-counted) by
   useScrollLock; nested overlays share the single class. */
html.fab-scroll-locked, html.fab-scroll-locked body { overflow: hidden; overscroll-behavior: none; }

/* ─── Reduced motion ───────────────────────────────────────────
   Two ways to opt in: the OS-level prefers-reduced-motion media
   query, or our explicit `data-reduce-motion="true"` on <html>
   (driven by the user's Settings → Reduce motion toggle). Either
   collapses animation + transition durations to ~instant. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
[data-reduce-motion="true"] *,
[data-reduce-motion="true"] *::before,
[data-reduce-motion="true"] *::after {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.01ms !important;
  scroll-behavior: auto !important;
}
