@import "./tokens.css";

:root {
  color-scheme: dark;
  --page-bg: #05050d;
  --panel: #151a1c;
  --line: rgba(255, 255, 255, 0.12);
  --text: #f1f4f8;
  --muted: #aeb6ba;
  --accent: #48d8ec;

  /* Neon pass (2026-08-03): reused across every real screen via this file,
     rather than the accent-tinted rgba() literals (rgba(72,216,236,0.12) etc.)
     that were previously copy-pasted with slightly different opacities on
     every page's own <style> block. --accent is #48d8ec = 72,216,236. */
  --accent-rgb: 72, 216, 236;
  --accent-wash: rgba(var(--accent-rgb), 0.08);   /* faint fill, e.g. a stat pill */
  --accent-tint: rgba(var(--accent-rgb), 0.12);   /* active nav / selected row */
  --accent-glow: rgba(var(--accent-rgb), 0.35);   /* outer glow on a hero card only */
  --panel-2: #0c1116;   /* deeper panel, e.g. nested rows inside a card */
  --danger: #ff8a8a;
  --warn: #ffd166;
  --success: #3ecf7a;
  --violet: #b48cff;
  --motion-fast: 150ms;
  --motion-med: 260ms;
  --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
}

* { box-sizing: border-box; }

/* The UA's own `[hidden] { display: none }` loses to ANY author rule that sets
   `display`. The deciding factor is cascade ORIGIN, not specificity: normal
   author declarations outrank the UA stylesheet before specificity is
   consulted. Measured - `.pill { display: inline-flex }` at 0,1,0, the same
   specificity as the UA rule, kept admin/index.html's «В эфире» badge on
   screen permanently. `a.edit-btn { display: block }` in profile.html (0,1,1) is
   one instance, not the bar; reading it as the bar implies a plain class rule
   is safe, and it is not.

   `hidden` is an accessibility contract, not a hint - an element announced as
   absent must not still take space and clicks - so it has to outrank whatever
   the page declares, and only !important does that. Restating this without
   !important would change nothing: it would still be a normal author rule
   against another normal author rule.

   Global here rather than per-page because every frame.css page inherits the
   same collision - including the 26 pages under frontend/admin/, which load
   this file too. js/qr-scanner.js:35 injects `#<overlay>[hidden] { display:
   none }` for the same reason and is still load-bearing there, since that
   overlay's own `#<overlay> { display: flex }` would otherwise win.

   THE COST, and it is real: !important also defeats a *deliberate* re-show.
   admin/admin-ui.css:343 re-shows the FAB create panel above 760px
   (`@media (min-width: 761px) { .create-panel[hidden] { display: block } }`)
   and this rule broke it until that one was made `!important` too, on
   lane/hidden-attr-specificity. Anything that genuinely needs a `hidden`
   element visible must now say `!important` and win on specificity among
   important declarations (0,2,0 beats this rule's 0,1,0) - or, better, use a
   real state class instead of overriding `hidden`.
   Pinned in tests-frontend/overlay-frame.spec.js. */
[hidden] { display: none !important; }

html, body { margin: 0; min-height: 100%; }
/* System-font stack (2026-08-03): what every real screen's own <style> block
   was independently redeclaring anyway (login, routes, profile, leaderboard,
   exercise-camera-test, admin/profile) - promoted here once instead of
   staying duplicated seven times. Arial/Helvetica remain as the tail
   fallback, so anything not yet touched still renders the same. */
body { background: var(--page-bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif; }
a { color: inherit; }
button, a, input, select, textarea { -webkit-tap-highlight-color: transparent; }
button, a { touch-action: manipulation; }
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* One glow reserved for a screen's single "hero" card (rank/mission/streak) -
   deliberately not applied broadly. The mockups that prompted this pass used
   the same glow on every card, which flattened hierarchy instead of creating
   it; a screen makes at most one thing glow. Bounded blur/no spread so it's
   cheap to paint - see nft-market-achievements.html's header comment for why a heavier
   gradient+shadow combination on this app was a measured performance
   regression on a budget device, not just a style preference. */
.glow-card { box-shadow: 0 0 0 1px var(--line), 0 0 28px -10px var(--accent-glow); }

/* Canonical bottom tab bar, rendered by frontend/js/nav.js (customer) and
   frontend/admin/js/admin-shell.js (admin's own admin-bottom-nav uses its
   own admin.css rules, this is the customer one). Previously hand-copied
   into 10 separate pages' own <style> blocks with two competing looks (a
   frosted-blur version and a solid-accent version); this is the one
   canonical look, defined once. */
.bottom-nav-icons {
  position: fixed; left: 50%; transform: translateX(-50%);
  /* Sits above the home indicator on a notched phone instead of under it. */
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  /* The 24px is the bar's own margin from the screen edges; the insets are the
     notch, which in LANDSCAPE is at the side rather than the top. Without them
     a centred bar still runs under it on a rotated phone. Both fall back to
     0px, so this is the same `min(--frame-chrome, 100% - 24px)` as before on
     every page that has not opted into safe areas. */
  width: min(
    var(--frame-chrome),
    calc(100% - 24px - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px))
  );
  display: flex; justify-content: space-between; align-items: stretch;
  background: rgba(15, 20, 23, 0.75); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 10px 6px;
  /* Above Leaflet. A .leaflet-container is position:relative with z-index:auto,
     so it creates NO stacking context and its children compete directly with
     everything else in the root one - .leaflet-top/.leaflet-bottom sit at 1000
     and the panes at 200-700. With no z-index here the nav lost to any map it
     overlapped and simply vanished behind it. */
  z-index: 1100;
}
/* Each tab takes an equal share of the bar rather than being sized by its own
   16px glyph plus fixed padding. With `space-around` and content-width items
   the bar degraded as tabs were added: at eight items on a 360px phone the
   gaps had collapsed to 2px, so the icons read as one crowded strip and the
   tappable area was 38px wide - under the 44px minimum - with no spacing
   between neighbouring targets. `flex: 1` makes the share a function of the
   item count, so the bar stays evenly spaced at seven items or nine, and the
   target grows to the full slot instead of hugging the glyph. */
.bottom-nav-icons a {
  flex: 1; min-width: 0;
  display: grid; place-items: center; text-decoration: none;
  padding: 8px 4px; border-radius: 999px; color: var(--muted); opacity: 0.55;
  transition: opacity var(--motion-fast) ease, background var(--motion-fast) ease, color var(--motion-fast) ease, transform var(--motion-fast) ease;
}
.bottom-nav-icons a.nav-active { background: var(--accent-tint); color: var(--accent); opacity: 1; }
.bottom-nav-icons a:active { transform: translateY(1px) scale(0.96); }

.index-shell { width: min(1180px, calc(100vw - 32px)); margin: 0 auto; padding: 28px 0 48px; }
.index-head { display: flex; align-items: end; justify-content: space-between; gap: 18px; margin-bottom: 24px; }
.index-head h1 { margin: 0; font-size: 28px; line-height: 1.1; }
.index-head p { margin: 8px 0 0; color: var(--muted); font-size: 14px; }
.frame-count { color: var(--accent); font-weight: 700; white-space: nowrap; }
.frame-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 18px; }
.frame-tile { display: grid; gap: 10px; text-decoration: none; }
.thumb-wrap { display: grid; place-items: center; min-height: 260px; border: 1px solid var(--line); background: #0a0d12; overflow: hidden; }
.thumb-wrap img { display: block; width: 100%; height: 100%; object-fit: contain; }
.tile-name { margin: 0; font-size: 13px; line-height: 1.25; color: var(--text); }
.tile-meta { margin: 0; color: var(--muted); font-size: 12px; }

.frame-page { min-height: 100vh; display: grid; place-items: start center; overflow: auto; }
/* --frame-height defaults to the viewport, and the real (data-driven) screens
   deliberately don't set it (2026-08-03). It exists for the static Figma-frame
   exports, which render one fixed-size image and genuinely need a fixed box.
   A screen whose height depends on live data does not: pinning it to the
   frame's original pixel height left a screen with less content than the
   mockup scrolling into hundreds of pixels of empty page below the last card
   - reported from an Android device as "white space", and reproduced here as
   content ending at 929px inside a 1900px-tall canvas. */
.frame-canvas { width: min(100vw, var(--frame-width)); min-height: var(--frame-height, 100vh); background: #05050d; }
.frame-canvas img { display: block; width: 100%; height: auto; }

/* Safe areas, and the meta tag that makes them exist.

   `env(safe-area-inset-*)` resolves to its FALLBACK - 0px everywhere it is
   written in this repo - unless the page's viewport meta says
   `viewport-fit=cover`. On 2026-08-21 only 7 of 153 pages said it, while 16
   places across six stylesheets computed with these insets, so every one of
   those calculations was quietly doing nothing. The bottom nav below is the
   clearest case: `bottom: calc(14px + env(safe-area-inset-bottom, 0px))` and a
   comment claiming it "sits above the home indicator", which on a notched
   iPhone it did not - the inset was 0 and the bar sat under the indicator.

   That is invisible in a desktop browser at any width, which is why resizing
   Chrome never reproduced the client's report and why this went unfound through
   two responsive lanes.

   These rules are safe on pages that have NOT opted in: env() falls back to 0px
   and the padding is a no-op. So the inset handling can live here, shared by
   every .frame-canvas page, while the meta tag stays a per-page opt-in - which
   matters, because opting a page in makes it paint under the notch and it must
   have somewhere for that content to go first. Any page declaring a fixed
   --frame-height and not opting in is left alone, and env()=0 means it is.
   (The ~30 static Figma exports this clause was written for left frontend/ on
   2026-08-23 — Q105 — and are no longer served at all.)

   Padding on the canvas rather than on each screen's own shell: .today-shell,
   .profile-shell and .calendar-shell each hardcode a bottom padding chosen for
   a 14px nav offset (108/100/96px), and two of the three live in files this
   lane does not own. One rule on their shared parent moves all of them, and
   keeps working for a screen added later that picks its own number. */
.frame-canvas {
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}

/* The fluid-on-a-phone half of this rule lives in tokens.css, beside the token
   it overrides, because interactive.css's 16 auth screens import tokens.css and
   do NOT load this file - scoping it here reached only the pages with
   frame.css, and most participant pages do not even carry .frame-page on their
   <body>. See the long note in tokens.css.

   BOTH terms above are viewport lengths, and that is not incidental. An attempt
   to write this as `min(100%, var(--frame-width))` - so that a canvas inside a
   PADDED body could not overflow it - broke two pages, and the mechanism is
   worth knowing because it is invisible in the declaration:

   .frame-page is `display: grid` with one auto-sized track. A percentage width
   on a grid item cannot be resolved while the track is still being sized, so it
   is treated as `auto` for intrinsic sizing, and the track sizes to the item's
   CONTENT instead of to the viewport. Measured 2026-08-21 at a 390px viewport
   with a competition card on screen: routes.html's track grew to 542px, the
   canvas kept its 390px used width and was CENTRED in that track at x=76, and
   the document scrolled sideways to 466px. A definite length contributes its
   own value to the track and has no such circularity.

   The padded-body case that prompted the percentage is real but has exactly one
   instance - join-competition.html - and it carries an inline `--frame-width: 360px`, which
   already keeps it inside its own padding. Checked across every page carrying
   .frame-page: no other one pads its body horizontally. */

/* A full-height overlay that stays inside the phone frame.

   `position: fixed; inset: 0` covers the viewport, which is correct on a phone
   and wrong everywhere else: this app renders as a ~390px column
   (`.frame-canvas`), so on a laptop the camera, QR and prompt overlays spilled
   across the whole window while the app they belonged to sat in a narrow strip
   — reported as popups "not within the size of the mobile screen + floating".

   The geometry is the one `.bottom-nav-icons` already uses (centred by
   `left: 50%` + `translateX(-50%)` rather than by a flex parent, because these
   are fixed and have no layout parent to be centred by), so the overlay and the
   nav underneath it agree on where the app's edges are. It tracks that nav
   (`--frame-chrome`) rather than `.frame-canvas`'s narrower content column
   (`--frame-width`), deliberately: the nav is the widest chrome on screen, and
   an overlay narrower than the bar it covers reads as a misaligned card rather
   than as the app's own surface.

   Both tokens are defined in tokens.css and inherited from `:root`, so an
   overlay appended to <body> - which is every one of them, and therefore has no
   `.frame-canvas` ancestor to inherit from - still resolves them. That was the
   reason this width used to be a literal: the value it needed to match was set
   inline on `.frame-canvas` and was out of scope by construction. It no longer
   is, so the literal is gone. */
.frame-overlay {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(100vw, var(--frame-overlay-width));
}

.rank-card,
.streak-card,
.calendar-card,
.stat-card,
.mission-card,
.map-card,
.challenge-card,
.event-card,
.history-item,
.route-card,
.comp-card,
.locate-card,
.duel-hero,
.friend-req-row,
.pg-card,
.pg-hero {
  transition:
    border-color var(--motion-med) ease,
    background-color var(--motion-med) ease,
    box-shadow var(--motion-med) ease,
    transform var(--motion-fast) ease;
}

.rank-card:hover,
.streak-card:hover,
.calendar-card:hover,
.mission-card:hover,
.map-card:hover,
.challenge-card:hover,
.event-card:hover,
.history-item:hover,
.route-card:hover,
.comp-card:hover,
.locate-card:hover,
.duel-hero:hover,
.friend-req-row:hover,
.pg-card:hover,
.pg-hero:hover {
  border-color: rgba(var(--accent-rgb), 0.32);
}

.progress-fill,
.bal-fill,
.streak-ring {
  transition: width 520ms var(--ease-out), background 520ms var(--ease-out);
}

@keyframes cps-rise-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes cps-soft-pulse {
  0%, 100% { box-shadow: 0 0 0 1px var(--line), 0 0 22px -12px var(--accent-glow); }
  50% { box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.28), 0 0 34px -10px var(--accent-glow); }
}

.rank-card,
.streak-card,
.mission-board,
.pg-hero,
.duel-hero {
  animation: cps-rise-in 420ms var(--ease-out) both, cps-soft-pulse 3.2s ease-in-out 480ms infinite;
}

.calendar-shell > *,
.athlete-shell > *,
.routes-shell > *,
.pg-shell > * {
  animation: cps-rise-in 360ms var(--ease-out) both;
}

.calendar-shell > *:nth-child(2),
.athlete-shell > *:nth-child(2),
.routes-shell > *:nth-child(2),
.pg-shell > *:nth-child(2) { animation-delay: 45ms; }
.calendar-shell > *:nth-child(3),
.athlete-shell > *:nth-child(3),
.routes-shell > *:nth-child(3),
.pg-shell > *:nth-child(3) { animation-delay: 90ms; }
.calendar-shell > *:nth-child(4),
.athlete-shell > *:nth-child(4),
.routes-shell > *:nth-child(4),
.pg-shell > *:nth-child(4) { animation-delay: 135ms; }
.calendar-shell > *:nth-child(n+5),
.athlete-shell > *:nth-child(n+5),
.routes-shell > *:nth-child(n+5),
.pg-shell > *:nth-child(n+5) { animation-delay: 170ms; }

@media (hover: hover) {
  .bottom-nav-icons a:hover,
  .route-filter:hover,
  button:hover,
  .map-btn:hover,
  .pg-btn:hover,
  .duel-cta:hover,
  .btn-challenge:hover {
    transform: translateY(-1px);
  }
}

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

@media (max-width: 560px) {
  .index-shell { width: min(var(--frame-width), 100vw); padding: 18px 12px 32px; }
  .index-head { display: block; }
  .frame-count { display: inline-block; margin-top: 10px; }
  .frame-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
  .thumb-wrap { min-height: 180px; }
}
