/* Project-specific overrides beyond Tailwind utility classes go here. */

/* add-global-design-system: named tokens mirroring the same --tsg-* values already established
   in static/css/home-preview.css (home.html's separate stylesheet) -- declared here too so this
   file's new component classes below share one token language with that file instead of
   hardcoding hex a second time. Values are identical to base.html's Tailwind config
   (primary/secondary/dark/darker/danger/warning/accent) and docs/BRAND_GUIDE.md -- this is not a
   second color system, just this file's own copy of the same numbers, since a plain stylesheet
   loaded via <link> has no access to Tailwind's build-time theme() function. */
:root {
    --tsg-red: #E70000;
    --tsg-red-dark: #a30000;
    --tsg-teal: #5BB9C3;
    --tsg-bg: #000000;
    --tsg-surface: #121212;
    --tsg-white: #ffffff;
    --tsg-muted: #9CA3AF;
    --tsg-border: rgba(255, 255, 255, 0.1);
    --tsg-danger: #FF6B35;
    --tsg-warning: #FFB020;
    --tsg-success: #22C55E;
}

body {
    font-family: 'Inter', sans-serif;
}

h1, h2, h3, h4 {
    font-family: 'Rajdhani', sans-serif;
    font-weight: 700;
}

.font-heading {
    font-family: 'Rajdhani', sans-serif;
    font-weight: 700;
}

/* add-i18n-foundation: Rajdhani/Inter (above) have zero Devanagari or Gujarati glyph coverage --
   without this, every heading/body string in Hindi or Gujarati would silently fall back to
   whatever default font the visitor's OS happens to ship, breaking the brand's typographic
   identity on exactly the pages meant to prove multilingual support works. Keyed off <html lang>
   (set dynamically in base.html/home.html from g.locale), not a class, so it applies everywhere
   with no per-template changes. Both fonts are loaded in base.html's/home.html's own <link> tags. */
html[lang="hi"] body,
html[lang="gu"] body {
    font-family: 'Noto Sans Devanagari', 'Noto Sans Gujarati', 'Inter', sans-serif;
}

html[lang="hi"] h1, html[lang="hi"] h2, html[lang="hi"] h3, html[lang="hi"] h4,
html[lang="hi"] .font-heading,
html[lang="gu"] h1, html[lang="gu"] h2, html[lang="gu"] h3, html[lang="gu"] h4,
html[lang="gu"] .font-heading {
    font-family: 'Noto Sans Devanagari', 'Noto Sans Gujarati', 'Inter', sans-serif;
    font-weight: 600;
}

/* Card hover lift + glow - browsable item cards (spaces, games, food, packages, testimonials) */
.hover-card {
    transition: box-shadow 0.2s ease;
}
.hover-card:hover {
    box-shadow: 0 12px 28px -8px rgba(91, 185, 195, 0.35);
}
@media (prefers-reduced-motion: no-preference) {
    .hover-card {
        transition: transform 0.2s ease, box-shadow 0.2s ease;
    }
    .hover-card:hover {
        transform: translateY(-4px);
    }
}

/* Primary (red) buttons: glow on hover, press feedback on active */
a.bg-primary, button.bg-primary {
    transition: box-shadow 0.2s ease;
}
a.bg-primary:hover, button.bg-primary:hover {
    box-shadow: 0 0 18px rgba(231, 0, 0, 0.45);
}
@media (prefers-reduced-motion: no-preference) {
    a.bg-primary, button.bg-primary {
        transition: box-shadow 0.2s ease, transform 0.1s ease;
    }
    a.bg-primary:active, button.bg-primary:active {
        transform: scale(0.97);
    }
}

/* Secondary (teal outline) buttons: same treatment, teal glow */
a.border-secondary.text-secondary, button.border-secondary.text-secondary {
    transition: box-shadow 0.2s ease;
}
a.border-secondary.text-secondary:hover, button.border-secondary.text-secondary:hover {
    box-shadow: 0 0 14px rgba(91, 185, 195, 0.4);
}
@media (prefers-reduced-motion: no-preference) {
    a.border-secondary.text-secondary, button.border-secondary.text-secondary {
        transition: box-shadow 0.2s ease, transform 0.1s ease;
    }
    a.border-secondary.text-secondary:active, button.border-secondary.text-secondary:active {
        transform: scale(0.97);
    }
}

/* Scroll reveal - .reveal-pending is added by JS only if it can actually animate it back off,
   so no-JS / a JS error leaves content visible rather than permanently hidden */
.reveal-on-scroll {
    transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal-on-scroll.reveal-pending {
    opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
    .reveal-on-scroll.reveal-pending {
        transform: translateY(24px);
    }
}

/* Hero dragon-logo entrance - one-time, on load (already in the initial viewport, not
   scroll-triggered). Default state is fully visible; the animation only overrides that when
   motion is allowed, so reduced-motion just shows the logo normally. */
@keyframes hero-logo-entrance {
    0% { opacity: 0; transform: scale(0.6) translateY(-12px); }
    60% { opacity: 1; transform: scale(1.05) translateY(0); }
    100% { opacity: 1; transform: scale(1) translateY(0); }
}
@media (prefers-reduced-motion: no-preference) {
    .hero-logo-entrance {
        animation: hero-logo-entrance 1.2s cubic-bezier(0.34, 1.56, 0.64, 1) both;
    }
}

/* Testimonial carousel - native CSS scroll-snap does the swipe/scroll, JS only adds optional
   prev/next buttons on top. Hide the scrollbar for a cleaner look since the buttons/swipe
   already communicate scrollability. */
.testimonial-carousel {
    scrollbar-width: none;
}
.testimonial-carousel::-webkit-scrollbar {
    display: none;
}

/* Dashboard "Recommended For You" strip - same native scroll-snap + hidden-scrollbar approach as
   .testimonial-carousel above (add-dashboard-recommended-games). */
.recommended-games-scroll {
    scroll-snap-type: x proximity;
    scrollbar-width: none;
}
.recommended-games-scroll::-webkit-scrollbar {
    display: none;
}
.recommended-games-scroll > a {
    scroll-snap-align: start;
}

/* People You May Know strip on /gamers and /dashboard/profile (add-friend-suggestions) - same
   technique as .recommended-games-scroll above, its own class since each card is a <div> (avatar
   link + a sibling Add Friend <form>), not a single <a>. */
.friend-suggestions-scroll {
    scroll-snap-type: x proximity;
    scrollbar-width: none;
}
.friend-suggestions-scroll::-webkit-scrollbar {
    display: none;
}
.friend-suggestions-scroll > div {
    scroll-snap-align: start;
}

/* Package cards on /booking (add-package-image): below the `sm` breakpoint the grid is already a
   single column (Tailwind's `grid-cols-1`) -- cap it to roughly 2 card-heights and turn on
   vertical scroll-snap so exactly 2 packages are visible before the user scrolls for the rest,
   same hidden-scrollbar/scroll-snap technique as .recommended-games-scroll above, just vertical. */
@media (max-width: 639px) {
    .package-card-scroll {
        max-height: 450px;
        overflow-y: auto;
        scroll-snap-type: y proximity;
        scrollbar-width: none;
    }
    .package-card-scroll::-webkit-scrollbar {
        display: none;
    }
    .package-card-scroll > .package-card {
        scroll-snap-align: start;
    }
}

/* Booking slot grid: loading shimmer while a fetch is in flight, static flat block if motion
   is reduced (still communicates "loading" without the moving gradient) */
.slot-skeleton {
    background: rgba(255, 255, 255, 0.08);
}
@keyframes slot-shimmer {
    0% { background-position: -200px 0; }
    100% { background-position: 200px 0; }
}
@media (prefers-reduced-motion: no-preference) {
    .slot-skeleton {
        background: linear-gradient(90deg, rgba(255, 255, 255, 0.05) 25%, rgba(255, 255, 255, 0.14) 50%, rgba(255, 255, 255, 0.05) 75%);
        background-size: 400px 100%;
        animation: slot-shimmer 1.4s ease-in-out infinite;
    }
}

/* Hero background carousel: stacked images, cross-faded via a JS-toggled class. Default state
   (no JS, or only one image) is the first slide fully visible -- never a blank hero. */
.hero-carousel-slide {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* These source photos are portrait-oriented (e.g. 335x597) inside a landscape hero banner --
       on short mobile viewports the box becomes width-constrained, which crops top and bottom
       instead of just the sides. All three source photos are darkest/least-detailed in their
       bottom ~10-15% (unlit floor/seating, checked directly against the actual files) -- combined
       with this section's own darkening overlay, showing that region reads as a plain black band
       rather than as photo content. Biased toward the top instead, so the crop comes mostly off
       the bottom (that dark strip) rather than the top (ceiling), keeping each photo's well-lit
       middle -- where the actual desk/monitor/seating setups are -- fully in view either way. */
    object-position: center 25%;
    opacity: 0;
    transition: opacity 3.5s ease;
}
.hero-carousel-slide.is-active {
    opacity: 1;
}

/* Booking slot grid: brief pop on selection, layered on top of the existing color change */
@keyframes slot-select-pop {
    0% { transform: scale(1); }
    50% { transform: scale(1.12); }
    100% { transform: scale(1); }
}
@media (prefers-reduced-motion: no-preference) {
    .slot-select-pop {
        animation: slot-select-pop 0.3s ease;
    }
}

/* Native date inputs: color-scheme tells the browser to render its built-in calendar icon (and
   the popup calendar itself) in a light-appropriate palette so it's actually visible against our
   dark theme -- default rendering was a dark icon on a dark background, easy to miss entirely. */
input[type="date"] {
    color-scheme: dark;
    cursor: pointer;
}
input[type="date"]::-webkit-calendar-picker-indicator {
    cursor: pointer;
    padding: 4px;
    border-radius: 4px;
}
input[type="date"]::-webkit-calendar-picker-indicator:hover {
    background-color: rgba(255, 255, 255, 0.1);
}

/* add-community-blogs: styling for sanitized Markdown-rendered post bodies (services/
   markdown_render.py) -- Tailwind is loaded via CDN script here, no Typography plugin bundled, so
   this is a small hand-written equivalent scoped to the one place it's used. */
.prose-blog p, .prose-blog ul, .prose-blog ol, .prose-blog blockquote, .prose-blog pre {
    margin-bottom: 1.1em;
}
.prose-blog h2, .prose-blog h3, .prose-blog h4 {
    color: #fff;
    font-weight: 700;
    margin-top: 1.6em;
    margin-bottom: 0.6em;
}
.prose-blog ul, .prose-blog ol {
    padding-left: 1.4em;
}
.prose-blog ul {
    list-style-type: disc;
}
.prose-blog ol {
    list-style-type: decimal;
}
.prose-blog blockquote {
    border-left: 3px solid #5BB9C3;
    padding-left: 1em;
    color: #9CA3AF;
}
.prose-blog code {
    background: #1A1A1A;
    padding: 0.15em 0.4em;
    border-radius: 4px;
    font-size: 0.9em;
}
.prose-blog pre {
    background: #1A1A1A;
    padding: 1em;
    border-radius: 8px;
    overflow-x: auto;
}
.prose-blog pre code {
    background: none;
    padding: 0;
}
.prose-blog a {
    color: #5BB9C3;
    text-decoration: underline;
}
.prose-blog img {
    max-width: 100%;
    border-radius: 8px;
    margin: 1em 0;
}

/* add-vip-svip-membership: avatar glow ring (Gamers directory cards + profile page), gold for
   VIP, silver for SVIP. Static ring always shows; the pulsing glow layer is reduced-motion-gated
   like every other animation in this file. Silver hex matches this codebase's existing
   GIFT_BADGE_LEVELS "Silver" convention (models.py). */
.membership-ring-vip, .membership-ring-svip {
    border: 2px solid;
}
.membership-ring-vip {
    border-color: #FFD700;
    box-shadow: 0 0 10px rgba(255, 215, 0, 0.55);
}
.membership-ring-svip {
    border-color: #C0C0C0;
    box-shadow: 0 0 10px rgba(192, 192, 192, 0.55);
}
@keyframes membership-ring-pulse-vip {
    0%, 100% { box-shadow: 0 0 10px rgba(255, 215, 0, 0.55); }
    50% { box-shadow: 0 0 18px rgba(255, 215, 0, 0.9); }
}
@keyframes membership-ring-pulse-svip {
    0%, 100% { box-shadow: 0 0 10px rgba(192, 192, 192, 0.55); }
    50% { box-shadow: 0 0 18px rgba(192, 192, 192, 0.9); }
}
@media (prefers-reduced-motion: no-preference) {
    .membership-ring-vip { animation: membership-ring-pulse-vip 2.4s ease-in-out infinite; }
    .membership-ring-svip { animation: membership-ring-pulse-svip 2.4s ease-in-out infinite; }
}

/* Directory card border tint to match the member's tier (subtle -- the ring/badge do the real
   work, this just keeps the whole card feeling "theirs" without fighting the hover-card border) */
.membership-card-vip:hover { border-color: rgba(255, 215, 0, 0.5) !important; }
.membership-card-svip:hover { border-color: rgba(192, 192, 192, 0.5) !important; }

/* add-membership-profile-background: admin-configurable per tier (see /admin/membership-
   appearance), so unlike every other membership-* class here the actual image isn't known at
   author time -- the caller sets --profile-bg-url inline (a url(...) value) alongside this class.
   Mobile-only (this codebase's own sm breakpoint) -- same posture as every earlier attempt at
   this: `background-size: 100% auto` fits the image at its full width rather than `cover` (a
   tall, multi-section profile page is much taller than any of these images, and `cover` blows
   the art up to cover that height, cropping away most of its width instead); no
   background-attachment: fixed, scrolls with the page. */
@media (max-width: 639px) {
    .profile-bg-image {
        background-image: linear-gradient(rgba(5, 3, 1, 0.35), rgba(5, 3, 1, 0.45)), var(--profile-bg-url);
        background-size: 100% auto;
        background-position: top center;
        background-repeat: no-repeat;
    }
}

/* add-profile-cosmetics (phase 4): shared shell for every equipped Profile Effect cosmetic
   (templates/_cosmetics.html's cosmetic_profile_effect macro) -- pointer-events: none and the
   z-index/positioning are baked in here, not left to each cosmetic's own admin-entered css_class,
   so an effect can never block a click/tap on real profile content regardless of what an admin
   types into a cosmetic's CSS class field.
   fix-profile-effect-full-page-bg: `inset: 0` resolves against the nearest positioned ancestor --
   dashboard_profile.html/gamer_detail.html render this as the first child of the page-wide
   `.relative` profile container (not the header-only wrapper it used to sit inside), so the effect
   now spans the whole profile page's content height and grows automatically as sections are
   added/removed, instead of being clipped to the header block's own height. Explicit z-index: 0
   here vs. that container's real-content wrapper being `.relative.z-10` is what guarantees the
   effect always paints behind every profile section, not DOM order (a plain non-positioned
   sibling would otherwise paint *behind* this positioned overlay per CSS stacking rules, not in
   front of it -- see that wrapper's own comment). */
.cosmetic-effect-overlay {
    position: absolute;
    inset: 0;
    /* fix-cosmetic-effect-overlay-image-sizing: `inset: 0` alone reliably stretches a plain div
       (the CSS-class effect path) but NOT a replaced element like the asset_url <img> fallback --
       without an explicit size, a browser computes an absolutely-positioned img's box from its
       own intrinsic aspect ratio instead of stretching to fill top/right/bottom/left, and (found
       live, testing a real image-based effect for the first time) that zero/undersized initial
       box can also stall the img's own `loading="lazy"` entirely, since the browser's visibility
       heuristic never sees it as onscreen. width/height: 100% forces the correct box up front for
       both element types, before the image itself has loaded. */
    width: 100%;
    height: 100%;
    z-index: 0;
    pointer-events: none;
    overflow: hidden;
    border-radius: inherit;
}

/* Starter cosmetic effect/border/name-effect classes -- an admin picks one of these by name in a
   cosmetic's CSS class field at /admin/cosmetics (or a developer adds more here later, per this
   feature's own "flexible enough to add fire/lightning/neon effects later" requirement). All
   CSS-only (no image/video assets), respect prefers-reduced-motion like every other animation in
   this file, and never touch pointer-events/positioning themselves -- only visual properties. */
@keyframes cosmetic-ember-drift {
    0%, 100% { opacity: 0.35; transform: translateY(0); }
    50% { opacity: 0.55; transform: translateY(-6px); }
}
.cosmetic-effect-fire {
    background: radial-gradient(ellipse 70% 50% at 50% 0%, rgba(255, 120, 20, 0.35), transparent 70%);
}
@media (prefers-reduced-motion: no-preference) {
    .cosmetic-effect-fire { animation: cosmetic-ember-drift 3s ease-in-out infinite; }
}

@keyframes cosmetic-neon-pulse {
    0%, 100% { opacity: 0.4; }
    50% { opacity: 0.8; }
}
.cosmetic-effect-neon {
    background: radial-gradient(ellipse 70% 50% at 50% 0%, rgba(91, 185, 195, 0.4), transparent 70%);
}
@media (prefers-reduced-motion: no-preference) {
    .cosmetic-effect-neon { animation: cosmetic-neon-pulse 2.4s ease-in-out infinite; }
}

.cosmetic-border-gold {
    border: 1px solid rgba(255, 215, 0, 0.45);
    border-radius: 1rem;
    box-shadow: 0 0 24px rgba(255, 215, 0, 0.12);
}
.cosmetic-border-neon {
    border: 1px solid rgba(91, 185, 195, 0.5);
    border-radius: 1rem;
    box-shadow: 0 0 24px rgba(91, 185, 195, 0.15);
}

@keyframes cosmetic-name-shimmer {
    0% { background-position: 0% 50%; }
    100% { background-position: 200% 50%; }
}
.cosmetic-name-shimmer {
    background: linear-gradient(90deg, #FFD700, #FFF6C8, #FFD700, #FFF6C8);
    background-size: 300% auto;
    -webkit-background-clip: text;
    background-clip: text;
    /* !important: the name text also carries Tailwind's text-white utility (same "override a
       sibling Tailwind class" need as .membership-card-vip:hover above), which has equal
       specificity -- this stylesheet loads after the compiled tailwind.css in base.html's <head>
       so plain cascade order alone would already win now, but !important is kept anyway as a
       robustness margin against that load order ever changing. */
    color: transparent !important;
}
@media (prefers-reduced-motion: no-preference) {
    .cosmetic-name-shimmer { animation: cosmetic-name-shimmer 3s linear infinite; }
}


/* =========================================================================
   GLOBAL DESIGN SYSTEM (add-global-design-system)
   Formalizes conventions that already existed as repeated, hand-typed Tailwind utility
   combinations (e.g. "bg-primary hover:bg-primary/80 transition text-white font-semibold px-6
   py-3 rounded-lg" retyped on every primary button) into single reusable classes. Colors/radii/
   spacing values are the SAME ones already in use across the site and documented in
   docs/BRAND_GUIDE.md -- nothing here is a new visual language, only a name for the existing one.
   Purely additive: no existing template was changed to use these yet (that's individual
   page-by-page work for a later phase), so nothing currently on the site can regress from this
   block existing. New pages/components can opt in by using these classes instead of retyping the
   raw utility combo.
   ========================================================================= */

/* ---- Buttons: 3-tier hierarchy (primary action / secondary action / low-priority link) ---- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    font-family: 'Inter', sans-serif;
    font-weight: 600;
    font-size: 0.875rem;
    line-height: 1.2;
    padding: 0.75rem 1.5rem;
    min-height: 44px; /* comfortable touch target, per accessibility requirement */
    border-radius: 0.5rem; /* matches the site's existing rounded-lg button convention */
    border: 1px solid transparent;
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease, transform 0.1s ease;
}
.btn:disabled, .btn[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
    box-shadow: none;
    transform: none;
}

/* Primary: the one strongest CTA per view (Book Now, Join Tournament, Sign Up) */
.btn-primary {
    background: var(--tsg-red);
    color: var(--tsg-white);
}
.btn-primary:hover:not(:disabled) {
    background: var(--tsg-red-dark);
    box-shadow: 0 0 18px rgba(231, 0, 0, 0.45);
}

/* Secondary: teal outline, existing "View menu / Explore Games / Learn More" language */
.btn-secondary {
    background: transparent;
    color: var(--tsg-teal);
    border-color: var(--tsg-teal);
}
.btn-secondary:hover:not(:disabled) {
    background: rgba(91, 185, 195, 0.1);
    box-shadow: 0 0 14px rgba(91, 185, 195, 0.4);
}

/* Tertiary: plain text link for low-priority actions -- no button chrome, no forced min-height
   (it's inline with text, not a tap target competing with the primary/secondary pair) */
.btn-tertiary {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    background: none;
    border: none;
    padding: 0.25rem 0;
    min-height: 0;
    font-family: 'Inter', sans-serif;
    font-weight: 600;
    font-size: 0.875rem;
    color: var(--tsg-teal);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}
.btn-tertiary:hover:not(:disabled) {
    color: var(--tsg-white);
}

@media (prefers-reduced-motion: no-preference) {
    .btn-primary:active:not(:disabled), .btn-secondary:active:not(:disabled) {
        transform: scale(0.97);
    }
}

/* ---- Cards: shared chrome (border/background/radius), NOT a single forced look -- combine with
   the existing .hover-card class for anything browsable/clickable, same as today. Two radius
   tiers rather than one, matching how the site already sizes radius by card prominence: compact
   for dense repeating grids, comfortable for larger standalone cards. ---- */

.card {
    background: var(--tsg-surface);
    border: 1px solid var(--tsg-border);
    border-radius: 0.75rem; /* 12px, matches rounded-xl -- dense grid cards: games, blog, gamers */
}
.card-lg {
    border-radius: 1rem; /* 16px, matches rounded-2xl -- larger standalone cards: tournaments, packages, modals */
}

/* ---- Status badges: text label always present (never color-only), palette restricted to the
   existing brand + status colors. A leading dot reinforces state at a glance without replacing
   the text, the same "text + color" pairing the rest of the site already uses for status. ---- */

.badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3125rem;
    font-family: 'Inter', sans-serif;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-transform: capitalize;
    padding: 0.1875rem 0.625rem;
    border-radius: 9999px;
    border: 1px solid transparent;
    white-space: nowrap;
}
.badge::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
    flex-shrink: 0;
}
/* Positive / open-for-action states -- success green */
.badge-open, .badge-available, .badge-online {
    background: rgba(34, 197, 94, 0.12);
    color: var(--tsg-success);
    border-color: rgba(34, 197, 94, 0.4);
}
/* Neutral / no-longer-actionable states -- muted gray, not a "bad" color, just inactive */
.badge-closed, .badge-full, .badge-offline {
    background: rgba(255, 255, 255, 0.06);
    color: var(--tsg-muted);
    border-color: var(--tsg-border);
}
/* Urgent/live -- brand red, since "happening right now" is the closest fit to the primary-action
   color's existing "this matters most" role. Subtle pulse (esports "on air" convention),
   reduced-motion gated like every other animation in this file. */
.badge-live {
    background: rgba(231, 0, 0, 0.12);
    color: var(--tsg-red);
    border-color: rgba(231, 0, 0, 0.4);
}
@keyframes badge-live-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.55; }
}
@media (prefers-reduced-motion: no-preference) {
    .badge-live::before {
        animation: badge-live-pulse 1.6s ease-in-out infinite;
    }
}
/* Scheduled/pending -- warning amber, matching the existing pending-status convention */
.badge-upcoming {
    background: rgba(255, 176, 32, 0.12);
    color: var(--tsg-warning);
    border-color: rgba(255, 176, 32, 0.4);
}

/* ---- Section heading pattern: small eyebrow label + heading + short supporting text, the shape
   already used ad hoc throughout (home.html's "PLAY WHAT YOU LOVE" / "FEATURED GAMES", the
   Champion's Corner hero, etc.) -- named here so it can be reused instead of re-typing the same
   three Tailwind rules each time. A CTA alongside it stays whatever link/button classes fit that
   page; this only standardizes the label/heading/subtext, not a required trailing action. ---- */

.section-eyebrow {
    display: block;
    font-family: 'Inter', sans-serif;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--tsg-teal);
    margin-bottom: 0.5rem;
}
.section-heading {
    font-family: 'Rajdhani', sans-serif;
    font-weight: 700;
    font-size: 1.875rem; /* text-3xl */
    color: var(--tsg-white);
}
@media (min-width: 768px) {
    .section-heading {
        font-size: 2.25rem; /* text-4xl */
    }
}
@supports (text-wrap: balance) {
    .section-heading {
        text-wrap: balance;
    }
}
.section-subtext {
    font-family: 'Inter', sans-serif;
    color: var(--tsg-muted);
    margin-top: 0.5rem;
    max-width: 42rem;
}

/* ---- Baseline visible focus state for every interactive element site-wide -- teal, matching
   the brand guide's existing "teal = focus" rule (today only applied to form input rings). Only
   fires for :focus-visible (keyboard/assistive-tech focus), never for a mouse click, so it adds
   keyboard accessibility without changing anything about mouse/touch interaction. ---- */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
    outline: 2px solid var(--tsg-teal);
    outline-offset: 2px;
}

/* add-avatar-live-indicator: sitewide "is this person live right now" signal on every avatar,
   everywhere one is shown -- confirmed with the user as a small dot on the avatar itself (not the
   existing text pill badge home.html's "WHO'S PLAYING"/"LIVE NOW" cards already use), animated
   with expanding rings that appear and fade like a radar/broadcast ping, matching the reference
   image the user gave. See templates/_live_indicator.html's live_indicator(user) macro -- the one
   call site every avatar render uses, so this shape/animation only needs to exist once.
   Deliberately one small fixed size regardless of the avatar's own size (5px avatars up to
   96px hall-of-fame photos alike) -- "a small circle," not scaled to match, per the user's own
   wording. Requires the avatar's own wrapper to be position:relative (or better, an ancestor a
   couple levels up so the indicator isn't clipped by the avatar photo's own rounded-full
   overflow-hidden mask) -- each template site adds that wrapper as needed. */
.live-indicator {
    position: absolute;
    width: 9px;
    height: 9px;
    z-index: 5;
    pointer-events: none;
}
/* Default corner (bottom-right); alternates for when another badge (membership corner icon,
   online-presence dot) already occupies that spot on a given card -- see
   templates/_live_indicator.html's live_indicator(user, corner=...) macro. */
.live-indicator--bottom-right { bottom: -1px; right: -1px; }
.live-indicator--top-right { top: -1px; right: -1px; }
.live-indicator--top-left { top: -1px; left: -1px; }
.live-indicator--bottom-left { bottom: -1px; left: -1px; }
.live-indicator .live-dot {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: var(--tsg-red);
    border: 1.5px solid var(--tsg-bg);
    box-shadow: 0 0 4px rgba(231, 0, 0, 0.85);
}
.live-indicator .live-ring {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 1.5px solid var(--tsg-red);
    opacity: 0;
    animation: tsg-live-ping 1.8s cubic-bezier(0, 0, 0.3, 1) infinite;
}
.live-indicator .live-ring:nth-child(2) {
    animation-delay: 0.6s;
}
@keyframes tsg-live-ping {
    0% { transform: scale(1); opacity: 0.9; }
    100% { transform: scale(2.8); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
    .live-indicator .live-ring {
        animation: none;
        opacity: 0;
    }
}

