/* ==========================================================================
   HidRyx theme for CTFd

   Layered on top of the core theme's compiled Bootstrap bundle, which is still
   what provides Alpine, the modals, dropdowns and the challenge board logic.
   Two jobs:

     1. Re-point Bootstrap's custom properties at the HidRyx palette, so
        components inherit the look without a per-component override.
     2. Fix the handful of places where core's own rules out-specify the theme
        and quietly win. Each of those is called out where it happens, because
        the fix is a matching selector rather than an `!important`.

   CTFd's colour mode switcher writes `data-bs-theme` onto <html>, so every
   colour here is keyed off that rather than the `data-theme` attribute the
   static site uses.

   RESPONSIVE STRATEGY
   -------------------
   One structural breakpoint -- 992px -- for the single genuinely structural
   change in the theme: the navigation switching between an inline bar and a
   disclosure panel. It is pinned to Bootstrap's `lg`
   so the menu and the page grid turn over at the same width. The one other
   width query is a density decision inside the bar, not a layout change: the
   account actions drop their labels to their icons between 992px and 1200px.

   Everything else is intrinsic sizing rather than overrides -- `clamp()` for
   type and spacing, `minmax()`/`auto-fit` where a grid helps, flex wrapping,
   `min-width: 0` plus `text-overflow: ellipsis` for the things that genuinely
   can be unbounded (the CTF name, challenge names, team names). The two
   `@media` blocks at the bottom are capability queries, not width ones.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Design tokens
   -------------------------------------------------------------------------- */

:root,
[data-bs-theme="dark"] {
  --hx-bg: #0a0a0c;
  --hx-surface: #0f0d12;
  --hx-surface-2: #121014;
  --hx-border: #241e2c;
  --hx-deep: #2e0b4e;
  --hx-purple: #4b1878;
  --hx-accent: #8a4fd1;
  --hx-text: #edebf2;
  --hx-soft: #b9afcb;
  --hx-muted: #807697;
  --hx-on-accent: #170b20;
  /* Secondary text has to clear 4.5:1 against the page in both modes: the
     muted grey-purple this started from measured 3.58:1 here and 4.43:1 in
     light mode, i.e. the primary navigation failed WCAG AA on its resting
     colour. The hue is unchanged; both values are the same colour lightened or
     darkened until the ratio passes (4.65:1 in each mode). */
  --hx-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
  --hx-glow: 0 0 0 1px rgba(138, 79, 209, 0.35), 0 12px 30px rgba(46, 11, 78, 0.45);
  --hx-scan: 0.025;

  /* Page-header band. `#8004fb` is the brand purple the admin's optional
     `theme_header` injects as `.jumbotron { background-color: #8004fb
     !important }`, so the band exists in both colour modes whatever the
     theme does here. The theme therefore does not try to win that
     declaration -- it owns the text, and picks colours that pass AA against
     the band that is actually painted:
       #ffffff 5.9:1 .. 9.9:1   (headings)
       #efe7fb 4.8:1 .. 6.1:1   (sub-heading) */
  --hx-band: #8004fb;
  --hx-band-image: linear-gradient(115deg, #5c02b5 0%, #8004fb 55%, #8f0cff 100%);
  --hx-on-band: #ffffff;
  --hx-on-band-sub: #efe7fb;

  --bs-body-bg: var(--hx-bg);
  --bs-body-color: var(--hx-text);
  --bs-emphasis-color: #ffffff;
  --bs-secondary-color: var(--hx-soft);
  --bs-tertiary-color: var(--hx-muted);
  --bs-border-color: var(--hx-border);
  --bs-link-color: var(--hx-accent);
  --bs-link-hover-color: #9a61df;
  --bs-primary: var(--hx-accent);
  --bs-primary-rgb: 138, 79, 209;
  --bs-secondary-rgb: 185, 175, 203;
  --bs-body-font-family: "Space Grotesk", system-ui, -apple-system, sans-serif;
  --bs-font-monospace: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --bs-border-radius: 4px;
  --bs-border-radius-sm: 3px;
  --bs-border-radius-lg: 6px;
}

[data-bs-theme="light"] {
  --hx-bg: #f6f4f8;
  --hx-surface: #ffffff;
  --hx-surface-2: #efecf3;
  --hx-border: #d9d1e1;
  --hx-deep: #efe7f7;
  --hx-purple: #6b2fa6;
  --hx-accent: #7a3dc4;
  --hx-text: #17131c;
  --hx-soft: #4a4257;
  --hx-muted: #736a85;
  --hx-on-accent: #ffffff;
  --hx-shadow: 0 18px 40px rgba(23, 19, 28, 0.12);
  --hx-glow: 0 0 0 1px rgba(122, 61, 196, 0.3), 0 12px 28px rgba(107, 47, 166, 0.16);
  --hx-scan: 0.015;

  --bs-emphasis-color: #000000;
  --bs-link-color: var(--hx-purple);
  --bs-link-hover-color: var(--hx-accent);
}

:root {
  /* The bar's height. One variable feeds the bar, the panel's offset and the
     scroll padding, so they cannot drift apart. */
  --hx-nav-h: 4rem;

  /* Type scale. Uppercase, wide-tracked labels need more room as they grow, and
     less of it as they shrink -- which is exactly what clamp() expresses. */
  --hx-fs-nav: clamp(0.625rem, 0.58rem + 0.2vw, 0.6875rem);
  --hx-fs-label: clamp(0.6875rem, 0.64rem + 0.22vw, 0.75rem);
  --hx-fs-body: clamp(0.875rem, 0.83rem + 0.2vw, 0.9375rem);
  --hx-track-nav: clamp(1.4px, 1.1px + 0.4vw, 1.8px);

  --hx-gap-nav: clamp(0.9rem, 0.55rem + 1.1vw, 1.75rem);
  --hx-pad-page: clamp(1rem, 0.5rem + 2vw, 2rem);

  /* Minimum comfortable touch target. Applied to the whole control, not only
     to the small icon buttons, so it is not a width-dependent decision. */
  --hx-tap: 2.75rem;
}

html {
  scrollbar-color: var(--hx-border) var(--hx-bg);
  /* The bar is sticky, so it still overlaps an in-page anchor target. */
  scroll-padding-top: calc(var(--hx-nav-h) + 1rem);
}

/* --------------------------------------------------------------------------
   Base
   -------------------------------------------------------------------------- */

body {
  min-height: 100vh;
  background: var(--hx-bg);
  color: var(--hx-text);
  letter-spacing: 0.01em;
  transition: background 0.3s ease, color 0.3s ease;

  /* Sticky footer. Replaces core's `.footer { position: absolute; bottom: 1px;
     height: 60px; line-height: 60px }` plus `main { margin-bottom: 100px }`,
     which fought the theme's own footer box and left the text overflowing a
     60px-tall absolutely positioned strip. */
  display: flex;
  flex-direction: column;
}

main {
  flex: 1 0 auto;
  width: 100%;
  /* Core's `_jumbotron.scss` opens with `main { padding-top: 3.5rem }`, a
     leftover from core's fixed-height navbar. This bar is sticky, so it is
     already in the flow: the reserved 3.5rem was dead space in front of every
     page, and the page header's own padding on top of it. Same specificity,
     later stylesheet, so restating it is enough. */
  padding-top: 0;
}

/* Admin-authored page content (the home page's logo) pins an inline
   `style="max-width: 420px"` on a `.img-fluid`. An inline declaration can only
   be beaten by `!important`; `min()` keeps the author's 420px cap on a wide
   screen while still letting the image shrink inside its container on a
   phone, which is the 38-108px of sideways scroll it used to cause there. */
main .img-fluid {
  max-width: min(100%, 420px) !important;
}

/* Core's `_fonts.scss` sets `html, body, .container { font-family: Lato }` and
   `.jumbotron .container { font-family: Raleway }`. Those are direct
   declarations, so they beat the inherited `--bs-body-font-family` and the
   Space Grotesk face never reached the page content. Same specificity, later
   stylesheet, so restating them here is enough. */
html,
body,
.container,
.jumbotron .container {
  font-family: "Space Grotesk", system-ui, -apple-system, sans-serif;
}

/* The signature scanline wash from the HidRyx site. Kept above the page but
   below anything interactive, and switched off entirely for anyone who has
   asked for less motion. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9999;
  opacity: var(--hx-scan);
  pointer-events: none;
  background-image: repeating-linear-gradient(
    0deg,
    #fff 0,
    #fff 1px,
    transparent 1px,
    transparent 4px
  );
}

::selection {
  background: var(--hx-accent);
  color: var(--hx-on-accent);
}

a {
  text-decoration: none;
}

a:hover {
  color: var(--hx-accent);
}

code,
pre,
.solve-time,
[data-time] {
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

h1,
h2,
h3,
h4,
h5,
h6,
.hx-eyebrow {
  letter-spacing: 2px;
  text-transform: uppercase;
  font-weight: 600;
  /* Long challenge and team names are the common case on a CTF, and an
     unbroken 60-character name is what pushes a page sideways. */
  overflow-wrap: break-word;
}

.hx-eyebrow {
  color: var(--hx-accent);
  font-size: 10px;
  letter-spacing: 3px;
}

.hx-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Alpine's x-show leaves elements in the DOM before first paint; without this
   the unread-notification count is briefly visible as a bare "0". */
[x-cloak] {
  display: none !important;
}

/* --------------------------------------------------------------------------
   Navbar

   Mobile-first: the bar is a brand plus a toggler, and the links live in a
   panel that the theme hides and hidryx.js reveals. The single media query
   further down turns that into an inline row.
   -------------------------------------------------------------------------- */

.hx-navbar {
  position: sticky;
  top: 0;
  z-index: 1030;
  min-height: var(--hx-nav-h);
  /* Fallback first for browsers without color-mix(). */
  background: var(--hx-bg);
  background: color-mix(in srgb, var(--hx-bg) 82%, transparent);
  border-bottom: 1px solid var(--hx-border);
  backdrop-filter: blur(18px);
}

/* One Bootstrap container holds the whole bar, so the brand, the links and the
   controls share the page's own gutter at every width. The bar used to work
   its alignment out with a hand-rolled
   `padding-inline: max(1.25rem, (100% - 1320px) / 2)`, which put the brand 12px
   outside the content on wide screens and 40px inside it on a laptop. */
.hx-navbar-inner {
  display: flex;
  align-items: center;
  min-width: 0;
}

.hx-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 5px;
  color: var(--hx-text);
  white-space: nowrap;
}

.hx-brand:hover {
  color: var(--hx-text);
}

/* A long CTF name set in the eyebrow has to give way before the bar does. */
.hx-brand > .hx-eyebrow {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* Intrinsic sizing: a wide logo is allowed to shrink with the bar instead of
   pushing the menu button off the screen. `flex: 0 1 auto` (not `none`) is what
   makes it shrinkable. */
.hx-brand img,
.hx-brand .ctf_logo {
  max-width: 100%;
  max-height: 26px;
  width: auto;
  height: auto;
  flex: 0 1 auto;
  filter: none;
}

.hx-nav-list {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.hx-nav-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--hx-muted);
  font-size: var(--hx-fs-label);
  letter-spacing: var(--hx-track-nav);
  text-transform: uppercase;
  /* Full-bleed rows in the panel, so the tap target is the whole line. */
  padding: 0.7rem 0;
  min-height: var(--hx-tap);
  transition: color 0.2s ease;
}

.hx-nav-link:hover,
.hx-nav-link:focus-visible,
.hx-nav-link.active {
  color: var(--hx-text);
}

.hx-nav-link:focus-visible {
  outline: 2px solid var(--hx-accent);
  outline-offset: 2px;
}

.hx-nav-link i {
  flex: none;
  width: 1em;
  text-align: center;
}

.hx-nav-link .badge-notification {
  margin-left: 0.2rem;
}

/* The unread count sits tight against the bell on the bar. */
.hx-nav-link-bell {
  position: relative;
}

.hx-nav-link-bell .badge-notification {
  position: absolute;
  top: 0.15em;
  left: 0.85em;
  margin-left: 0;
  font-size: 0.55rem;
  line-height: 1;
  padding: 0.2em 0.35em;
}

/* The status line: the CTF name in the type the brand uses for reference text.
   It lives in the disclosure panel, not in the bar -- in the bar it was a second
   copy of the brand that no one needed, and wide enough to push the whole bar
   past the viewport at 992-1024px, where the logo's name still fits but a text
   duplicate does not. */
.hx-nav-status {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding-bottom: 0.75rem;
  margin-bottom: 0.25rem;
  border-bottom: 1px solid var(--hx-border);
  color: var(--hx-muted);
  font-size: 10px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
}

/* On a tablet the panel has width to spare, so the name sits alongside the
   first group of links instead of taking a row of its own. */
@media (min-width: 576px) and (max-width: 991.98px) {
  .hx-nav-panel {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
    column-gap: var(--hx-gap-nav);
  }

  .hx-nav-status {
    grid-column: 2;
    grid-row: 1;
    padding-bottom: 0;
    margin-bottom: 0;
    border-bottom: 0;
  }

  .hx-nav-list-primary {
    grid-column: 1;
    grid-row: 1;
    padding-bottom: 0.5rem;
  }

  .hx-nav-list-account {
    grid-column: 1 / -1;
    grid-row: 2;
  }
}

.hx-nav-status-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hx-status-dot {
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--hx-accent);
  box-shadow: 0 0 10px var(--hx-accent);
  animation: hx-pulse 1.8s ease-in-out infinite;
}

@keyframes hx-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.25;
  }
}

/* Toggles that must never be collapsed away, mirroring the behaviour of
   .nav-toggles on the static site: they sit in the bar, not in the panel.
   `order` is what puts them back to the left of the menu button on a phone
   even though the button comes first in the DOM -- the DOM order is the
   keyboard order, and tabbing forward out of an open panel should land on the
   menu button, the way it does in core's bar. */
.hx-nav-toggles {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
  order: 1;
  margin-left: auto;
}

.hx-icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--hx-tap);
  min-height: var(--hx-tap);
  padding: 0 9px;
  background: transparent;
  border: 1px solid var(--hx-border);
  border-radius: 4px;
  color: var(--hx-soft);
  font-family: inherit;
  font-size: 11px;
  letter-spacing: 0.5px;
  cursor: pointer;
  transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
}

.hx-icon-btn:hover,
.hx-icon-btn:focus-visible {
  border-color: var(--hx-accent);
  color: var(--hx-text);
  background: color-mix(in srgb, var(--hx-deep) 45%, transparent);
}

.hx-icon-btn:focus-visible {
  outline: 2px solid var(--hx-accent);
  outline-offset: 2px;
}

.hx-navbar-toggler {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  order: 2;
  min-width: var(--hx-tap);
  min-height: var(--hx-tap);
  padding: 0;
  background: transparent;
  border: 1px solid var(--hx-border);
  border-radius: 4px;
  cursor: pointer;
  transition: border-color 0.2s ease, background 0.2s ease;
}

.hx-navbar-toggler:hover,
.hx-navbar-toggler:focus-visible {
  border-color: var(--hx-accent);
  background: color-mix(in srgb, var(--hx-deep) 45%, transparent);
}

.hx-navbar-toggler:focus-visible {
  outline: 2px solid var(--hx-accent);
  outline-offset: 2px;
}

/* Three bars, drawn rather than three elements, so the open/close transform
   can be driven by aria-expanded alone. */
.hx-toggler-bars {
  position: relative;
  display: block;
  width: 16px;
  height: 1px;
  background: var(--hx-soft);
  box-shadow: 0 -5px 0 var(--hx-soft), 0 5px 0 var(--hx-soft);
  transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}

.hx-navbar-toggler[aria-expanded="true"] .hx-toggler-bars {
  background: transparent;
  box-shadow: none;
}

.hx-navbar-toggler[aria-expanded="true"] .hx-toggler-bars::before,
.hx-navbar-toggler[aria-expanded="true"] .hx-toggler-bars::after {
  content: "";
  position: absolute;
  left: 0;
  width: 16px;
  height: 1px;
  background: var(--hx-soft);
  transition: transform 0.2s ease;
}

.hx-navbar-toggler[aria-expanded="true"] .hx-toggler-bars::before {
  transform: translateY(5px) rotate(45deg);
}

.hx-navbar-toggler[aria-expanded="true"] .hx-toggler-bars::after {
  transform: translateY(-5px) rotate(-45deg);
}

/* The panel. Hidden by default at every width; the media query below makes it
   visible unconditionally on wide screens, so the closed state is only ever a
   mobile state. Full-bleed, with its own Bootstrap container inside so the
   links line up with the brand above them rather than with the viewport. */
.hx-navbar-links {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  max-height: calc(100vh - var(--hx-nav-h));
  max-height: calc(100dvh - var(--hx-nav-h));
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--hx-bg);
  background: color-mix(in srgb, var(--hx-bg) 96%, transparent);
  border-bottom: 1px solid var(--hx-border);
  backdrop-filter: blur(18px);
}

.hx-navbar[data-open] .hx-navbar-links {
  display: block;
}

.hx-nav-panel {
  padding-top: 0.75rem;
  padding-bottom: 1.25rem;
}

.hx-nav-list-primary {
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--hx-border);
}

.hx-nav-list-account {
  padding-top: 0.25rem;
}

.hx-nav-list-account .hx-nav-link {
  color: var(--hx-soft);
}

/* The inline bar. */
@media (min-width: 992px) {
  .hx-navbar-toggler {
    display: none;
  }

  .hx-navbar-links,
  .hx-navbar[data-open] .hx-navbar-links {
    display: block;
    position: static;
    max-height: none;
    overflow: visible;
    flex: 1 1 auto;
    min-width: 0;
    background: none;
    border-bottom: 0;
    backdrop-filter: none;
  }

  /* Inside the panel the wrapper is the alignment container; in the bar it is
     the flex row that holds the two lists. */
  .hx-nav-panel {
    display: flex;
    align-items: center;
    gap: var(--hx-gap-nav);
    padding: 0;
  }

  /* The status line stops at the bar: the CTF name is already spelled out by
     the brand above and the footer below, and in the bar it was a third copy
     wide enough to push the whole bar past the viewport at 992-1024px. */
  .hx-nav-status {
    display: none;
  }

  .hx-nav-list {
    flex-direction: row;
    align-items: center;
    gap: var(--hx-gap-nav);
  }

  .hx-nav-list-primary,
  .hx-nav-list-account {
    padding: 0;
    border: 0;
  }

  .hx-nav-list-account {
    margin-left: auto;
  }

  .hx-nav-link {
    min-height: 0;
    padding: 0.4rem 0;
    font-size: var(--hx-fs-nav);
    gap: 0.35rem;
  }

  /* The underline marker reads as a bar indicator; in a stacked panel it would
     just underline every item. */
  .hx-nav-link.active::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: var(--hx-accent);
  }
}

/* Between the bar breakpoint and the point where the account actions fit with
   their labels, the account list is icons only -- a glyph with the same
   aria-label/title it always had, so nothing is lost but the word. 1200px is
   where the measured content (brand + three links + four actions + two
   toggles) fits inside the container with room to spare. */
@media (min-width: 992px) and (max-width: 1199.98px) {
  .hx-nav-list-account .hx-nav-label {
    display: none;
  }

  /* Icon-only still gets a real target: the label is gone, so the glyph has to
     carry the hit area on its own. */
  .hx-nav-list-account .hx-nav-link {
    justify-content: center;
    gap: 0;
    min-width: 1.75rem;
    min-height: 1.75rem;
    padding-inline: 0.25rem;
  }
}

/* --------------------------------------------------------------------------
   Jumbotron / page headers
   -------------------------------------------------------------------------- */

.jumbotron {
  /* The admin's optional `theme_header` forces the band colour with
     `!important`, so the theme cannot replace it -- and does not try to. It
     sets the same purple itself (so the header still looks like a header with
     no theme_header configured) and adds a gradient on top, which
     `background-color` cannot overwrite. Every colour here is chosen to pass AA
     against the band, in both colour modes. */
  background: var(--hx-band);
  background-image: var(--hx-band-image);
  color: var(--hx-on-band);
  /* Core centres the jumbotron and sizes it in a 576px media query. Both are
     restated here so the header keeps its current look if core changes, and
     because the horizontal padding has to collapse to zero to stay aligned
     with the container above it. */
  text-align: center;
  border-bottom: 1px solid var(--hx-border);
  padding: clamp(1.75rem, 1rem + 3vw, 2.5rem) 0 clamp(1.25rem, 0.85rem + 1.6vw, 1.75rem);
  margin-bottom: 0;
}

.jumbotron h1 {
  font-size: clamp(1.5rem, 1.05rem + 2.2vw, 2.1rem);
  margin: 0;
  color: inherit;
}

/* Core's `.jumbotron { color: var(--bs-white) }` is a direct declaration, so it
   survived the theme's transparent background and put white headings on the
   near-white light-mode page. The theme's own colour above replaces it: the
   heading is white-on-band in light mode as well, where core's --bs-white
   would have been the one readable value but the theme had overridden it with
   near-black, at 2.9:1. */
.hx-page-sub {
  color: var(--hx-on-band-sub);
  font-size: var(--hx-fs-label);
  letter-spacing: 1.6px;
  text-transform: uppercase;
  margin-top: 0.5rem;
  margin-bottom: 0;
  text-wrap: balance;
}

/* --------------------------------------------------------------------------
   Challenge board
   -------------------------------------------------------------------------- */

.category-header {
  border-bottom: 1px solid var(--hx-border);
  padding-bottom: 0.6rem;
}

.category-header h3 {
  color: var(--hx-soft);
  font-size: 11px;
  letter-spacing: 2.4px;
  margin: 0;
}

/* Core ships `[data-bs-theme] .challenge-button` (0,2,0) and
   `[data-bs-theme] .challenge-button.challenge-solved` (0,3,0), the latter
   painting solved tiles #29c830. The old theme rules were (0,1,0) and
   (0,2,0) and simply lost, so solved challenges rendered bright green. Same
   selector shape, later in the cascade, so the theme wins deliberately rather
   than by accident. */
[data-bs-theme] .challenge-button,
[data-bs-theme] .challenge-button.btn-dark {
  position: relative;
  display: block;
  background: var(--hx-surface);
  border: 1px solid var(--hx-border);
  border-radius: 4px;
  color: var(--hx-text);
  text-align: left;
  padding: 0;
  /* The first-blood badge is pinned outside the tile's corner. */
  overflow: visible;
  transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease,
    background-color 0.2s ease;
}

[data-bs-theme] .challenge-button:hover,
[data-bs-theme] .challenge-button:focus-visible {
  border-color: var(--hx-accent);
  background: var(--hx-surface-2);
  transform: translateY(-2px);
  box-shadow: var(--hx-glow);
  color: var(--hx-text);
}

[data-bs-theme] .challenge-button:focus-visible {
  outline: 2px solid var(--hx-accent);
  outline-offset: 2px;
}

.challenge-inner {
  padding: 0 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.challenge-inner p {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 500;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

.challenge-inner span {
  color: var(--hx-muted);
  font-size: 10px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
}

[data-bs-theme] .challenge-button.challenge-solved {
  border-color: color-mix(in srgb, var(--hx-accent) 55%, var(--hx-border));
  background: color-mix(in srgb, var(--hx-deep) 32%, var(--hx-surface));
}

[data-bs-theme] .challenge-button.challenge-solved:hover {
  background: color-mix(in srgb, var(--hx-deep) 42%, var(--hx-surface-2));
}

.challenge-button.challenge-solved .challenge-inner span::after {
  content: " · solved";
  color: var(--hx-accent);
}

/* --------------------------------------------------------------------------
   First Blood
   -------------------------------------------------------------------------- */

.fb-drop {
  width: 18px;
  height: 18px;
  display: inline-block;
  vertical-align: -3px;
  flex: none;
  filter: drop-shadow(0 0 6px rgba(138, 79, 209, 0.55));
}

.fb-drop-lg {
  width: 34px;
  height: 34px;
}

.fb-claim {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.2rem 0.6rem 0.2rem 0.45rem;
  background: color-mix(in srgb, var(--hx-deep) 55%, transparent);
  border: 1px solid color-mix(in srgb, var(--hx-accent) 45%, transparent);
  border-radius: 999px;
  color: var(--hx-soft);
  font-size: 10px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  white-space: nowrap;
}

.fb-claim a {
  color: var(--hx-accent);
  font-weight: 600;
}

.fb-claim a:hover {
  color: var(--hx-text);
  text-decoration: underline;
}

.fb-claim.is-mine {
  border-color: var(--hx-accent);
  color: var(--hx-text);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--hx-accent) 30%, transparent);
}

.fb-claim.is-hidden {
  opacity: 0.65;
  font-style: italic;
}

/* Badge pinned to the corner of a challenge tile. */
.fb-badge {
  position: absolute;
  top: -9px;
  right: -9px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--hx-surface);
  border: 1px solid var(--hx-accent);
  box-shadow: 0 0 12px rgba(138, 79, 209, 0.5);
  z-index: 2;
  /* Decoration only: the button underneath is the control, and a pointer
     target floating over it would swallow taps meant for the challenge. */
  pointer-events: none;
}

.fb-badge .fb-drop {
  width: 14px;
  height: 14px;
}

.fb-badge.is-mine {
  background: var(--hx-accent);
}

.fb-badge.is-mine .fb-drop {
  filter: none;
}

/* Banner inside the challenge modal. */
.fb-banner {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.7rem 0.9rem;
  margin: 0 auto 1rem;
  max-width: 34rem;
  background: color-mix(in srgb, var(--hx-deep) 45%, var(--hx-surface));
  border: 1px solid color-mix(in srgb, var(--hx-accent) 40%, transparent);
  border-radius: 4px;
}

.fb-banner-body {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}

.fb-banner-label {
  color: var(--hx-accent);
  font-size: 9px;
  letter-spacing: 2.2px;
  text-transform: uppercase;
}

.fb-banner-name {
  color: var(--hx-text);
  font-size: 0.9rem;
  font-weight: 500;
  overflow-wrap: anywhere;
}

.fb-banner-time {
  color: var(--hx-muted);
  font-size: 10px;
  letter-spacing: 1px;
}

/* The "field" on the player's own dashboard. */
.fb-field {
  background: var(--hx-surface);
  border: 1px solid var(--hx-border);
  border-radius: 6px;
  overflow: hidden;
}

.fb-field-header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  /* Wraps on its own: no breakpoint needed, and the count drops to its own
     line only when the header genuinely runs out of room. */
  flex-wrap: wrap;
  padding: 0.9rem 1.1rem;
  background: color-mix(in srgb, var(--hx-deep) 40%, var(--hx-surface-2));
  border-bottom: 1px solid var(--hx-border);
}

.fb-field-count {
  margin-left: auto;
  display: inline-flex;
  align-items: baseline;
  gap: 0.35rem;
  color: var(--hx-text);
}

.fb-field-count b {
  font-size: 1.35rem;
  font-weight: 600;
  line-height: 1;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
}

.fb-field-count span {
  color: var(--hx-muted);
  font-size: 9px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
}

.fb-field-body {
  padding: 0.35rem 0;
}

.fb-field-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 1.1rem;
  color: var(--hx-soft);
  border-bottom: 1px solid color-mix(in srgb, var(--hx-border) 60%, transparent);
}

.fb-field-row:last-child {
  border-bottom: 0;
}

.fb-field-row a {
  color: var(--hx-text);
  min-width: 0;
  overflow-wrap: anywhere;
}

.fb-field-row a:hover {
  color: var(--hx-accent);
}

.fb-field-row .fb-field-when {
  margin-left: auto;
  padding-left: 0.5rem;
  color: var(--hx-muted);
  font-size: 10px;
  letter-spacing: 1px;
  white-space: nowrap;
}

.fb-field-empty {
  padding: 1.4rem 1.1rem;
  color: var(--hx-muted);
  font-size: 0.85rem;
  text-align: center;
}

/* --------------------------------------------------------------------------
   Forms, buttons, tables
   -------------------------------------------------------------------------- */

.btn {
  border-radius: 4px;
  font-size: var(--hx-fs-nav);
  font-weight: 600;
  letter-spacing: 1.7px;
  text-transform: uppercase;
  min-height: var(--hx-tap);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
}

.btn-primary {
  --bs-btn-bg: var(--hx-accent);
  --bs-btn-border-color: var(--hx-accent);
  --bs-btn-hover-bg: #9a61df;
  --bs-btn-hover-border-color: #9a61df;
  --bs-btn-active-bg: var(--hx-purple);
  --bs-btn-active-border-color: var(--hx-purple);
  --bs-btn-color: var(--hx-on-accent);
  --bs-btn-hover-color: var(--hx-on-accent);
  --bs-btn-active-color: var(--hx-on-accent);
}

.btn-outline-secondary {
  --bs-btn-color: var(--hx-soft);
  --bs-btn-border-color: var(--hx-border);
  --bs-btn-hover-bg: color-mix(in srgb, var(--hx-deep) 55%, transparent);
  --bs-btn-hover-border-color: var(--hx-accent);
  --bs-btn-hover-color: var(--hx-text);
  --bs-btn-active-bg: var(--hx-purple);
  --bs-btn-active-border-color: var(--hx-accent);
  --bs-btn-active-color: var(--hx-text);
}

.btn-info {
  --bs-btn-bg: var(--hx-purple);
  --bs-btn-border-color: var(--hx-purple);
  --bs-btn-hover-bg: var(--hx-accent);
  --bs-btn-hover-border-color: var(--hx-accent);
  --bs-btn-color: var(--hx-text);
  --bs-btn-hover-color: var(--hx-on-accent);
}

.form-control,
.form-select {
  background-color: var(--hx-surface);
  border: 1px solid var(--hx-border);
  color: var(--hx-text);
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 0.9rem;
  min-height: var(--hx-tap);
  /* Core sets `input, select { padding: .6rem !important; height: auto !important }`.
     Those win over everything short of another !important, so the theme cannot
     make room for the select's chevron by padding alone -- hence the arrow is
     placed with a background-position offset instead. */
  max-width: 100%;
}

.form-control:focus,
.form-select:focus {
  background-color: var(--hx-surface-2);
  border-color: var(--hx-accent);
  color: var(--hx-text);
  box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--hx-accent) 22%, transparent);
}

.form-control::placeholder {
  color: var(--hx-muted);
}

/* Core's `.form-select` arrow is a hard-coded dark grey chevron, and the old
   theme's `background` shorthand wiped the `background-image` out of it
   entirely -- so a themed select rendered as a plain box with no indicator at
   all. Restore it, and give it a palette-matched colour per mode. */
.form-select {
  background-image: var(--hx-select-arrow);
  background-repeat: no-repeat;
  background-position: right 0.7rem center;
  background-size: 10px 12px;
  /* A long option must not push the arrow off the right edge. */
  text-overflow: ellipsis;
}

[data-bs-theme="dark"] {
  --hx-select-arrow: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23b9afcb' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e");
}

[data-bs-theme="light"] {
  --hx-select-arrow: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%234a4257' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e");
}

.form-check-input {
  background-color: var(--hx-surface);
  border-color: var(--hx-border);
}

.form-check-input:checked {
  background-color: var(--hx-accent);
  border-color: var(--hx-accent);
}

.card {
  background: var(--hx-surface);
  border: 1px solid var(--hx-border);
  border-radius: 6px;
}

.table {
  --bs-table-bg: transparent;
  --bs-table-color: var(--hx-soft);
  --bs-table-border-color: var(--hx-border);
  --bs-table-striped-bg: color-mix(in srgb, var(--hx-surface-2) 70%, transparent);
  --bs-table-striped-color: var(--hx-soft);
  --bs-table-hover-bg: color-mix(in srgb, var(--hx-deep) 30%, transparent);
  --bs-table-hover-color: var(--hx-text);
  font-size: var(--hx-fs-body);
  /* The solves and scoreboard tables are the widest things on the site; this
     is what keeps a 60-character account name from scrolling the page. */
  max-width: 100%;
}

.table > :not(caption) > * > * {
  padding: 0.7rem 0.75rem;
  overflow-wrap: anywhere;
}

.table > thead > tr > th {
  color: var(--hx-muted);
  font-weight: 600;
  font-size: 0.75rem;
  letter-spacing: 1.2px;
  text-transform: uppercase;
}

.dropdown-menu {
  background: var(--hx-surface);
  border: 1px solid var(--hx-border);
  border-radius: 4px;
  box-shadow: var(--hx-shadow);
  max-height: min(60vh, 24rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.dropdown-item {
  display: block;
  width: 100%;
  color: var(--hx-soft);
  font-size: 0.85rem;
  min-height: 2.25rem;
  padding-block: 0.4rem;
  cursor: pointer;
}

.dropdown-item:hover,
.dropdown-item:focus {
  background: color-mix(in srgb, var(--hx-deep) 55%, transparent);
  color: var(--hx-text);
}

/* Replaces core's `bg-primary text-white` on the current language, which put a
   Bootstrap blue block in the middle of a purple dropdown. */
.dropdown-item.is-active,
.dropdown-item.is-active:hover {
  background: color-mix(in srgb, var(--hx-accent) 30%, transparent);
  color: var(--hx-text);
  font-weight: 600;
}

/* --------------------------------------------------------------------------
   Modal
   -------------------------------------------------------------------------- */

.modal-content {
  background: var(--hx-surface);
  border: 1px solid var(--hx-border);
  border-radius: 6px;
  box-shadow: var(--hx-shadow);
  color: var(--hx-text);
}

.modal-header,
.modal-footer {
  border-color: var(--hx-border);
  padding-inline: 1rem;
}

.modal-body {
  padding-inline: 1rem;
  /* Long challenge descriptions and pasted flags are the whole point of the
     modal; they must scroll inside it, not stretch the page. */
  overflow-wrap: anywhere;
}

.modal-backdrop.show {
  opacity: 0.75;
  background: var(--hx-bg);
}

.nav-tabs {
  border-bottom-color: var(--hx-border);
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
}

.nav-tabs::-webkit-scrollbar {
  display: none;
}

.nav-tabs .nav-link {
  color: var(--hx-muted);
  font-size: var(--hx-fs-nav);
  letter-spacing: 1.5px;
  text-transform: uppercase;
  white-space: nowrap;
  border: 0;
  border-bottom: 1px solid transparent;
}

.nav-tabs .nav-link:hover {
  color: var(--hx-text);
  border-bottom-color: var(--hx-border);
}

.nav-tabs .nav-link.active {
  color: var(--hx-text);
  background: transparent;
  border-bottom-color: var(--hx-accent);
}

.challenge-name {
  font-size: 1.15rem;
  letter-spacing: 2px;
  text-transform: uppercase;
  overflow-wrap: anywhere;
}

.challenge-value {
  color: var(--hx-accent);
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 0.85rem;
  letter-spacing: 1px;
}

.challenge-tag.badge {
  background: color-mix(in srgb, var(--hx-deep) 60%, transparent) !important;
  border: 1px solid color-mix(in srgb, var(--hx-accent) 40%, transparent);
  color: var(--hx-soft);
  font-size: 9px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
}

.challenge-desc,
.challenge-desc * {
  color: var(--hx-soft);
  line-height: 1.7;
}

.alert {
  border-radius: 4px;
  border-width: 1px;
  font-size: 0.9rem;
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */

.footer {
  /* Undoes core's sticky-footer block: position/bottom/height/line-height/
     z-index all survive into the theme otherwise, which pinned this to the
     bottom of the document at a fixed 60px with 24px of padding inside it. */
  position: static;
  bottom: auto;
  z-index: auto;
  height: auto;
  line-height: inherit;
  width: auto;

  flex: none;
  margin-top: clamp(2rem, 1rem + 4vw, 3rem);
  padding: 1.5rem 0;
  background: var(--hx-surface);
  border-top: 1px solid var(--hx-border);
  color: var(--hx-muted);
  font-size: 10px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
}

/* Core wraps the credit in a <small>, whose 0.875em default lands the smallest
   text on the page at 8.75px. The footer already has its own scale, so the
   nested element inherits it. */
.footer small {
  font-size: inherit;
}

.footer a {
  color: var(--hx-muted);
}

.footer a:hover {
  color: var(--hx-accent);
}

/* --------------------------------------------------------------------------
   Custom cursor

   The static site hides the native pointer and draws a dot with a trailing
   ring. `hx-has-cursor` is added by hidryx.js only once the replacement is
   actually on screen, so a failed script leaves the native cursor intact.
   -------------------------------------------------------------------------- */

.hx-cursor-dot,
.hx-cursor-ring {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10000;
  pointer-events: none;
  border-radius: 50%;
  will-change: transform;
}

.hx-cursor-dot {
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  background: var(--hx-accent);
  box-shadow: 0 0 8px var(--hx-accent);
}

.hx-cursor-ring {
  width: 30px;
  height: 30px;
  margin: -15px 0 0 -15px;
  border: 1px solid rgba(138, 79, 209, 0.6);
  transition: width 0.2s ease, height 0.2s ease, margin 0.2s ease,
    border-color 0.2s ease, opacity 0.2s ease;
}

.hx-cursor-ring.is-hovering {
  width: 44px;
  height: 44px;
  margin: -22px 0 0 -22px;
  border-color: var(--hx-accent);
}

/* Only take the pointer away where there is a real pointer to replace. */
@media (pointer: fine) {
  .hx-has-cursor,
  .hx-has-cursor * {
    cursor: none !important;
  }

  /* Text entry keeps its own caret. Hiding the I-beam inside the flag field and
     the profile forms makes pasting a flag feel broken, and the dot trails well
     behind the pointer while you are typing. */
  .hx-has-cursor input,
  .hx-has-cursor textarea,
  .hx-has-cursor select,
  .hx-has-cursor [contenteditable] {
    cursor: auto !important;
  }
}

/* --------------------------------------------------------------------------
   Capability queries, not width breakpoints
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  body,
  .challenge-button,
  .hx-icon-btn,
  .hx-navbar-toggler,
  .hx-toggler-bars {
    transition: none;
  }

  .hx-cursor-dot,
  .hx-cursor-ring {
    display: none;
  }

  .hx-status-dot {
    animation: none;
  }
}
