/* Front Office — small extras Tailwind's utility classes don't cover cleanly. */

/* htmx: hide [hx-indicator] targets until the request they belong to is in flight.
   pointer-events: none while hidden so a full-screen overlay indicator doesn't
   block clicks when it's invisible. */
.htmx-indicator { opacity: 0; pointer-events: none; transition: opacity 150ms ease; }
.htmx-request.htmx-indicator,
.htmx-request .htmx-indicator { opacity: 1; pointer-events: auto; }

/* Skeleton loaders: showing one for a near-instant response reads as a
   glitch, not a loading state, so delay onset until only requests that
   actually take a moment ever show it, but hide instantly once the real
   content lands (no delay on the way out). */
.skeleton-indicator.htmx-indicator { transition: opacity 150ms ease; }
.htmx-request.skeleton-indicator.htmx-indicator { transition: opacity 150ms ease 200ms; }

/* Sign-in/sign-up background. Mobile: the full hero photo read as too
   busy/cluttered on small screens (Bill's call, 2026-08-17) — replaced
   with the EXACT same background the dashboard/Lineup Advisor `<main>`
   already uses (same photo, same darkening overlay, same 300% zoom +
   fixed attachment) for real one-look consistency across the mobile
   site — no extra gold-glow layer here; a first attempt added one on
   top and Bill corrected it back to matching dashboard exactly. Desktop
   (`sm:` up) keeps the original full-brightness hero photo + glow,
   untouched. */
.auth-bg {
  background-color: #14212E;
  background-image:
    linear-gradient(rgba(20,33,46,0.90), rgba(20,33,46,0.90)),
    url('/static/img/bg_grid_antique.jpg');
  background-size: auto, 300% auto;
  background-position: center, 0% 0%;
  background-repeat: no-repeat, no-repeat;
  background-attachment: scroll, fixed;
}
/* Desktop (sm: and up): one continuous full-bleed hero image across the
   whole page — the source image already composes its grid-line texture on
   the left (empty space for the sign-in/sign-up card to sit on top of) and
   the shield/wordmark on the right, by design (2026-08-31). `cover`
   anchored right keeps the shield in frame and guarantees true full-bleed
   (no letterbox bars, tried `contain` first and Bill correctly rejected
   the bars it left at non-16:9 window shapes) — the tradeoff is real
   cropping that varies with the window's exact aspect ratio. */
@media (min-width: 640px) {
  .auth-bg {
    background-image:
      linear-gradient(rgba(20,33,46,0.35), rgba(20,33,46,0.35)),
      url('/static/img/hero_page.jpg');
    background-size: auto, cover;
    background-position: center, right center;
    background-attachment: scroll, fixed;
  }
}

/* Dashboard/Lineup Advisor/Weekly Matchup/Draft Generator `<main>` background —
   same low-opacity texture as .auth-bg. Fixed attachment only at sm: and up:
   background-attachment: fixed is a known mobile-browser quirk that can break
   position: sticky on descendants (confirmed root cause of Weekly Matchup's
   mobile jump-nav not sticking, 2026-08-19) and isn't free on mobile scroll
   performance either. */
.app-main-bg {
  background-color: #14212E;
  /* Edge-fade layers (top/left/bottom) paint solid navy fading to
     transparent right where this panel touches the flat header/rail/footer
     chrome, so the textured photo underneath ramps in from nothing at the
     seam to full strength in the interior — fixes a hard visual line
     between "textured panel" and "flat chrome" that a uniform zoom/opacity
     change couldn't (2026-08-31, Bill's diagnosis: the seam isn't a color
     mismatch, navy matches exactly — it's texture-vs-flat). */
  background-image:
    linear-gradient(to bottom, #14212E, transparent 64px),
    linear-gradient(to right, #14212E, transparent 64px),
    linear-gradient(to top, #14212E, transparent 64px),
    linear-gradient(rgba(20,33,46,0.90), rgba(20,33,46,0.90)),
    url('/static/img/bg_grid_antique.jpg');
  background-size: 100% 64px, 64px 100%, 100% 64px, auto, 100% auto;
  background-position: top, left, bottom, center, 0% 0%;
  background-repeat: no-repeat, no-repeat, no-repeat, no-repeat, no-repeat;
  background-attachment: scroll, scroll, scroll, scroll, scroll;
}
@media (min-width: 640px) {
  .app-main-bg {
    background-attachment: scroll, scroll, scroll, scroll, fixed;
  }
}

::selection { background: #C19528; color: #14212E; }

/* Keyboard focus stays visible everywhere, including inside dark panels.
   !important is deliberate: several inputs sitewide carry Tailwind's
   `focus:outline-none` (higher specificity, 2 classes vs this bare
   pseudo-class), which silently made this rule a no-op on every one of
   them despite the comment above -- confirmed via a real focused-element
   computed-style check, not assumed. This is the one place that override
   must always win, so every focusable element gets a visible ring
   regardless of what utility classes it carries. */
:focus-visible { outline: 2px solid #C19528 !important; outline-offset: 2px !important; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }
}

/* Dashboard module tiles: the tile and its icon both nudge on hover -- state
   indication that the tile is interactive, nothing more. Icon moves a bit
   more than the card so it reads as the focal point. Gated to real
   hover-capable pointers so a tap on touch doesn't leave anything stuck
   mid-transform; the global prefers-reduced-motion rule above already
   collapses the duration to near-zero, so no separate override is needed
   here. 2026-09-07, amplitude increased same day per Bill's ask for more
   "pop" now that the tiles have more room (Data Sources section removed). */
.module-tile, .module-tile-icon { transition: transform 150ms ease; }
@media (hover: hover) and (pointer: fine) {
  .module-tile:hover { transform: translateY(-2px) scale(1.02); }
  .module-tile:hover .module-tile-icon { transform: translateY(-1px) scale(1.15); }
}

/* Dashboard module tiles: spinning gradient glow on hover only, no resting
   state (Bill's call, 2026-09-20) -- a persistent glow on all four tiles
   would fight the brass accent's whole reason for working elsewhere in
   this app (Draft Dates, etc.): it reads as emphasis specifically because
   it's rare.

   First pass animated a CSS custom property inside a conic-gradient
   background -- correct-looking in a single frame, but that forces a
   full repaint every frame (background isn't GPU-compositable), and
   read as blinking rather than spinning once actually watched live
   (Bill, 2026-09-20). Rebuilt as the standard two-layer version instead:
   ::before is an oversized (200%), centered conic-gradient square that
   does nothing but `rotate` -- a real transform, GPU-compositable, so it
   spins smoothly at any speed. ::after is a plain rect the same color as
   the tile's own background (navy-light, #1F3040), inset by the ring
   thickness, sitting on top of it -- covers the middle, leaving only
   that thin inset margin showing the gradient underneath. `overflow:
   hidden` on the tile itself clips ::before's oversized square to the
   tile's own rounded shape (oversized on purpose: at 45°, a same-size
   square's corners would rotate outside the ring and reveal gaps). */
.module-tile { position: relative; overflow: hidden; }
.module-tile::before {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 200%; height: 200%;
  transform: translate(-50%, -50%) rotate(0deg);
  /* No will-change here on purpose -- it was here originally to hint a
     dedicated GPU layer, but that's a documented real source of frozen
     animations on certain GPU driver/hardware combinations (the browser
     promotes the layer once and never repaints it again). Confirmed live,
     2026-09-20: real Chrome, Firefox, and Brave, on Bill's actual
     hardware, all showed a completely static ring with will-change set;
     Playwright's automated Chromium (software-rendered, never touching
     the host GPU driver) was the only one that ever animated, which is
     the opposite of what a real perf win should look like -- a strong
     sign the hint itself was the problem, not a fix. Letting the browser
     decide on its own whether to composite this is the safer default. */
  will-change: transform;
  background: conic-gradient(from 0deg,
    #8B6914 0%, #B8860B 15%, #F0E6D2 25%, #B8860B 35%, #8B6914 50%,
    #B8860B 65%, #F0E6D2 75%, #B8860B 85%, #8B6914 100%);
  opacity: 0;
  transition: opacity 200ms ease;
  pointer-events: none;
  z-index: -2;
}
.module-tile::after {
  content: "";
  position: absolute;
  inset: 1.5px;
  border-radius: inherit;
  background: #1F3040;
  pointer-events: none;
  z-index: -1;
}
@media (hover: hover) and (pointer: fine) {
  .module-tile:hover::before {
    opacity: 1;
    animation: module-tile-spin 2s linear infinite;
  }
}
@keyframes module-tile-spin {
  from { transform: translate(-50%, -50%) rotate(0deg); }
  to   { transform: translate(-50%, -50%) rotate(360deg); }
}
/* Exempts this animation from the blanket reduced-motion rule above,
   rather than fighting it with :not() -- :not() can't take a
   pseudo-element (`.module-tile::before` isn't legal inside :not(),
   would silently invalidate the whole selector list and break reduced
   motion sitewide, caught before shipping it). Cleaner: a higher-
   specificity, later-declared, !important override wins outright, no
   selector tricks needed. Bill's actual Windows setup reports
   prefers-reduced-motion: reduce in every real browser he has -- the
   blanket rule's near-zero duration turns this `infinite` animation into
   a strobe instead of stillness, which is why it looked broken in every
   real browser during design review while looking fine in an automated
   test browser that doesn't inherit the host's accessibility settings. */
@media (prefers-reduced-motion: reduce) {
  .module-tile:hover::before { animation-duration: 2s !important; }
}
