body.launcher-active { overflow: auto; }
[hidden] { display: none !important; }
body.launcher-active > :not(#launcher) { display: none !important; }
/* ODST PASS (user: "look more like Halo ODST's menu", just a tad).
   Three things carry that look, and they are all cheap:
   1. AMBER LEADS. The old palette was cyan-primary; VISR is amber-primary with
      cool blue as the *informational* colour. Swapping the hierarchy — not the
      hues — does most of the work on its own.
   2. NOTHING HAS A SQUARE CORNER. Halo's UI chamfers panels and buttons and
      hangs corner brackets off whatever has focus.
   3. IT IS RAINING. The one image everybody actually remembers from that menu.
   Deliberately NOT done: no webfont (an external font is a request the page
   does not need and a flash of unstyled text), and no markup changes — every
   class and hook below already existed. */
#launcher {
  --hub-bg: #04070a; --hub-panel: #080d13; --hub-line: #1e2b38;
  --hub-ink: #dce8ef; --hub-muted: #8b9dab;
  /* amber is the ACTION colour now; cyan reads as data, the way VISR splits them */
  --hub-amber: #f2a93b; --hub-amber-dim: #8a6428;
  --hub-cyan: #7fc4d8; --hub-red: #c14a37; --hub-green: #85c79d;
  /* FLOOD GREEN, for the one line that says who won (user: "make it the sickly
     flood green yellow").
     Honest about where this came from: the project has no sickly tone to
     derive it from. Its only flood colours are the map's vivid #51ff6a dot,
     the mint #7fd1a0 log lines, the 0x8fef5a hit-flash and violet for
     conversion — all healthy or hot. So this is a deliberate pick, chosen off
     a contact sheet at the real 96px against the real background rather than
     by eye: bilious green-yellow, unhealthy but still legible. Pushing further
     yellow reads as a highlighter; further olive muddies at display size.
     Deliberately NOT --hub-red: that still means "error" on this page, and a
     green error state reads as success. */
  --hub-flood: #b7c24a;
  --hub-chamfer: 9px;
  /* condensed technical caps, from what the OS already has — Halo's UI voice */
  --hub-face: "Bahnschrift", "DIN Alternate", "Roboto Condensed", "Arial Narrow", "Helvetica Neue", system-ui, sans-serif;
  position: relative; min-height: 100vh; z-index: 100; overflow: hidden;
  background:
    linear-gradient(rgba(127,196,216,.02) 1px, transparent 1px),
    linear-gradient(90deg, rgba(127,196,216,.02) 1px, transparent 1px),
    radial-gradient(ellipse at 68% 8%, rgba(242,169,59,.07), transparent 46%),
    radial-gradient(circle at 73% 18%, rgba(40,74,92,.18), transparent 32%),
    var(--hub-bg);
  background-size: 44px 44px, 44px 44px, auto, auto, auto;
  color: var(--hub-ink); font: 14px/1.55 "SF Mono", "Cascadia Code", Consolas, monospace;
}
/* DUST, NOT RAIN (user: "No raining. The dust effect from the game itself is
   good."). Rain was the wrong borrow — it belongs to ODST's city, and this menu
   opens onto a sealed drifting warship where the air does not go anywhere.
   So this matches the game's own motes (game/main.js) rather than inventing a
   second look: soft round specks, the same cool grey 0x9daabb, tiny, sparse,
   and barely moving.
   Three tiled layers instead of one. A single tiled dot field reads as a GRID
   the moment the eye catches two neighbours; three layers on co-prime tile
   sizes drifting at different rates and directions never line up, so it reads
   as scatter. Slow on purpose — the game's motes wander on 0.22-0.3 rad/s
   sines, so anything quick here would look like a screensaver next to it. */
#launcher::after {
  content: ""; position: fixed; inset: -25%; pointer-events: none; z-index: 0;
  background-image:
    radial-gradient(circle 1.6px at 22% 31%, rgba(157,170,187,.50), transparent 100%),
    radial-gradient(circle 1.1px at 68% 74%, rgba(157,170,187,.38), transparent 100%),
    radial-gradient(circle 2.0px at 44% 12%, rgba(157,170,187,.28), transparent 100%);
  background-size: 227px 181px, 313px 263px, 173px 431px;
  animation: hub-dust 84s linear infinite;
}
/* ONE animation driving all three layers, not three animations — they would
   all be writing background-position and the last declared would simply win,
   leaving the other two frozen. Each layer instead gets its own travel vector
   inside the single keyframe, and each vector is an exact whole number of that
   layer's own tiles (1x1, 1x1, 1x2), so the wrap is seamless and the three
   speeds and directions still disagree. */
@keyframes hub-dust {
  to { background-position: 227px -181px, -313px -263px, 173px -862px; }
}
.hub-shell, .hub-top { position: relative; z-index: 1; }
#launcher[hidden] { display: none; }
#launcher::before {
  content: ""; position: fixed; inset: 0; pointer-events: none; opacity: .3;
  background: repeating-linear-gradient(0deg, transparent 0 3px, rgba(0,0,0,.18) 3px 4px);
}
.hub-top {
  position: relative; display: flex; align-items: center; gap: 24px;
  min-height: 72px; padding: 14px clamp(20px, 5vw, 72px); border-bottom: 1px solid var(--hub-line);
  background: rgba(5,8,12,.86); backdrop-filter: blur(12px);
}
.hub-brand { display: flex; align-items: center; gap: 12px; min-width: 210px; }
/* the brand diamond reads as a lit reticle rather than an outline */
.hub-mark {
  width: 28px; height: 28px; border: 1px solid var(--hub-amber); transform: rotate(45deg);
  box-shadow: inset 0 0 0 5px var(--hub-bg), inset 0 0 0 6px rgba(242,169,59,.5),
              0 0 16px rgba(242,169,59,.28);
}
.hub-brand strong { display: block; color: #f1f7fa; font-size: 14px; letter-spacing: .28em; font-family: var(--hub-face); }
.hub-brand span { display: block; color: var(--hub-muted); font-size: 9px; letter-spacing: .14em; }
.hub-nav { margin-left: auto; display: flex; gap: 6px; }
.hub-nav button, .hub-link {
  min-height: 44px; border: 0; background: transparent; color: var(--hub-muted); cursor: pointer;
  padding: 8px 13px; font: 11px/1 monospace; letter-spacing: .13em; text-transform: uppercase;
}
.hub-nav button:hover, .hub-link:hover { color: var(--hub-cyan); }
.hub-shell { position: relative; width: min(1180px, calc(100% - 40px)); margin: 0 auto; padding: clamp(42px, 8vh, 92px) 0 46px; }
[data-launch-page][hidden], [data-doc-panel][hidden] { display: none !important; }
/* every kicker gets a lit tick in front of it — the small "this line is live"
   tell that Halo's menus put beside a heading */
.hub-kicker {
  color: var(--hub-amber); font-size: 11px; letter-spacing: .2em; text-transform: uppercase;
  display: flex; align-items: center; gap: 9px; text-shadow: 0 0 12px rgba(242,169,59,.4);
}
.hub-kicker::before {
  content: ""; width: 7px; height: 7px; flex: none; background: var(--hub-amber);
  transform: rotate(45deg); box-shadow: 0 0 9px rgba(242,169,59,.7);
}
.hub-hero { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(300px, .8fr); gap: clamp(36px, 7vw, 90px); align-items: end; min-height: 52vh; }
/* The old hero was a 300-weight editorial headline on NEGATIVE tracking, which
   is the opposite voice: Halo sets its titles condensed, heavy and OPEN, so the
   caps read as stencilled onto equipment rather than set in a magazine. */
.hub-hero h1, .hub-page-title {
  margin: 14px 0 18px; max-width: 900px; color: #f3f8fa;
  font: 600 clamp(44px, 7.4vw, 96px)/.92 var(--hub-face);
  letter-spacing: .012em; text-transform: uppercase;
  text-shadow: 0 0 34px rgba(120,175,205,.16);
}
/* the amber rule under the title — Halo underscores its headings, it is not decoration */
.hub-hero h1::after, .hub-page-title::after {
  content: ""; display: block; width: 210px; height: 3px; margin-top: 22px;
  background: linear-gradient(90deg, var(--hub-amber) 0 42%, rgba(242,169,59,.25) 42% 62%, transparent);
  box-shadow: 0 0 18px rgba(242,169,59,.55);
}
.hub-hero h1:focus, .hub-page-title:focus { outline: none; }
.hub-hero h1 span { display: block; color: var(--hub-flood); }
.hub-deck { max-width: 690px; color: #9db0bc; font: 17px/1.65 system-ui, sans-serif; }
.hub-actions { display: grid; gap: 10px; margin-top: 34px; max-width: 590px; }
/* CHAMFERED, with a lit rail on the leading edge that fills on hover. The
   clip-path takes the top-left and bottom-right corners off, which is the
   Halo panel shape; the rail is what makes a selection read at a glance
   without moving anything (the old +3px slide nudged the whole list). */
.hub-button {
  position: relative; width: 100%; min-height: 58px;
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  border: 1px solid var(--hub-line); background: rgba(8,13,19,.8); color: var(--hub-ink);
  padding: 12px 16px 12px 22px; cursor: pointer; text-align: left; font: inherit;
  clip-path: polygon(var(--hub-chamfer) 0, 100% 0, 100% calc(100% - var(--hub-chamfer)),
                     calc(100% - var(--hub-chamfer)) 100%, 0 100%, 0 var(--hub-chamfer));
  transition: border-color .16s, background .16s, color .16s;
}
.hub-button::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--hub-amber-dim); transition: background .16s, box-shadow .16s;
}
.hub-button:hover, .hub-button:focus-visible {
  border-color: rgba(242,169,59,.55); background: #101823; outline: none;
}
.hub-button:hover::before, .hub-button:focus-visible::before {
  background: var(--hub-amber); box-shadow: 0 0 16px rgba(242,169,59,.75);
}
.hub-button:hover em, .hub-button:focus-visible em { color: var(--hub-amber); }
.hub-button.primary { border-color: rgba(242,169,59,.42); background: rgba(58,40,14,.34); }
.hub-button.primary::before { background: var(--hub-amber); box-shadow: 0 0 14px rgba(242,169,59,.6); }
.hub-button .num { color: var(--hub-amber-dim); font-size: 10px; letter-spacing: .1em; }
.hub-button b { flex: 1; letter-spacing: .16em; font-size: 13px; font-family: var(--hub-face); font-weight: 600; }
.hub-button em { color: var(--hub-cyan); font-style: normal; font-size: 11px; letter-spacing: .12em; }
/* the readout column: a ticked rail instead of a plain border, and the VALUES
   in cyan — this is the data half of the VISR split, so it must not compete
   with the amber actions for attention */
.hub-telemetry {
  align-self: stretch; padding-left: 24px; display: flex; flex-direction: column; justify-content: flex-end;
  border-left: 1px solid var(--hub-line);
  background: repeating-linear-gradient(180deg, var(--hub-line) 0 8px, transparent 8px 22px) left / 1px 100% no-repeat;
}
.hub-telemetry h2 { color: var(--hub-amber); font-size: 10px; letter-spacing: .22em; margin-bottom: 14px; }
.telemetry-row { display: grid; grid-template-columns: 95px 1fr; gap: 16px; padding: 9px 0; border-top: 1px solid rgba(30,43,56,.8); }
.telemetry-row dt { color: #7e909d; font-size: 10px; letter-spacing: .09em; text-transform: uppercase; }
.telemetry-row dd { color: var(--hub-cyan); letter-spacing: .04em; }
#menu-notice { margin-top: 18px; color: var(--hub-red); }
.hub-page-title { font-size: clamp(44px, 7vw, 82px); line-height: .95; }
.hub-copy { max-width: 760px; color: #9db0bc; font: 16px/1.75 system-ui, sans-serif; }
.hub-copy p + p { margin-top: 18px; }
.hub-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 34px; }
.hub-card { border: 1px solid var(--hub-line); background: rgba(10,16,23,.72); padding: 22px; min-height: 170px; }
.hub-card .index { color: var(--hub-red); font-size: 10px; letter-spacing: .14em; }
.hub-card h2 { margin: 24px 0 10px; color: #e5eef2; font-size: 14px; letter-spacing: .1em; }
.hub-card p { color: var(--hub-muted); font: 14px/1.6 system-ui, sans-serif; }
.hub-form { max-width: 760px; margin-top: 30px; border: 1px solid var(--hub-line); background: rgba(10,16,23,.78); padding: 24px; }
.hub-label { display: block; color: var(--hub-muted); font-size: 10px; letter-spacing: .14em; margin: 0 0 7px; text-transform: uppercase; }
.hub-input { width: 100%; min-height: 49px; background: #060a0f; border: 1px solid #304152; color: #edf5f7; padding: 11px 13px; font: 14px monospace; outline: none; }
.hub-input:focus { border-color: var(--hub-cyan); box-shadow: 0 0 0 2px rgba(121,216,223,.1); }
.hub-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 16px; }
.hub-form .hub-button { margin-top: 10px; }
.hub-note { color: #94a8b4; font-size: 11px; margin-top: 10px; }
.hub-error { color: var(--hub-red); margin-top: 9px; font-size: 12px; }
.hub-back { margin-bottom: 22px; width: auto; display: inline-flex; }
.lobby-layout { display: grid; grid-template-columns: 1fr 330px; gap: 28px; align-items: start; }
.lobby-panel { border: 1px solid var(--hub-line); background: rgba(10,16,23,.82); padding: 24px; }
.lobby-head { display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--hub-line); padding-bottom: 14px; }
.lobby-head h2 { color: var(--hub-cyan); letter-spacing: .14em; font-size: 12px; }
#lobby-count { color: var(--hub-muted); }
#lobby-roster { list-style: none; margin: 8px 0 22px; padding: 0; }
#lobby-roster li { display: flex; align-items: center; min-height: 48px; gap: 11px; border-bottom: 1px solid rgba(35,49,64,.65); }
.roster-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--hub-green); box-shadow: 0 0 10px rgba(133,199,157,.55); }
.roster-name { flex: 1; }
.roster-role { color: var(--hub-muted); font-size: 10px; text-transform: uppercase; letter-spacing: .12em; }
#lobby-status { min-height: 40px; color: var(--hub-muted); }
#lobby-status[data-tone="ok"] { color: var(--hub-green); }
#lobby-status[data-tone="error"] { color: var(--hub-red); }
.transport-pill { display: inline-flex; border: 1px solid #2f4852; color: var(--hub-cyan); padding: 5px 8px; font-size: 9px; letter-spacing: .13em; }
.invite-copy { display: flex; gap: 8px; margin-top: 8px; }
.invite-copy .hub-input { flex: 1; }
.mini-button { min-height: 44px; border: 1px solid #3a5060; background: #101a22; color: var(--hub-ink); padding: 8px 12px; font: 10px monospace; cursor: pointer; }
.mini-button:hover { border-color: var(--hub-cyan); }
.voice-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--hub-line); }
#voice-state { color: var(--hub-muted); font-size: 11px; }
.docs-layout { display: grid; grid-template-columns: 220px 1fr; gap: 34px; margin-top: 32px; }
.docs-tabs { display: flex; flex-direction: column; gap: 5px; }
.docs-tabs button { min-height: 44px; border: 1px solid transparent; background: transparent; color: var(--hub-muted); text-align: left; padding: 8px 12px; font: 11px monospace; cursor: pointer; }
.docs-tabs button[aria-selected="true"] { color: var(--hub-cyan); border-color: var(--hub-line); background: var(--hub-panel); }
.docs-panel { max-width: 790px; border-left: 1px solid var(--hub-line); padding-left: 30px; }
.docs-panel h2 { color: #edf5f7; font: 300 30px system-ui, sans-serif; margin-bottom: 16px; }
.docs-panel h3 { color: var(--hub-amber); font-size: 12px; letter-spacing: .1em; margin: 28px 0 8px; }
.docs-panel p, .docs-panel li { color: #91a5b1; font: 14px/1.7 system-ui, sans-serif; }
.docs-panel ul { padding-left: 20px; }
.docs-panel code { color: var(--hub-cyan); font: 12px monospace; }
.docs-panel pre { overflow: auto; margin-top: 12px; border: 1px solid var(--hub-line); background: #05090d; color: #b7cad3; padding: 16px; font: 12px/1.6 monospace; }
.hub-footer { margin-top: 72px; padding-top: 18px; border-top: 1px solid var(--hub-line); color: #8da0ac; font-size: 10px; letter-spacing: .1em; display: flex; justify-content: space-between; }
#networkHud { left: 14px; bottom: 174px; display: flex; align-items: center; gap: 8px; pointer-events: auto; }
#networkHud[hidden] { display: none; }
#networkHud span { background: rgba(5,9,14,.86); border: 1px solid #314755; color: #87d9df; padding: 5px 8px; font-size: 10px; letter-spacing: .1em; }
#gameVoice { min-height: 44px; background: rgba(5,9,14,.9); border: 1px solid #314755; color: #dce8ef; padding: 5px 9px; font: 10px monospace; cursor: pointer; }
/* --- the shared Halo panel shape, applied to everything boxy ------------- */
.hub-card, .lobby-panel, .hub-form, .docs-panel pre, .hub-input, .mini-button {
  clip-path: polygon(var(--hub-chamfer) 0, 100% 0, 100% calc(100% - var(--hub-chamfer)),
                     calc(100% - var(--hub-chamfer)) 100%, 0 100%, 0 var(--hub-chamfer));
}
.hub-card { border-color: rgba(30,43,56,.9); }
.hub-card .index { color: var(--hub-amber); }
.hub-card h2 { font-family: var(--hub-face); letter-spacing: .14em; font-weight: 600; }
.hub-input:focus { border-color: var(--hub-amber); box-shadow: 0 0 0 2px rgba(242,169,59,.12); }
.lobby-head h2, .docs-panel h3 { color: var(--hub-amber); font-family: var(--hub-face); letter-spacing: .16em; }
.docs-tabs button[aria-selected="true"] { color: var(--hub-amber); border-color: rgba(242,169,59,.4); }
.transport-pill { border-color: rgba(242,169,59,.35); color: var(--hub-amber); }
.hub-nav button:hover, .hub-link:hover { color: var(--hub-amber); }

/* CORNER BRACKETS on the thing you are about to do. Halo hangs these off the
   focused element; here they mark the primary action so the eye lands on it
   before it reads a single word. Drawn with two borders on ::after so they
   cost no markup and cannot affect layout. */
.hub-actions { position: relative; }
.hub-button.primary::after {
  content: ""; position: absolute; inset: -5px; pointer-events: none;
  border: 1px solid transparent;
  border-image: linear-gradient(135deg, var(--hub-amber) 0 14%, transparent 14% 86%, var(--hub-amber) 86%) 1;
  opacity: .75;
}
@media (prefers-reduced-motion: reduce) {
  #launcher::after { animation: none; }
}

@media (max-width: 820px) {
  .hub-nav { display: none; }
  .hub-hero, .lobby-layout, .docs-layout { grid-template-columns: 1fr; }
  .hub-hero { align-items: start; }
  .hub-telemetry { border-left: 0; border-top: 1px solid var(--hub-line); padding: 22px 0 0; }
  .hub-grid { grid-template-columns: 1fr; }
  .docs-panel { border-left: 0; border-top: 1px solid var(--hub-line); padding: 24px 0 0; }
  .docs-tabs { flex-direction: row; overflow-x: auto; }
  .hub-form-row { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
  .hub-shell { width: min(100% - 28px, 1180px); padding-top: 36px; }
  .hub-top { padding-inline: 16px; }
  .hub-hero h1 { font-size: 52px; }
  .hub-footer { flex-direction: column; gap: 8px; }
  .invite-copy { flex-direction: column; }
  .invite-copy .hub-input { min-width: 0; }
}
@media (prefers-reduced-motion: reduce) {
  #launcher::before { display: none; }
  .hub-button { transition: none; }
}
