/* Small custom styles layered on top of Tailwind's CDN build.
   Kept intentionally minimal — utility classes in the HTML do most of the
   work; this file just covers things Tailwind's CDN JIT can't (keyframes,
   safe-area insets for iPhone, focus rings, scrollbar, theming). */

/* ── 2026-07 look-and-feel refresh ────────────────────────────────────────
   Each HTML page now carries a small tailwind.config block (right after the
   Tailwind CDN <script> tag) that remaps the "neutral" color scale to a
   cooler, slightly blue-tinted dark palette instead of flat gray — every
   existing bg-neutral-900 / border-neutral-800 / text-neutral-400 class
   picks this up automatically, so this one config change refreshes the
   whole app's base tone without editing any other markup. This file adds
   the rest of the "feels newer" pass on top: a proper typeface (Inter,
   loaded from Google Fonts — same "CDN loads at runtime in the user's own
   browser" pattern already used for Tailwind/Chart.js), real card
   elevation (soft shadows, not just a 1px border), and smoother global
   transitions. */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');

/* ── Theming ──────────────────────────────────────────────────────────────
   Dark is the default look. Light mode is implemented as a targeted
   override of the exact Tailwind utility classes already used throughout
   the app (bg-neutral-900, text-neutral-400, etc.) — this means NO HTML
   markup has to change to support theming; toggling <html data-theme="..">
   is all it takes. theme.js sets/reads this attribute and persists the
   choice in localStorage. */

:root {
  color-scheme: dark;
  --accent: #a3e635;
}

html, body {
  background-color: #0b0d12;
  min-height: 100%;
}

body {
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
  letter-spacing: -0.011em;
  padding-bottom: env(safe-area-inset-bottom, 0);
  padding-top: env(safe-area-inset-top, 0);
  transition: background-color 0.2s ease, color 0.2s ease;
}

* {
  -webkit-tap-highlight-color: transparent;
}

/* Real elevation instead of a flat 1px border — applied to the exact
   "card" background class already used everywhere in the app, so every
   card/tile across every page picks this up with zero markup changes.
   The light-theme override further below (same class name, later in this
   file, so it wins the cascade tie) sets its own softer shadow — this rule
   is the dark-mode default. */
.bg-neutral-900 {
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 10px 28px rgba(0, 0, 0, 0.35);
}

/* Smoother, slightly springier default motion for anything that already
   transitions on hover/active/focus (buttons, links, tap-scale, etc.) —
   most elements only specify transform in their own utility class, so
   giving the property itself a nicer global easing curve upgrades the
   whole app's "feel" without hunting down every element individually. */
a, button {
  transition: transform 0.15s cubic-bezier(0.2, 0.8, 0.2, 1), background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
}

[data-theme="light"] {
  color-scheme: light;
  --accent: #65a30d;
}
[data-theme="light"] html,
[data-theme="light"] body,
[data-theme="light"] .bg-neutral-950,
[data-theme="light"] .bg-neutral-950\/80,
[data-theme="light"] .bg-neutral-950\/90,
[data-theme="light"] .bg-neutral-950\/95 { background-color: #f5f5f7 !important; }

/* 2026-08 fix: the top bar (bg-neutral-950/80) and generate-workout.html's
   bottom action bar (bg-neutral-950/95) were missing from the rule above
   entirely — only the bare class and /90 were covered — so in light mode
   they kept their dark background while text-white above them flipped to
   near-black, making the top bar (and that bar) illegible. Every opacity
   variant actually used anywhere in the app is now listed explicitly. */

[data-theme="light"] .bg-neutral-900,
[data-theme="light"] .bg-neutral-900\/60,
[data-theme="light"] .bg-neutral-900\/70,
[data-theme="light"] .bg-neutral-900\/90,
[data-theme="light"] .bg-neutral-900\/95 {
  background-color: #ffffff !important;
  box-shadow: 0 1px 2px rgba(15, 15, 20, 0.04), 0 8px 20px rgba(15, 15, 20, 0.05);
}
/* Same fix, same reasoning: the bottom nav pill (bg-neutral-900/90) and the
   cross-page "session in progress" bar (bg-neutral-900/95) were also
   missing — the bottom bar was staying dark in light mode instead of
   switching with everything else. */

[data-theme="light"] .bg-neutral-800,
[data-theme="light"] .bg-neutral-800\/60 { background-color: #ececef !important; }

[data-theme="light"] .border-neutral-800 { border-color: #e5e5ea !important; }
[data-theme="light"] .border-neutral-700 { border-color: #d4d4da !important; }
[data-theme="light"] .border-neutral-500 { border-color: #a1a1aa !important; }

[data-theme="light"] .text-white,
[data-theme="light"] .text-neutral-100 { color: #0a0a0a !important; }
[data-theme="light"] .text-neutral-200 { color: #18181b !important; }
[data-theme="light"] .text-neutral-300 { color: #3f3f46 !important; }
[data-theme="light"] .text-neutral-400 { color: #52525b !important; }
[data-theme="light"] .text-neutral-500,
[data-theme="light"] .text-neutral-600 { color: #71717a !important; }
/* text-neutral-900 deliberately NOT overridden — it's always used as
   button text sitting on a lime background, correct in both themes. */

[data-theme="light"] .text-lime-400,
[data-theme="light"] .text-lime-400\/90 { color: #4d7c0f !important; }
[data-theme="light"] .text-emerald-400,
[data-theme="light"] .text-emerald-200 { color: #047857 !important; }
[data-theme="light"] .text-red-400,
[data-theme="light"] .text-red-200 { color: #b91c1c !important; }

[data-theme="light"] .bg-emerald-950 { background-color: #ecfdf5 !important; }
[data-theme="light"] .border-emerald-800 { border-color: #6ee7b7 !important; }
[data-theme="light"] .bg-red-950 { background-color: #fef2f2 !important; }
[data-theme="light"] .border-red-800 { border-color: #fca5a5 !important; }

[data-theme="light"] .bg-gradient-to-br.from-lime-500\/15.to-emerald-500\/10 {
  background-image: linear-gradient(to bottom right, #ecfccb, #d1fae5) !important;
}
[data-theme="light"] .border-lime-500\/30 { border-color: #bef264 !important; }

[data-theme="light"] ::-webkit-scrollbar-thumb { background: #d4d4d8; }
[data-theme="light"] .gradient-hero {
  background: radial-gradient(circle at 30% 20%, rgba(101,163,13,0.10), transparent 55%),
              radial-gradient(circle at 80% 80%, rgba(4,120,87,0.08), transparent 55%),
              #f5f5f7 !important;
}

/* ── Floating bottom nav pill ─────────────────────────────────────────── */
.bottom-nav-pill {
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45), 0 1px 0 rgba(255, 255, 255, 0.04) inset;
}
[data-theme="light"] .bottom-nav-pill {
  box-shadow: 0 12px 28px rgba(15, 15, 20, 0.12), 0 1px 0 rgba(255, 255, 255, 0.6) inset;
}
.bottom-nav-item {
  transition: color 0.15s ease, background-color 0.15s ease, transform 0.15s ease;
}
.bottom-nav-item-active {
  background: rgba(163, 230, 53, 0.12);
}
[data-theme="light"] .bottom-nav-item-active {
  background: rgba(101, 163, 13, 0.12);
}
.bottom-nav-item:active {
  transform: scale(0.94);
}

.settings-gear-btn:hover {
  transform: rotate(20deg);
}

/* ── Theme toggle switch (top bar + settings) ────────────────────────── */
.theme-toggle {
  position: relative;
  width: 52px;
  height: 30px;
  border-radius: 999px;
  background: #262626;
  border: 1px solid #404040;
  transition: background-color 0.2s ease;
  flex-shrink: 0;
}
[data-theme="light"] .theme-toggle { background: #e4e4e7; border-color: #d4d4d8; }
.theme-toggle .knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 24px;
  height: 24px;
  border-radius: 999px;
  background: #a3e635;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background-color 0.2s ease;
}
[data-theme="light"] .theme-toggle .knob {
  transform: translateX(22px);
  background: #65a30d;
}

/* ── Generic on/off toggle switch (Settings — daily quote pop-up, etc) ──
   Same box model as the theme-toggle switch above, but its knob position is
   driven by an .is-on class the calling page adds/removes itself, instead
   of the global data-theme attribute — so it can represent any independent
   per-feature on/off setting. */
.switch-toggle {
  position: relative;
  width: 52px;
  height: 30px;
  border-radius: 999px;
  background: #262626;
  border: 1px solid #404040;
  transition: background-color 0.2s ease, border-color 0.2s ease;
  flex-shrink: 0;
}
.switch-toggle.is-on {
  background: rgba(163, 230, 53, 0.2);
  border-color: rgba(163, 230, 53, 0.45);
}
.switch-toggle .knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 24px;
  height: 24px;
  border-radius: 999px;
  background: #737373;
  transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background-color 0.2s ease;
}
.switch-toggle.is-on .knob {
  transform: translateX(22px);
  background: #a3e635;
}

/* ── General polish ───────────────────────────────────────────────────── */
.tap-scale {
  transition: transform 0.1s ease;
}
.tap-scale:active {
  transform: scale(0.97);
}

@keyframes pop-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
.animate-pop-in {
  animation: pop-in 0.25s ease-out;
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
.animate-fade-in { animation: fade-in 0.2s ease-out; }

@keyframes pulse-ring {
  0% { box-shadow: 0 0 0 0 rgba(163, 230, 53, 0.35); }
  100% { box-shadow: 0 0 0 14px rgba(163, 230, 53, 0); }
}
.pulse-ring { animation: pulse-ring 1.6s ease-out infinite; }

input:focus, button:focus, textarea:focus, select:focus {
  outline: 2px solid var(--accent, #a3e635);
  outline-offset: 2px;
}

::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}
::-webkit-scrollbar-thumb {
  background: #404040;
  border-radius: 999px;
}

.gradient-hero {
  background: radial-gradient(circle at 30% 20%, rgba(163,230,53,0.18), transparent 55%),
              radial-gradient(circle at 80% 80%, rgba(52,211,153,0.12), transparent 55%),
              #0b0d12;
}

/* ── Landing page (index.html) background photo ──────────────────────────
   A real, professional workout photo instead of a plain gradient, rotating
   once per day (see the inline script in index.html — picked deterministically
   by day-of-year, so it's the same photo all day for every visitor and a
   different one tomorrow, not a fresh random pick on every reload). Sits on
   top of the page's existing .gradient-hero background on <body> — if the
   photo is slow to load or fails for any reason, that gradient is still
   there underneath and nothing looks broken, so no separate fallback markup
   is needed the way the login page's video hero requires. */
.landing-hero-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
}
.landing-hero-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  /* Slightly punchier/darker grade, same treatment as the login page's hero,
     so the overlay below doesn't have to work as hard to keep foreground
     text and cards legible on top of it. */
  filter: saturate(1.08) contrast(1.05) brightness(0.85);
  opacity: 0;
  transition: opacity 0.6s ease;
}
.landing-hero-img.is-loaded {
  opacity: 1;
}
.landing-hero-overlay {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 50% 10%, rgba(163, 230, 53, 0.12), transparent 45%),
    linear-gradient(180deg, rgba(11, 13, 18, 0.55) 0%, rgba(11, 13, 18, 0.72) 45%, rgba(11, 13, 18, 0.92) 100%);
}
[data-theme="light"] .landing-hero-overlay {
  background:
    radial-gradient(circle at 50% 10%, rgba(101, 163, 13, 0.14), transparent 45%),
    linear-gradient(180deg, rgba(247, 248, 250, 0.62) 0%, rgba(247, 248, 250, 0.8) 45%, rgba(247, 248, 250, 0.96) 100%);
}
/* Content needs to explicitly sit above the fixed background layer (z-index
   0) — mirrors .login-card's z-index:2 treatment on the login page. */
.landing-content {
  position: relative;
  z-index: 1;
}

/* ── Rest timer ring ──────────────────────────────────────────────────── */
.timer-ring circle {
  transition: stroke-dashoffset 1s linear;
}

/* ── Exercise "how to" expandable (text + embedded video) ─────────────── */
.howto-panel {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
}
.howto-panel.open {
  /* Generous enough for a 16:9 video at card width plus the how-to text
     below it, without needing per-card JS height measurement. */
  max-height: 620px;
}
.howto-chevron {
  transition: transform 0.2s ease;
}
.howto-chevron.open {
  transform: rotate(180deg);
}
.howto-video-wrap {
  position: relative;
  width: 100%;
  padding-top: 56.25%; /* 16:9 */
  background: #000;
  border-radius: 0.75rem;
  overflow: hidden;
}
.howto-video-wrap iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}
.howto-video-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 6px;
  color: #d4d4d8;
  cursor: pointer;
  background: radial-gradient(circle at center, rgba(163,230,53,0.12), transparent 70%), #171717;
}
.howto-video-placeholder .play-circle {
  width: 48px;
  height: 48px;
  border-radius: 999px;
  background: var(--accent, #a3e635);
  color: #0a0a0a;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  transition: transform 0.15s ease;
}
.howto-video-placeholder:hover .play-circle,
.howto-video-placeholder:active .play-circle {
  transform: scale(1.08);
}

/* ── Login page: full-bleed hero video background (2026-07) ─────────────
   Ed asked for "a real cool real video in the background of someone
   working out real hard... like a professional app." Sourced a short
   (10s, 3.5MB, landscape) royalty-free clip from Mixkit's Free stock video
   library — commercial use allowed, no attribution required (verified live
   via Mixkit's own license page this session, not assumed from training) —
   of someone working hard on battle ropes. It's hotlinked from Mixkit's own
   CDN and loaded at runtime in the visitor's own browser, the same
   "CDN-at-runtime" pattern already used for Tailwind/Supabase-js/Chart.js
   and the exercise how-to YouTube embeds elsewhere in this app, since this
   sandbox can't download and bundle a binary video file into the zip itself
   (same network restriction that limited how many exercise photos could be
   sourced in an earlier round). See login.html's inline script: if the clip
   fails to load, stalls, or the visitor has prefers-reduced-motion set, it's
   swapped out automatically for the dependency-free CSS/SVG pulse scene
   below (the ORIGINAL hero animation from before this round, kept and
   repurposed as the fallback rather than deleted) — the login page is never
   left with a blank background. */
.login-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: #0b0d12;
}
.login-bg-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Slightly punchier/darker grade so the overlay below doesn't have to work
     as hard to keep the form legible on top of it. */
  filter: saturate(1.08) contrast(1.05) brightness(0.9);
}
.login-bg-overlay {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 50% 12%, rgba(163, 230, 53, 0.14), transparent 45%),
    linear-gradient(180deg, rgba(11, 13, 18, 0.55) 0%, rgba(11, 13, 18, 0.74) 45%, rgba(11, 13, 18, 0.94) 100%);
}
[data-theme="light"] .login-bg-overlay {
  background:
    radial-gradient(circle at 50% 12%, rgba(101, 163, 13, 0.16), transparent 45%),
    linear-gradient(180deg, rgba(247, 248, 250, 0.6) 0%, rgba(247, 248, 250, 0.78) 45%, rgba(247, 248, 250, 0.95) 100%);
}

/* Fallback scene — same pulse-line/ring/floating-icon animation this page
   always had, just re-scaled to fill the whole viewport (it used to sit in
   a small header area above the form) so a failed video never looks like a
   downgrade, just a different mood. */
.login-bg-fallback {
  position: absolute;
  inset: 0;
}
.login-bg-fallback .pulse-line {
  stroke: var(--accent, #a3e635);
  stroke-width: 2.5;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 6px rgba(163, 230, 53, 0.55));
  stroke-dasharray: 340;
  stroke-dashoffset: 340;
  animation: pulse-draw 2.6s ease-in-out infinite;
}
@keyframes pulse-draw {
  0% { stroke-dashoffset: 340; }
  55% { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -340; }
}
.login-bg-fallback .pulse-line-wrap {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  opacity: 0.8;
}
.login-bg-fallback .hero-ring {
  position: absolute;
  top: 50%;
  left: 50%;
  border-radius: 999px;
  border: 1px solid rgba(163, 230, 53, 0.25);
  transform: translate(-50%, -50%);
}
[data-theme="light"] .login-bg-fallback .hero-ring { border-color: rgba(101, 163, 13, 0.25); }
.login-bg-fallback .hero-ring-1 {
  width: 60vmin; height: 60vmin;
  animation: ring-pulse 3.2s ease-in-out infinite;
}
.login-bg-fallback .hero-ring-2 {
  width: 85vmin; height: 85vmin;
  animation: ring-pulse 3.2s ease-in-out infinite 0.4s;
}
@keyframes ring-pulse {
  0%, 100% { opacity: 0.25; transform: translate(-50%, -50%) scale(1); }
  50% { opacity: 0.6; transform: translate(-50%, -50%) scale(1.04); }
}
.login-bg-fallback .float-icon {
  position: absolute;
  font-size: 26px;
  opacity: 0.5;
  animation: icon-float 5s ease-in-out infinite;
}
@keyframes icon-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-14px); }
}
@media (prefers-reduced-motion: reduce) {
  .login-bg-fallback .pulse-line,
  .login-bg-fallback .hero-ring,
  .login-bg-fallback .float-icon {
    animation: none;
  }
}

/* The floating "glass card" the login form now sits in, so it stays fully
   legible over the video/fallback background instead of text sitting
   directly on top of moving footage. */
.login-card {
  position: relative;
  z-index: 2;
  background: rgba(18, 21, 28, 0.62);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 28px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45), 0 1px 0 rgba(255, 255, 255, 0.06) inset;
  padding: 36px 28px;
}
[data-theme="light"] .login-card {
  background: rgba(255, 255, 255, 0.74);
  border-color: rgba(0, 0, 0, 0.06);
}

/* ── Exercise name chips (dashboard "today's workout" preview) ──────────
   Replaced the old per-exercise photo/icon badges (2026-07 look-and-feel
   pass, at Ed's request) with plain text chips — simpler, always-correct
   (no broken-image risk), and fits the cleaner/newer visual direction. */
.exercise-chip {
  display: inline-flex;
  align-items: center;
  padding: 5px 11px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 500;
  background: rgba(163, 230, 53, 0.1);
  color: var(--accent, #a3e635);
  border: 1px solid rgba(163, 230, 53, 0.18);
}
[data-theme="light"] .exercise-chip {
  background: rgba(101, 163, 13, 0.1);
  border-color: rgba(101, 163, 13, 0.2);
}

.stat-badge {
  transition: transform 0.15s ease;
}
.stat-badge:hover {
  transform: translateY(-2px);
}

/* ── Muscle picker (generate-workout.html, 2026-07) ──────────────────────
   Each muscle is a whole tappable card (icon + label + checkbox indicator)
   rather than a plain checkbox input, so the tap target is generous on
   mobile. Selected state is a border/background change plus filling in the
   little checkbox square — no native <input type="checkbox"> is used, so
   the styling stays fully in our control across browsers. */
.muscle-card {
  transition: border-color 0.15s ease, background-color 0.15s ease;
}
.muscle-card-selected {
  border-color: rgba(163, 230, 53, 0.5) !important;
  background: rgba(163, 230, 53, 0.08) !important;
}
.muscle-card-selected .muscle-card-icon {
  color: var(--accent, #a3e635);
}
.muscle-card-selected .muscle-card-check {
  background: var(--accent, #a3e635);
  border-color: var(--accent, #a3e635);
  position: relative;
}
.muscle-card-selected .muscle-card-check::after {
  content: "";
  position: absolute;
  left: 6px;
  top: 2px;
  width: 5px;
  height: 9px;
  border: solid #0b0d12;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

/* Calendar day cells (calendar.html) — 2026-07 "make it easier to read...
   modern and legible" pass. A subtle real shadow (matching this app's
   `.bg-neutral-900` card-elevation treatment elsewhere, which cal-cell can't
   just reuse directly since its background color varies by day state) so
   cells read as raised tiles rather than flat color blocks. */
.cal-cell {
  min-height: 0;
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 4px 10px rgba(0, 0, 0, 0.25);
}
[data-theme="light"] .cal-cell {
  box-shadow: 0 1px 2px rgba(15, 15, 20, 0.05), 0 3px 8px rgba(15, 15, 20, 0.05);
}

/* Day-cell activity dots (2026-07 "no emoji, show something that indicates
   a run/other activity" ask) — a tiny emoji at 9-10px renders blurry and is
   hard to read; small color-coded dots are the modern calendar-heatmap
   convention (Strava/Garmin-style) instead. Real per-session detail is one
   tap away in the day popover; the dot on the cell only needs to say
   "something happened here, and roughly what kind," which color does more
   legibly than illegible tiny iconography. Real, dedicated CSS classes for
   the positioning too (not Tailwind's absolute/inset utilities) — this
   project already hit one real bug from load-bearing Tailwind utility
   classes silently doing nothing if the Tailwind CDN script is ever slow or
   blocked (see cardio.html's celebration-overlay fix); worth avoiding here
   from the start rather than repeating it. */
.cal-cardio-badge,
.cal-custom-badge {
  position: absolute;
  bottom: -3px;
  width: 10px;
  height: 10px;
  border-radius: 999px;
  border: 2px solid #0b0d12;
}
[data-theme="light"] .cal-cardio-badge,
[data-theme="light"] .cal-custom-badge {
  border-color: #f5f5f7;
}
.cal-cardio-badge {
  right: -3px;
}
.cal-custom-badge {
  left: -3px;
  background: #a78bfa;
}
.cal-badge-run {
  background: #a3e635;
}
.cal-badge-walk {
  background: #38bdf8;
}
.cal-badge-hike {
  background: #fbbf24;
}
.cal-badge-custom {
  background: #a78bfa;
}
/* Small inline legend/popover dot — same color language as the cell
   badges above but sized for sitting inline with text rather than
   floating over a date number. */
.legend-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 999px;
  vertical-align: middle;
}

/* ── Calendar drag-and-drop (2026-07) ─────────────────────────────────────
   See calendar.html's pointer-event drag handling — a future workout/rest
   day can be dragged onto another future workout/rest day to swap them. */
.cal-cell-draggable {
  cursor: grab;
}
.cal-cell-drag-source {
  opacity: 0.35;
}
.cal-cell-drag-ghost {
  position: fixed;
  z-index: 70;
  pointer-events: none;
  opacity: 0.92;
  transform: scale(1.08);
  box-shadow: 0 16px 34px rgba(0, 0, 0, 0.45);
}
.cal-cell-drop-hover {
  outline: 2px dashed var(--accent, #a3e635);
  outline-offset: 2px;
}


/* ── Buddy chat ───────────────────────────────────────────────────────── */
.chat-bubble-user {
  align-self: flex-end;
  background: var(--accent, #a3e635);
  color: #0a0a0a;
  border-radius: 16px 16px 4px 16px;
}
.chat-bubble-buddy {
  align-self: flex-start;
  border-radius: 16px 16px 16px 4px;
}
@keyframes mic-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.12); }
}
.mic-listening { animation: mic-pulse 1s ease-in-out infinite; }

/* ── Animated buddy avatar ────────────────────────────────────────────────
   Replaces the old static mood-emoji icon (⚡/🔥/🤝/🌟) — this little blob
   mascot's shape/color/animation speed reflects real workout consistency
   over time (see js/buddyAvatar.js), and it never stops gently bouncing so
   it reads as "alive," not a static image. Speed is set per-instance via the
   --buddy-bounce custom property (faster at higher energy tiers). */
.buddy-avatar-anim {
  animation: buddy-bounce var(--buddy-bounce, 2.2s) ease-in-out infinite;
  transform-origin: center bottom;
}
@keyframes buddy-bounce {
  0%, 100% { transform: translateY(0) scale(1); }
  50% { transform: translateY(-3px) scale(1.04); }
}

/* 2026-08 ask: "update the buddy to change periodically when the user starts
   working out" — a livelier, faster "flexing" loop that overrides the normal
   idle bounce while a session (strength/cardio/custom) is actually running.
   Combines a quicker up-down bob with a slight side lean + squash/stretch so
   it visibly reads as "more energetic" rather than just "faster," without
   needing any JS interval to swap frames. */
.buddy-avatar-working {
  animation: buddy-working 1.3s ease-in-out infinite;
  transform-origin: center bottom;
}
@keyframes buddy-working {
  0%, 100% { transform: translateY(0) scale(1, 1) rotate(0deg); }
  25% { transform: translateY(-4px) scale(1.06, 0.96) rotate(-3deg); }
  50% { transform: translateY(-1px) scale(0.97, 1.05) rotate(0deg); }
  75% { transform: translateY(-4px) scale(1.06, 0.96) rotate(3deg); }
}

/* 2026-08 ask: "make him smile... when the user finishes a workout session"
   — a one-shot celebratory pop-in for the moment a session wraps up (used on
   the celebration/finish screens), distinct from the ongoing idle/working
   loops above since it only needs to play once, not loop forever. */
.buddy-avatar-celebrate {
  animation: buddy-celebrate 0.7s cubic-bezier(0.34, 1.56, 0.64, 1) 1;
  transform-origin: center bottom;
}
@keyframes buddy-celebrate {
  0% { transform: scale(0.7) translateY(6px); }
  60% { transform: scale(1.18) translateY(-6px); }
  100% { transform: scale(1) translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .buddy-avatar-anim,
  .buddy-avatar-working,
  .buddy-avatar-celebrate { animation: none; }
}

/* ── Sequential exercise gating (2026-07 Train tab) ──────────────────────
   Only one exercise is fully interactive at a time (the auto-picked next
   one, or whichever the user manually switched to — see workout.html's
   manualActiveId/pickActiveIndex); everything else not-yet-done renders
   locked/greyed, and everything done collapses into a small green summary.
   IMPORTANT: pointer-events is deliberately NOT disabled here (it used to
   be, before the 2026-07 "switch workouts" ask) — locked cards must stay
   tappable so the user can pick a different exercise to start instead of
   the default sequential one. The dimmed look alone (opacity) is enough to
   communicate "not your current focus" without blocking the tap. */
.exercise-card-locked {
  opacity: 0.55;
  cursor: pointer;
}
.exercise-card-locked:active {
  opacity: 0.75;
}
@keyframes exercise-complete-flash {
  0% { background-color: rgba(16, 185, 129, 0.22); }
  100% { background-color: transparent; }
}
.exercise-card-completing {
  animation: exercise-complete-flash 1.1s ease-out;
}

/* Small "tap to switch" badge shown on a locked/greyed exercise card so the
   tappability isn't a hidden affordance — 2026-07 "switch up the workouts
   suggested" ask. */
.switch-to-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(163, 230, 53, 0.12);
  color: var(--accent, #a3e635);
  border: 1px solid rgba(163, 230, 53, 0.22);
}
[data-theme="light"] .switch-to-badge {
  background: rgba(101, 163, 13, 0.12);
  border-color: rgba(101, 163, 13, 0.22);
}

/* ── Per-set logging rows (2026-07 Train tab rewrite) ─────────────────────
   Each exercise card now renders exactly as many rows as it has prescribed
   sets — one of three states per row. */
.set-row-pending {
  opacity: 0.4;
}
.set-row-active {
  border-color: rgba(163, 230, 53, 0.35) !important;
  background: rgba(163, 230, 53, 0.06);
}
.set-row-logged {
  background: rgba(16, 185, 129, 0.08);
  border-color: rgba(16, 185, 129, 0.22) !important;
  cursor: pointer;
}
/* The brief celebratory highlight-flash the instant a set is logged, paired
   with the sound + vibration in js/sound.js's playSetLoggedBlip()/
   vibrateSetLogged() — "highlight the whole set... getting them excited to
   move onto the next set." Removed via a timeout in workout.html after it
   plays once, so it never becomes a permanent look for a logged row. */
@keyframes set-logged-flash {
  0% { background-color: rgba(163, 230, 53, 0.45); transform: scale(1.015); }
  100% { background-color: rgba(16, 185, 129, 0.08); transform: scale(1); }
}
.set-row-just-logged {
  animation: set-logged-flash 1.1s ease-out;
}

/* Per-exercise percentage progress bar, shown above the exercise name —
   fills based on sets logged vs. sets prescribed. */
.exercise-progress-track {
  width: 100%;
  height: 5px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
[data-theme="light"] .exercise-progress-track {
  background: rgba(0, 0, 0, 0.08);
}
.exercise-progress-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--accent, #a3e635);
  transition: width 0.3s ease;
}

/* ── Post-workout mastery-status screen (2026-07) ─────────────────────────
   Per-exercise progress bar toward the next bronze/silver/gold tier. */
.mastery-progress-track {
  width: 100%;
  height: 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
[data-theme="light"] .mastery-progress-track {
  background: rgba(0, 0, 0, 0.08);
}
.mastery-progress-fill {
  height: 100%;
  border-radius: 999px;
  transition: width 0.4s ease;
}
.mastery-fill-none { background: #737373; }
.mastery-fill-bronze { background: #cd7f32; }
.mastery-fill-silver { background: #c0c0c0; }
.mastery-fill-gold { background: #e6b800; }

/* ── Post-workout streak screen (2026-07) ─────────────────────────────────
   Monday-Sunday week-at-a-glance, each day marked with a star once its
   workout is completed. Deliberately Monday-anchored — an intentional
   exception to the rest of the app's Sunday-anchored week grid (see
   calendar.html), matching how most people picture "this week." */
.week-star-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 8px 0;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(255, 255, 255, 0.06);
}
[data-theme="light"] .week-star-cell {
  background: rgba(0, 0, 0, 0.03);
  border-color: rgba(0, 0, 0, 0.06);
}
.week-star-cell-done {
  background: rgba(163, 230, 53, 0.1);
  border-color: rgba(163, 230, 53, 0.3);
}
.week-star-icon {
  font-size: 16px;
  opacity: 0.25;
}
.week-star-cell-done .week-star-icon {
  opacity: 1;
  animation: pop-in 0.3s ease-out;
}

/* ── Cross-page "session in progress" banner ─────────────────────────────
   See js/activeWorkout.js — shown on every page except the one that owns
   full controls for whatever kind of session (workout/cardio/custom
   activity) is mid-flight, so the timer reads as something the user is
   still actively inside of no matter which tab of the app they're looking
   at, not just something that quietly keeps running unseen. 2026-07: made
   `position: sticky` (was `relative`, sitting in normal document flow) so it
   stays pinned to the top of the viewport as the page scrolls, instead of
   scrolling away with the rest of the content — sticky needs no JS-measured
   padding hack the way `position: fixed` would, since the element still
   takes up its own space in the flow right up until it would scroll out of
   view. */
.active-workout-bar {
  position: sticky;
  top: 0;
  z-index: 45;
}

/* ── Workout screen's small timer header (2026-08 ask: "have the smaller
   timer stay on top no matter what when the user scrolls down") ──────────
   Same reasoning as .active-workout-bar just above: this header is built
   dynamically in workout.html's renderAll() (main.innerHTML = "" then
   re-appended every render), and real positioning behavior on dynamically-
   inserted content needs a hand-written rule here rather than a Tailwind
   `sticky`/`top-0`/`z-30` utility string set via className in JS — those
   utility classes only reliably get generated by Tailwind's CDN runtime
   for markup present at initial page load, not guaranteed for classes that
   only ever appear after an innerHTML rebuild. */
.workout-timer-header {
  position: sticky;
  top: 0;
  z-index: 30;
}

/* ── Cardio LIVE session stats — timer/distance/elevation (2026-08 ask:
   "when the user clicks start change the timer, distance and elevation font
   to have a black outside trim and have them pulsate") ────────────────────
   Applied only in start.html's renderCardioRunning() (the screen shown once
   Play has actually been tapped and tracking is live) — NOT on the earlier
   idle preview, which shows the same-looking numbers as placeholders before
   the session begins. `-webkit-text-stroke` (also supported unprefixed as
   `text-stroke` in newer engines) draws the "black outside trim" outline
   around each glyph; `paint-order: stroke` makes sure that outline renders
   behind the yellow fill instead of on top of it. A dedicated hand-written
   class rather than a Tailwind utility string, both because these text-
   stroke/animation properties have no Tailwind utility equivalent to begin
   with, and to stay consistent with this file's established pattern for
   anything applied to workout/start.html markup that's rebuilt via
   `innerHTML` on every render (see `.workout-timer-header` just above). The
   element is only ever updated via `.textContent` on each tick (never
   recreated), so the animation keeps running smoothly across ticks instead
   of restarting. */
.cardio-live-stat {
  -webkit-text-stroke: 1.5px #000;
  text-stroke: 1.5px #000;
  paint-order: stroke fill;
  animation: cardio-live-stat-pulse 1.6s ease-in-out infinite;
}
@keyframes cardio-live-stat-pulse {
  0%, 100% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.06);
    opacity: 0.88;
  }
}

/* ── Cardio "mini player" quick-control bar (2026-08-03 ask: "make that the
   primary screen and easier to get to... with a nice big pop up with stop
   and pause ability") ────────────────────────────────────────────────────
   Every positioning property here is genuinely load-bearing the same way
   `.relative` and `.hidden` above are — this bar needs to stay pinned near
   the bottom of the viewport regardless of Tailwind's CDN state (see
   js/activeWorkout.js's renderCardioQuickControl()). Confirmed the hard way
   during this round's own e2e testing: `position: fixed` alone, with the
   left/right/bottom OFFSETS left to Tailwind's `bottom-24 left-3 right-3`
   utility classes, rendered the button at its unpositioned default (stuck at
   its original document-flow spot, off the bottom of the visible page) the
   moment Tailwind's real CDN script wasn't the one supplying that CSS — so
   the actual offsets are real, explicit rules here too, not just the
   `position` keyword. The pulse only plays while the session is actually
   running (not paused) — a subtle, ongoing "this is live right now" cue,
   similar to a recording indicator. */
#cardio-quick-control-bar {
  position: fixed;
  bottom: 6rem; /* matches bottom-24 */
  left: 0.75rem; /* matches left-3 */
  right: 0.75rem; /* matches right-3 */
}
@keyframes cardio-quick-bar-pulse {
  0%, 100% {
    box-shadow: 0 0 0 0 rgba(163, 230, 53, 0.45), 0 12px 28px rgba(0, 0, 0, 0.4);
  }
  50% {
    box-shadow: 0 0 0 8px rgba(163, 230, 53, 0), 0 12px 28px rgba(0, 0, 0, 0.4);
  }
}
.cardio-quick-bar-live {
  animation: cardio-quick-bar-pulse 2.2s ease-out infinite;
}

/* ── Bottom-nav icons + the pulsating "Start" FAB (2026-08 ask: "make the
   icons... larger with no emojis" + "above them there will be a pulsating
   start button") ───────────────────────────────────────────────────────
   Same lesson as #cardio-quick-control-bar right above: don't lean on
   Tailwind's `fixed`/`w-7`/`h-7` utility classes alone for anything whose
   real pixel size or position actually matters (this test harness's
   stubbed Tailwind CDN never generates that utility CSS) — explicit rules
   here are the load-bearing ones; the Tailwind classes in the markup are
   only a (harmless, redundant) backup for the one real Netlify deploy path
   where the real CDN script does run. */
.nav-icon {
  width: 1.75rem;
  height: 1.75rem;
  display: block;
}
.nav-icon svg {
  width: 100%;
  height: 100%;
  display: block;
}
.start-fab {
  position: fixed;
  bottom: 5.75rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 4rem;
  height: 4rem;
  border-radius: 999px;
  background: #a3e635;
  color: #12151c;
}
.start-fab svg {
  width: 1.75rem;
  height: 1.75rem;
  margin-left: 0.15rem; /* optically centers the play triangle */
}
@keyframes start-fab-pulse {
  0%, 100% {
    box-shadow: 0 0 0 0 rgba(163, 230, 53, 0.55), 0 12px 28px rgba(0, 0, 0, 0.35);
  }
  50% {
    box-shadow: 0 0 0 14px rgba(163, 230, 53, 0), 0 12px 28px rgba(0, 0, 0, 0.35);
  }
}
.start-fab {
  animation: start-fab-pulse 2s ease-out infinite;
}

/* ── Start cockpit scroll fix (2026-08 ask: "after user clicks play... be
   able to move down to see the pause, finish buttons") ────────────────────
   #start-root is `fixed inset-0` (see start.html), which takes it out of the
   page's normal document scrolling entirely. That's fine for the idle and
   cardio screens, whose content is always a fixed, bounded size — but the
   strength cockpit's exercise list grows with however many exercises are in
   today's workout, and on a long workout (or a smaller phone screen) the
   content ran taller than the visible viewport with no way to reach the
   Pause/Resume/Finish buttons at the bottom: no scrollbar, no touch-scroll,
   nothing. Same root cause class as .start-fab above — real Tailwind's
   `overflow-y-auto` utility would handle this fine at deploy time, but
   anything whose exact behavior is load-bearing gets an explicit rule here
   too, since this test harness's stubbed Tailwind CDN never generates real
   utility CSS. Making #start-root itself the one scroll container (rather
   than nesting a second scrolling region inside it) means every screen
   rendered into it — including the shared finish editor, which has its own
   several fields and can also run taller than one screen — gets the same
   fix for free.

   The `fixed`/`inset-0` half of #start-root's own markup classes had the
   exact same gap that made the fix necessary to write explicitly here in
   the first place: with no real Tailwind CSS behind them, #start-root was
   never actually pinned to the viewport's height in this test harness at
   all — it just sat in normal document flow sized to whatever its content
   needed, which is how the overflow went undetected until now. Explicit
   rules for the positioning too, not just the new overflow behavior.

   Making #start-root genuinely `position: fixed` (for the first time, in
   this test harness) also means it now genuinely paints as a positioned
   layer over the rest of the page — which surfaced a second, pre-existing
   gap: start.html's own modal overlays (the activity picker, settings
   sheet, and confirm dialog) are ALSO built with nothing but Tailwind's
   `fixed inset-0 z-[N]` classes and no CSS fallback, so with #start-root
   now really fixed and them still not, #start-root's content sat on top of
   them and swallowed their clicks. .start-modal-overlay/.start-confirm-overlay
   below give those the same explicit treatment, at explicit z-index values
   above #start-root's, matching each other's relative order (confirm
   dialogs like "finish with exercises left?" sit above the activity/settings
   sheets, same as their z-[80] vs z-[70] Tailwind classes intend). */
#start-root {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.start-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 70;
}
.start-confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 80;
}

/* The strength cockpit's Pause/Resume/Finish buttons stick to the bottom of
   the screen as the exercise list scrolls underneath them, instead of
   scrolling out of reach along with it — same sticky pattern as
   .active-workout-bar above (search this file), just pinned to the opposite
   edge. The solid background keeps exercise cards from showing through as
   they scroll past underneath; the shadow gives it a little visual lift off
   the content behind it, like a real docked toolbar. */
#strength-footer {
  position: sticky;
  bottom: 0;
  background: #0b0d12; /* neutral-950, matches the cockpit's own background */
  box-shadow: 0 -10px 16px -6px rgba(0, 0, 0, 0.55);
}

/* ── Exercise card accent stripe ──────────────────────────────────────────
   Replaces the old per-exercise photo/icon badges (removed 2026-07 at Ed's
   request — see workout.html's MUSCLE_ACCENT map) with a simple colored
   left edge, using the same color as that exercise's muscle-group label. */
.exercise-card {
  position: relative;
  border-left: 3px solid var(--exercise-accent, #a3e635);
}

/* ── Dormant: real exercise photos (with icon fallback) ──────────────────
   NOT currently used anywhere in the app (removed 2026-07 — Ed asked for
   "picture icons for each workout" to be removed while the look-and-feel
   is reworked). Left here, unused, in case a future session wants to bring
   real photos back — js/exercisePhotos.js and js/exerciseIcons.js still
   have the underlying data/logic, just disconnected from every page.
   Each exercise's icon badge could hold either a real photo (img) or an
   emoji fallback (span) — the img had an onerror handler that hid itself
   and revealed the emoji sibling, so a broken/unreachable photo URL never
   showed the browser's broken-image glyph, it just quietly became the icon. */
.exercise-visual {
  position: relative;
  overflow: hidden;
  background: rgba(163, 230, 53, 0.12);
}
[data-theme="light"] .exercise-visual { background: rgba(101, 163, 13, 0.12); }
.exercise-visual img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.exercise-visual-lg {
  width: 100%;
  height: 128px;
  border-radius: 14px;
  margin-bottom: 10px;
}
.exercise-visual-lg .exercise-photo-caption {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: linear-gradient(to top, rgba(0,0,0,0.65), transparent);
  color: #e5e5e5;
  font-size: 10px;
  padding: 3px 8px;
}

/* ── Mastery badges (bronze / silver / gold, per exercise) ───────────────
   Sits next to an exercise's name once it's been performed at least once;
   pop-in animation gives newly-earned tier upgrades a little celebration
   moment without needing a whole separate toast. */
.mastery-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 10px;
  font-weight: 600;
  padding: 2px 7px;
  border-radius: 999px;
  letter-spacing: 0.02em;
  animation: pop-in 0.3s ease-out;
}
.mastery-bronze { background: rgba(205, 127, 50, 0.18); color: #cd7f32; }
.mastery-silver { background: rgba(192, 192, 192, 0.2); color: #c0c0c0; }
.mastery-gold { background: rgba(255, 215, 0, 0.18); color: #e6b800; }
[data-theme="light"] .mastery-gold { color: #a67c00; }

/* ── Workout-complete celebration screen ──────────────────────────────────
   Full-screen overlay shown right after finishing a workout — confetti
   canvas sits behind the content at a lower z-index. */
@keyframes celebrate-in {
  from { opacity: 0; transform: scale(0.92) translateY(12px); }
  to { opacity: 1; transform: scale(1) translateY(0); }
}
.celebration-card {
  animation: celebrate-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes fire-bounce {
  0%, 100% { transform: translateY(0) rotate(-4deg); }
  50% { transform: translateY(-6px) rotate(4deg); }
}
.celebration-fire {
  display: inline-block;
  animation: fire-bounce 1.1s ease-in-out infinite;
}
#confetti-canvas {
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
}

/* ── General "make it fancy" animation utilities, used throughout ───────── */
@keyframes shimmer {
  0% { background-position: -200px 0; }
  100% { background-position: calc(200px + 100%) 0; }
}
.skeleton-shimmer {
  background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.08) 37%, rgba(255,255,255,0.03) 63%);
  background-size: 400% 100%;
  animation: shimmer 1.6s ease-in-out infinite;
  border-radius: 12px;
}

/* Staggered list entrance: apply .stagger-in to a container, each direct
   child fades/slides up in sequence rather than popping in all at once. */
.stagger-in > * {
  opacity: 0;
  animation: pop-in 0.35s ease-out forwards;
}
.stagger-in > *:nth-child(1) { animation-delay: 0.02s; }
.stagger-in > *:nth-child(2) { animation-delay: 0.07s; }
.stagger-in > *:nth-child(3) { animation-delay: 0.12s; }
.stagger-in > *:nth-child(4) { animation-delay: 0.17s; }
.stagger-in > *:nth-child(5) { animation-delay: 0.22s; }
.stagger-in > *:nth-child(6) { animation-delay: 0.27s; }
.stagger-in > *:nth-child(n+7) { animation-delay: 0.3s; }

/* A subtle one-time "shine" sweep, used on stat tiles/rings right after they
   update (e.g. the Move ring closing, a streak going up) to draw the eye. */
@keyframes shine-sweep {
  from { transform: translateX(-120%) skewX(-15deg); }
  to { transform: translateX(220%) skewX(-15deg); }
}
.shine-once {
  position: relative;
  overflow: hidden;
}
.shine-once::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 30%; height: 100%;
  background: linear-gradient(120deg, transparent, rgba(255,255,255,0.18), transparent);
  animation: shine-sweep 1.2s ease-out 0.15s 1;
}

/* Gentle continuous glow used on the big "Continue"/primary CTA on the
   celebration screen so it reads as the obvious next tap. */
@keyframes cta-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(163, 230, 53, 0.35); }
  50% { box-shadow: 0 0 0 10px rgba(163, 230, 53, 0); }
}
.cta-glow { animation: cta-glow 2s ease-in-out infinite; }

/* Barcode-scan camera overlay (2026-08 ask: "add a red barcode scanning line
   going up and down within the camera feed so they know where to scan").
   A thin glowing red line sweeps the full height of the video feed on a
   loop, independent of the phone/barcode's own orientation — the scanner
   backends (native BarcodeDetector / ZXing) already read a code held at any
   angle, so this line is purely a visual "aim here" guide, not a hint the
   barcode itself must be horizontal. */
@keyframes barcode-scan-sweep {
  0% { top: 6%; opacity: 0; }
  8% { opacity: 1; }
  92% { opacity: 1; }
  100% { top: 94%; opacity: 0; }
}
.barcode-scan-line {
  position: absolute;
  left: 6%;
  right: 6%;
  height: 2px;
  background: #f87171;
  box-shadow: 0 0 8px 2px rgba(248, 113, 113, 0.8);
  border-radius: 999px;
  animation: barcode-scan-sweep 2.2s ease-in-out infinite;
}

/* ── Cardio route map (2026-07, "show a map of where the user ran") ──────
   Leaflet needs its container to have a real, non-zero height before it
   measures/initializes — these fixed heights are that. Rounded + clipped so
   the map tile grid respects the app's usual card corner-radius instead of
   rendering as a hard rectangle. */
.route-map {
  width: 100%;
  height: 220px;
  border-radius: 14px;
  overflow: hidden;
  background: #1b1f29;
}
.route-map-inline {
  height: 180px;
  margin-top: 8px;
}
/* Leaflet's own zoom-control/attribution chrome is styled for a light page
   by default — nudge it to sit comfortably on this app's dark cards without
   fighting Leaflet's own layout. */
.route-map .leaflet-control-attribution {
  background: rgba(11, 13, 18, 0.75);
  color: #8891a3;
  font-size: 9px;
}
.route-map .leaflet-control-attribution a { color: #a3e635; }
.route-map .leaflet-bar a {
  background: #1b1f29;
  color: #e5e5e5;
  border-color: #3a4152;
}
.route-map .leaflet-bar a:hover { background: #262b38; }

/* ── Cardio completion "party mode" — large map + stat overlay (2026-07) ──
   A taller variant of .route-map for the post-run/walk celebration screen,
   plus a semi-transparent stat bar that overlays its bottom edge (Miles/
   Moving Time/Pace, then Elevation Gain/City on a second row), and a dark
   gradient fallback box (same footprint) for a session with no tracked
   route to show a map of. Bumped from 300px to 380px (2026-07 "make the map
   larger" ask) — the overlay sits on TOP of this height via absolute
   positioning, so it doesn't need extra room of its own. */
/* position: relative + an explicit (non-auto) z-index here is load-bearing,
   not decorative: Leaflet's own internal panes (tile pane, marker pane,
   popup pane, etc.) carry explicit positive z-index values (200-700+). If
   this container doesn't establish its own stacking context, those panes
   "leak" past it and get compared directly against .route-map-stat-overlay
   below in the OUTER stacking context — and since z-index:auto paints
   beneath any explicit positive z-index, Leaflet's tiles would win and
   silently paint over the overlay, hiding it entirely (only visible if a
   viewer drags the map and tiles happen to shift). Giving this element its
   own z-index seals Leaflet's panes inside it so they can never escape. */
.route-map-large {
  position: relative;
  z-index: 0;
  width: 100%;
  height: 380px;
  border-radius: 14px;
  overflow: hidden;
  background: #1b1f29;
}
.route-map-fallback {
  width: 100%;
  height: 380px;
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 72px;
  background: linear-gradient(160deg, #1b1f29 0%, #12151c 100%);
}
/* The overlay's positioning context — deliberately a real, dedicated CSS
   class rather than leaning on Tailwind's ".relative" utility for
   something this load-bearing. Tailwind here is loaded from a CDN
   <script> at runtime (see "Why static HTML with no build step" in the
   project doc); if that script is ever slow, blocked, or fails to load for
   a real visitor, ".relative" would silently do nothing and this
   `position: absolute; bottom: 0` overlay below would anchor to some
   faraway ancestor instead of this box — this class guarantees the
   positioning context exists regardless of Tailwind's CDN state. */
.route-map-wrap {
  position: relative;
  margin-bottom: 1rem;
}
.route-map-stat-overlay {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 5;
  background: rgba(11, 13, 18, 0.82);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  border-bottom-left-radius: 14px;
  border-bottom-right-radius: 14px;
}

/* Same "don't silently rely on the Tailwind CDN state" reasoning as
   .relative above, for the one utility class this app actually toggles via
   JS at runtime (classList.add/remove("hidden") — e.g. the share editor's
   photo/spinner swap, error banners, modals). Tailwind's own `.hidden` rule
   says the same thing (display:none) once the CDN script has run; this is a
   real, load-bearing fallback for the brief window before it has (or, in
   the e2e test harness, for good — that harness stubs the Tailwind CDN
   script with a no-op so its generated utility CSS never exists at all).
   No responsive `hidden md:flex`-style classes are used anywhere in this
   app, so an unconditional !important here has no combo to conflict with. */
.hidden {
  display: none !important;
}
