/* ============================================================
   Helix Games — App shell
   ----------------------------------------------------------------
   Everything the Bootstrap admin template used to provide, minus the
   2,261KB. It replaces four files that were 85-92% of every page's
   weight (assets/css/vendor.min.css, assets/css/app.min.css,
   assets/js/vendor.min.js, assets/js/app.min.js) with the ~1% of them
   this site actually rendered:

     1. a document reset — the site leaned on Bootstrap Reboot for
        box-sizing, heading and paragraph rhythm, and form-control
        inheritance. Drop it without a replacement and every page
        reflows.
     2. the page background gradient.
     3. the app shell: fixed header, fixed sidebar, content offset,
        and the mobile slide-in + backdrop.
     4. the brand lockup, the hamburger, the sidebar navigation and
        the back-to-top pill.

   Load order: tokens.css -> app-shell.css -> themes.css ->
   atmosphere.css -> page stylesheet. Tokens first because everything
   here reads them; page stylesheets last so they always win.

   Behaviour lives in shared/app-shell.js; icons in shared/icons.js.
   ============================================================ */


/* ─────────────────────────────────────────────────────────────
   1. Document reset
   The subset of Bootstrap Reboot that this site's CSS was written
   against. Values are the ones that were previously computed, not
   fresh choices — changing them here would move type on 22 pages.
   ───────────────────────────────────────────────────────────── */
*,
*::before,
*::after { box-sizing: border-box; }

@media (prefers-reduced-motion: no-preference) {
  :root { scroll-behavior: smooth; }
}

body {
  margin: 0;
  /* font-family comes from tokens.css (html, body). */
  font-size: 0.875rem;
  font-weight: 300;
  line-height: 1.5;
  letter-spacing: 0.046875rem;
  color: var(--helix-text-body);
  background-color: transparent;
  min-height: 100vh;
  position: relative;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

h1, h2, h3, h4, h5, h6 {
  margin-top: 0;
  margin-bottom: 0.5rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--helix-text);
}
h1 { font-size: calc(1.34375rem + 1.125vw); }
h2 { font-size: calc(1.3rem + 0.6vw); }
h3 { font-size: calc(1.278125rem + 0.3375vw); }
h4 { font-size: calc(1.25625rem + 0.075vw); }
h5 { font-size: 1.09375rem; }
h6 { font-size: 0.875rem; }
@media (min-width: 1200px) {
  h1 { font-size: 2.1875rem; }
  h2 { font-size: 1.75rem; }
  h3 { font-size: 1.53125rem; }
  h4 { font-size: 1.3125rem; }
}

p { margin-top: 0; margin-bottom: 1rem; }
ul, ol { padding-left: 2rem; margin-top: 0; margin-bottom: 1rem; }
ul ul, ul ol, ol ul, ol ol { margin-bottom: 0; }
b, strong { font-weight: bolder; }
hr { margin: 1rem 0; color: inherit; border: 0; border-top: 1px solid; opacity: 0.25; }

a { color: var(--helix-primary); text-decoration: underline; }
a:hover { color: rgba(var(--helix-primary-rgb), 0.75); }

img, svg { vertical-align: middle; }
label { display: inline-block; }

table { caption-side: bottom; border-collapse: collapse; }
th { text-align: inherit; }
thead, tbody, tfoot, tr, th, td { border-color: inherit; border-style: solid; border-width: 0; }

button, input, optgroup, select, textarea {
  margin: 0;
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
}
button, select { text-transform: none; }
button { border-radius: 0; }
button:focus:not(:focus-visible) { outline: 0; }
button:not(:disabled),
[type="button"]:not(:disabled),
[type="submit"]:not(:disabled) { cursor: pointer; }
textarea { resize: vertical; }

[hidden] { display: none !important; }

/* Screen-reader-only. Global because a11y helpers on any page may
   need it, including the game pages that never load admin-pages.css. */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}


/* ─────────────────────────────────────────────────────────────
   2. Page background
   One gradient on the root element, sized to the document.

   The template painted this three times over: a tiled gradient on
   <html>, a viewport-fixed copy on html::before, and a 109KB stock
   photo on html::after that only ever showed through at 10%. Because
   the fixed copy covered a viewport and the tiled one repeated every
   viewport-height, long pages got a visible seam every 900px (look
   for it at y=897 and y=1794 in the old index baseline). Letting
   <html> size to its content and painting once removes both the seam
   and the photo.
   ───────────────────────────────────────────────────────────── */
html {
  min-height: 100%;
  background-color: var(--helix-bg-page-end);
  background-image: linear-gradient(
    180deg,
    var(--helix-bg-page) 0%,
    var(--helix-bg-page-end) 100%
  );
  background-repeat: no-repeat;
}


/* ─────────────────────────────────────────────────────────────
   3. App shell
   .app pads by the header height; .app-content is inset by the
   sidebar width on desktop and full-bleed on mobile, where the
   sidebar becomes an off-canvas panel.
   ───────────────────────────────────────────────────────────── */
.app {
  padding-top: var(--helix-header-height);
  /* A flex column so the footer (section 10) can sit at the bottom of a
     short page. The fixed chrome is out of flow and unaffected. */
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.app-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 1020;
  height: var(--helix-header-height);
  display: flex;
  background: var(--helix-surface-header);
  /* The template's header floated over the page with no edge at all.
     An inset shadow rather than a border, so the 1px rule does not eat
     into the 52px content box that .brand and .mobile-toggler size
     themselves against. */
  box-shadow: inset 0 -1px 0 rgba(var(--helix-primary-rgb), 0.18);
}

.app-content {
  padding: 2rem;
}

/* Only reserve the sidebar gutter on pages that actually have a sidebar.
   The template applied this unconditionally, so index.html and all 15 game
   pages left a 270px empty column on the left and sat ~135px right of centre.
   The sidebar always precedes .app-content in the DOM, so a sibling selector
   scopes it without needing a body class on every page. */
.app-sidebar ~ .app-content {
  margin-left: var(--helix-sidebar-width);
}

.app-sidebar {
  position: fixed;
  top: var(--helix-header-height);
  bottom: 0;
  left: 0;
  z-index: 1010;
  width: var(--helix-sidebar-width);
  box-shadow: inset -1px 0 0 rgba(var(--helix-primary-rgb), 0.14);
}
.app-sidebar-content {
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}

/* Mobile backdrop. A <button> so it is a real dismiss control rather
   than a div with a click handler; hidden from the tab order by
   shared/app-shell.js while the sidebar is closed. */
.app-sidebar-mobile-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1020;
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  border: 0;
  outline: 0;
  display: none;
  background: var(--helix-scrim);
}

@media (max-width: 767.98px) {
  .app-content { padding: 1rem; }
  /* Same specificity as the desktop sibling rule above, or it would not win. */
  .app-sidebar ~ .app-content { margin-left: 0; }

  .app-sidebar {
    top: 0;
    left: calc(-1 * var(--helix-sidebar-width));
    z-index: 1030;
    background: var(--helix-surface-sidebar);
    box-shadow: inset -1px 0 0 rgba(var(--helix-primary-rgb), 0.3);
    transition: left 0.18s ease;
  }
  .app-sidebar-mobile-toggled .app-sidebar { left: 0; }
  .app-sidebar-mobile-toggled .app-sidebar-mobile-backdrop { display: block; }
}


/* ─────────────────────────────────────────────────────────────
   4. Brand lockup
   A teal H inside white corner brackets — the same mark as
   assets/img/helix-icon.svg. The brackets used to be a 3KB PNG; they
   are now a masked border, so they inherit colour and stay crisp at
   any density. The mark itself is unscoped: the footer (section 10)
   reuses it at a smaller size.
   ───────────────────────────────────────────────────────────── */
.app-header .brand {
  height: 100%;
  width: 18.875rem;
  padding: 0 1rem;
  display: flex;
  align-items: center;
}
.app-header .brand-logo {
  display: flex;
  align-items: center;
  color: var(--helix-text);
  font-size: 0.91875rem;
  font-weight: 300;
  letter-spacing: 2px;
  text-decoration: none;
}
.brand-img {
  position: relative;
  width: 1.625rem;
  height: 1.625rem;
  margin-right: 1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
/* Four corner brackets: a full border masked down to its corners. Kept on a
   pseudo-element so the mask never touches the "H". Arms are 28.85% of the
   edge and the stroke 4.81% of it — the proportions scripts/build-icons.py
   records for the mark, so the header lockup and the favicon stay the same
   shape. */
.brand-img::before {
  content: '';
  position: absolute;
  inset: 0;
  border: 1.25px solid var(--helix-text);
  -webkit-mask:
    linear-gradient(#000 0 0) top left / 28.85% 28.85% no-repeat,
    linear-gradient(#000 0 0) top right / 28.85% 28.85% no-repeat,
    linear-gradient(#000 0 0) bottom left / 28.85% 28.85% no-repeat,
    linear-gradient(#000 0 0) bottom right / 28.85% 28.85% no-repeat;
  mask:
    linear-gradient(#000 0 0) top left / 28.85% 28.85% no-repeat,
    linear-gradient(#000 0 0) top right / 28.85% 28.85% no-repeat,
    linear-gradient(#000 0 0) bottom left / 28.85% 28.85% no-repeat,
    linear-gradient(#000 0 0) bottom right / 28.85% 28.85% no-repeat;
}
.brand-img-text {
  display: block;
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: initial;
}

@media (max-width: 767.98px) {
  .app-header .brand { display: none; }
}


/* ─────────────────────────────────────────────────────────────
   5. Header controls — hamburger + the auth/theme mount point
   ───────────────────────────────────────────────────────────── */
.app-header .menu {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  /* The template gave the header no right gutter, so the Sign Up
     button's right edge landed at exactly x=1280 / x=390. */
  padding-right: 1rem;
}

.app-header .mobile-toggler {
  height: 100%;
  display: none;
  margin-right: auto;
}
@media (max-width: 767.98px) {
  .app-header .mobile-toggler { display: flex; align-items: center; }
}

.menu-toggler {
  height: 100%;
  padding: 0.5rem 1rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  cursor: pointer;
}
.menu-toggler .bar {
  display: block;
  width: 1.625rem;
  height: 1px;
  background: var(--helix-rule);
  transition: background 0.2s linear;
}
.menu-toggler .bar + .bar { margin-top: 0.25rem; }
.menu-toggler:hover .bar,
.menu-toggler:focus-visible .bar { background: var(--helix-primary); }


/* ─────────────────────────────────────────────────────────────
   6. Sidebar navigation
   Markup comes from shared/components.js. Geometry matches what the
   template produced — the 30px icon gutter and 0.3rem row padding set
   the page's left margin, so moving them reflows every sidebar page.
   The states are new: hover borrows the games dropdown's orange wash
   so the two navigations read as one system, and "you are here" is
   the calm teal plus a marker rule.
   ───────────────────────────────────────────────────────────── */
.app-sidebar .menu { position: relative; }

.app-sidebar .menu-header {
  padding: 1rem 1rem 0.5rem;
  font-family: var(--helix-font-body);
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 3px;
  text-transform: uppercase;
  /* The template shipped this at rgba(255,255,255,.3) — 2.3:1, well under
     AA for 10.5px text. 0.6 is the quietest step that still clears it. */
  color: var(--helix-text-muted);
}

.app-sidebar .menu-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.app-sidebar .menu-item { position: relative; }

.app-sidebar .menu-link {
  position: relative;
  /* The 2px the marker rule occupies comes out of the left padding, so an
     item's contents sit at the same x whether or not it is the active one
     — and at the same x the template put them. */
  padding: 0.3rem 1rem 0.3rem calc(1rem - 2px);
  display: flex;
  align-items: center;
  /* Template value was rgba(255,255,255,.5) = 3.8:1 against the light end
     of the page gradient the sidebar sits over. */
  color: var(--helix-text-body);
  text-decoration: none;
  border-left: 2px solid transparent;
  transition: color 0.15s ease-in-out, background 0.15s ease-in-out;
}
.app-sidebar .menu-link:hover,
.app-sidebar .menu-link:focus-visible {
  color: var(--helix-secondary);
  background: var(--helix-secondary-hover);
}

.app-sidebar .menu-icon {
  position: relative;
  width: 1.875rem;
  height: 1.875rem;
  margin: -0.25rem 0.75rem -0.25rem 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.05rem;
  color: inherit;
}
.app-sidebar .menu-text { flex: 1; }

.app-sidebar .menu-item.active > .menu-link {
  color: var(--helix-primary);
  border-left-color: var(--helix-primary);
  background: rgba(var(--helix-primary-rgb), 0.07);
}
.app-sidebar .menu-item.active > .menu-link:hover,
.app-sidebar .menu-item.active > .menu-link:focus-visible {
  color: var(--helix-secondary);
  border-left-color: var(--helix-secondary);
}


/* ─────────────────────────────────────────────────────────────
   7. Back-to-top pill
   `.fade` is kept as the template named it so the 22 call sites did
   not have to change; shared/app-shell.js adds `.show` past 200px.
   ───────────────────────────────────────────────────────────── */
.btn-scroll-top {
  position: fixed;
  right: 1.875rem;
  bottom: 1.875rem;
  z-index: 1020;
  width: 2.5rem;
  height: 2.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--helix-radius-pill);
  background: var(--helix-surface-raised);
  color: var(--helix-text);
  text-align: center;
  text-decoration: none;
  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out;
}
.btn-scroll-top:hover,
.btn-scroll-top:focus-visible {
  background: var(--helix-primary);
  color: var(--helix-bg-page-end);
  text-decoration: none;
}
@media (max-width: 767.98px) {
  .btn-scroll-top { right: 1.25rem; bottom: 1.25rem; }
}

.fade { transition: opacity 0.15s linear; }
/* Also drop the hit target: the template left a 40x40 invisible link
   sitting over the bottom-right corner of every page. */
.fade:not(.show) { opacity: 0; pointer-events: none; }


/* ─────────────────────────────────────────────────────────────
   8. Icons
   Sprite + helper in shared/icons.js. One em-square box so an icon
   swaps in wherever the old font glyph sat, with the same optical
   size and baseline.
   ───────────────────────────────────────────────────────────── */
.hx-icon {
  width: 1em;
  height: 1em;
  display: inline-block;
  flex: none;
  vertical-align: -0.125em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.3;
  stroke-linecap: round;
  stroke-linejoin: round;
  overflow: visible;
}


/* ─────────────────────────────────────────────────────────────
   9. Utilities carried over from the template
   ───────────────────────────────────────────────────────────── */
.text-theme { color: var(--helix-primary) !important; }

/* ─────────────────────────────────────────────────────────────
   10. Footer
   Follows .app-content on all 22 pages. Section 3's flex column
   parks it at the bottom of a short page, on the dark end of the
   gradient its colours were checked against. Inset by the sidebar
   like .app-content; above the vignette like the header, so the
   1px rule keeps 3:1 in the corners. Reuses the section 4 mark.
   ───────────────────────────────────────────────────────────── */
.app-footer {
  position: relative;
  z-index: 2;
  margin-top: auto;
  /* Right padding clears the back-to-top pill (section 7) by 1rem. */
  padding: 1rem 5.375rem calc(1rem + env(safe-area-inset-bottom, 0px)) 2rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.25rem 1.5rem;
  border-top: 1px solid var(--helix-rule);
  font-size: 0.75rem;
  color: var(--helix-text-body);
}
.app-sidebar ~ .app-footer { margin-left: var(--helix-sidebar-width); }

.footer-brand {
  display: flex;
  align-items: center;
  font-family: var(--helix-font-display);
  letter-spacing: 2px;
  color: var(--helix-text);
}
.footer-brand .brand-img { width: 1.125rem; height: 1.125rem; margin-right: 0.5rem; }
.footer-brand .brand-img-text { font-size: 0.7rem; }

/* 44px hit boxes around 12px labels. The row wraps on a phone, where
   the negative margin lines the first label up with the brand. */
.footer-nav { margin: 0 -0.625rem; display: flex; flex-wrap: wrap; }
.footer-nav a {
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding: 0 0.625rem;
  display: inline-flex;
  align-items: center;
  color: inherit;
  text-transform: uppercase;
  text-decoration: none;
}
.footer-nav a:hover,
.footer-nav a:focus-visible,
.footer-nav a[aria-current="page"] { color: var(--helix-primary); }

.footer-legal { margin: 0; color: var(--helix-text-muted); }

/* Room for the START / RESTART pill shared/touch.js pins to the bottom
   of a phone's viewport outside of play (84px + 12px, plus 1rem clear). */
.app:has(.helix-touch-system:not([data-state="playing"]):not([hidden])) .app-footer {
  padding-bottom: calc(7rem + env(safe-area-inset-bottom, 0px));
}

@media (max-width: 767.98px) {
  .app-footer { padding-left: 1rem; padding-right: 1rem; }
  .app-sidebar ~ .app-footer { margin-left: 0; }
}
