/* ============================================================================
   Big Typing Quiz: the app stylesheet.

   An arcade at night. Deep indigo near black stage, ONE interactive accent
   (cyan), amber for XP and currency, magenta for threats and errors. It is the
   sibling of Big World Quiz's daylight atlas and Big Piano Quiz's warm concert
   hall: the same bones (Fraunces for display, Inter for UI, IBM Plex Mono as
   the working voice, a film grain wash over the stage, generous plates with
   hairline borders) under different weather.

   Mono is this site's voice. Every stat readout, every label and every typed
   surface is IBM Plex Mono, because the thing being measured here is typing and
   a proportional font quietly lies about how wide a character is.

   Rules this file holds to, so the next editor can keep them:
     - DARK ONLY. There is no light theme and no theme toggle in app.html, so
       there is no prefers-color-scheme branch and no [data-theme] hook.
     - No imported stylesheets, no external assets, no CSS nesting (there is no
       build step here, and nesting support is not universal on the phones this
       has to run on).
     - Flat specificity: one class per selector wherever possible, so a panel
       module can always win with a modifier class instead of an id.
     - Every colour comes from a token in :root. Literals below that line are a
       bug waiting to happen.
     - prefers-reduced-motion kills every transition and animation at the end of
       the file, so nothing added above has to remember to opt out.
   ========================================================================= */


/* ============ 1. TOKENS ============
   The palette is fixed by the implementation contract and mirrored by
   css/home.css on the front page. Do not fork the values: change them here and
   copy them across, or the two pages drift out of the same night. */
:root{
  --bg:#080B14;
  --surface:#121829;
  --surface-2:#1B2238;
  --surface-3:#252E4A;

  --ink:#E9EFFF;
  --muted:#A4B2D2;
  --faint:#6B799C;

  --line:#222A44;
  --line-strong:#333D63;

  --brand:#38DFD2;
  --brand-2:#7BF5EC;
  --brand-deep:#128C84;
  --brand-ink:#04201E;
  --brand-tint:rgba(56,223,210,.13);

  --xp:#F2B33D;
  --xp-2:#FFD07A;

  --hazard:#FF5D8F;
  --violet:#8B7BFF;

  --good:#5BE59A;
  --good-tint:rgba(91,229,154,.14);
  --bad:#FF6B6B;
  --bad-tint:rgba(255,107,107,.14);

  --radius:14px;
  --radius-sm:9px;

  --serif:'Fraunces',Georgia,serif;
  --sans:'Inter',system-ui,-apple-system,'Segoe UI',sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;

  /* Rarity, mirrored from RARITY in js/loot.js. Both ends have to agree: the
     canvas and the reveal cards read the JS copy, every plate and edge in here
     reads these. Standard is deliberately the muted ink colour (a Standard drop
     should be quiet), Legendary is the hazard pink (the loudest thing on the
     site, used for exactly two purposes: danger, and the rarest thing you own). */
  --r-standard:#A4B2D2;
  --r-polished:#38DFD2;
  --r-rare:#8B7BFF;
  --r-elite:#F2B33D;
  --r-legendary:#FF5D8F;

  /* Derived furniture. Kept as tokens so a plate's depth is tuned in one place
     rather than in forty box-shadows. */
  --shadow-1:0 1px 2px rgba(0,0,0,.4);
  --shadow-2:0 6px 22px rgba(0,0,0,.45);
  --shadow-3:0 18px 50px rgba(0,0,0,.55);
  --plate:linear-gradient(180deg,var(--surface-2),var(--surface));
  --head-h:112px;              /* masthead height, used by sticky offsets */
}


/* ============ 2. RESET ============
   Small on purpose. Anything bigger than this starts fighting the browser over
   form controls, which is a fight nobody wins on a phone. */
*,*::before,*::after{ box-sizing:border-box; }

html{
  background:var(--bg);        /* the canvas colour lives here so the grain
                                  layer below can sit at z-index -1 and still
                                  be in front of it */
  -webkit-text-size-adjust:100%;
}

body{
  margin:0;
  background:transparent;
  color:var(--ink);
  font:400 16px/1.55 var(--sans);
  min-height:100vh;
  overflow-x:hidden;           /* a wide table or a canvas must scroll inside
                                  its own box, never scroll the page sideways */
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

h1,h2,h3,h4{ margin:0 0 .5rem; font-family:var(--serif); font-weight:600; line-height:1.2; }
h1{ font-size:clamp(1.6rem,4vw,2.3rem); letter-spacing:-.01em; }
h2{ font-size:clamp(1.25rem,2.6vw,1.6rem); }
h3{ font-size:1.06rem; }
h4{ font-size:.95rem; }
p{ margin:0 0 .8rem; }
ul,ol{ margin:0 0 .8rem; padding-left:1.2rem; }
a{ color:var(--brand); text-decoration:none; }
a:hover{ color:var(--brand-2); text-decoration:underline; }
img,svg,canvas{ max-width:100%; }
img{ height:auto; }
table{ border-collapse:collapse; width:100%; }
button,input,select,textarea{ font:inherit; color:inherit; }
button{ cursor:pointer; }
hr{ border:0; border-top:1px solid var(--line); margin:1.2rem 0; }
code,kbd,samp{ font-family:var(--mono); font-size:.92em; }

/* The global guard. Several rules below set `display` in a class, and a class
   rule beats the browser's own [hidden] UA rule, so without this an element
   toggled by the hidden attribute (the panels, the account menu, the toast, the
   game host) would never actually hide. Explicit per-selector guards follow
   each of those rules as well, because that is the pattern the sibling repos
   use and it survives somebody deleting this line. */
[hidden]{ display:none; }


/* ============ 3. THE FILM GRAIN WASH ============
   The same inline SVG turbulence both sibling sites use, at the same low
   opacity. It is what stops a very dark flat page reading as a dead LCD: the
   grain gives the stage a surface, and at 5% it is felt rather than seen.
   Fixed rather than scrolling, so it behaves like a lens over the page rather
   than like a texture printed on it. z-index -1 keeps it above the html
   background and behind every scrap of content without a single wrapper
   needing a stacking context of its own. */
body::before{
  content:'';
  position:fixed;
  inset:0;
  z-index:-1;
  pointer-events:none;
  opacity:.05;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:300px 300px;
}


/* ============ 4. TYPE HELPERS ============
   Mono is the working voice, so .mono is used constantly and .display is the
   exception. Numerals are tabular everywhere a number can change while you are
   looking at it, or a counting readout jitters its own width. */
.mono{ font-family:var(--mono); font-variant-numeric:tabular-nums; }
.display{ font-family:var(--serif); font-weight:600; }
.num{ font-family:var(--mono); font-variant-numeric:tabular-nums; letter-spacing:-.01em; }

/* The small mono label that titles nearly every block on this site. */
.label{
  font-family:var(--mono);
  font-size:.68rem;
  font-weight:500;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--faint);
}
.lede{ color:var(--muted); font-size:1.02rem; max-width:64ch; }
.muted{ color:var(--muted); }
.faint{ color:var(--faint); }
.good{ color:var(--good); }
.bad{ color:var(--bad); }
.warn{ color:var(--xp); }
.center{ text-align:center; }
.nowrap{ white-space:nowrap; }
.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}


/* ============ 5. THE SKIP LINK ============
   Off screen until focused, then a real target at the top left. Keyboard users
   land on this before the six tab buttons, which is the whole point. */
.skip{
  position:absolute;
  left:-9999px;
  top:0;
  z-index:200;
  padding:.6rem 1rem;
  background:var(--brand);
  color:var(--brand-ink);
  font-family:var(--mono);
  font-weight:600;
  border-radius:0 0 var(--radius-sm) 0;
}
.skip:focus{ left:0; text-decoration:none; }


/* ============ 6. FOCUS ============
   :focus-visible only, so a mouse click never paints a ring but every keyboard
   traversal does. The ring is cyan because cyan is this site's one "you can
   interact with this" signal, and a focus ring is the purest case of that. */
:focus{ outline:none; }
:focus-visible{
  outline:2px solid var(--brand);
  outline-offset:2px;
  border-radius:4px;
}
.btn:focus-visible,
.tabs button:focus-visible,
.icon-btn:focus-visible{ outline-offset:3px; }


/* ============ 7. MASTHEAD ============
   Sticky, because the level chip is the running feedback for everything the
   player does and it should never be scrolled away from. Two rows on a wide
   screen (identity plus account on top, tabs underneath) collapsing to a
   scrollable tab strip on a phone. */
.masthead{
  position:sticky;
  top:0;
  z-index:60;
  background:rgba(8,11,20,.86);
  backdrop-filter:blur(14px) saturate(140%);
  -webkit-backdrop-filter:blur(14px) saturate(140%);
  border-bottom:1px solid var(--line);
}

.masthead-inner{
  max-width:1180px;
  margin:0 auto;
  padding:.55rem clamp(.7rem,3vw,1.4rem) 0;
  display:grid;
  /* The empty 1fr column is what keeps the level chip hugging its own contents
     next to the brand instead of being stretched across the whole masthead: the
     chip is a readout, and a readout stretched to fill a desktop width reads as
     a progress bar for the page rather than for the player. */
  grid-template-columns:auto auto 1fr auto auto;
  grid-template-areas:
    'brand chip .    acct set'
    'tabs  tabs tabs tabs tabs';
  align-items:center;
  gap:.5rem .9rem;
}

/* ---- brand ---- */
.brand{ grid-area:brand; min-width:0; }
.brand a{
  display:inline-flex;
  align-items:center;
  gap:.55rem;
  color:var(--ink);
  text-decoration:none;
}
.brand a:hover{ color:var(--ink); text-decoration:none; }
.brand-mark{
  width:30px;
  height:30px;
  flex:0 0 auto;
  filter:drop-shadow(0 0 10px rgba(56,223,210,.28));
}
.brand-name{
  font-family:var(--serif);
  font-weight:600;
  font-size:1.06rem;
  letter-spacing:-.01em;
  white-space:nowrap;
}
/* The middle word is the one that changes between the three sites, so it is
   the one that carries the accent. */
.brand-name em{ font-style:italic; color:var(--brand); }

/* ---- tabs ----
   Horizontally scrollable on a narrow screen rather than wrapping to two rows:
   six tabs wrapping makes the masthead taller than the game underneath it. The
   scrollbar is hidden because the row is short enough to be obviously swipeable
   and a visible bar under six buttons reads as a broken layout. */
.tabs{
  grid-area:tabs;
  display:flex;
  gap:.15rem;
  overflow-x:auto;
  overflow-y:hidden;
  scrollbar-width:none;
  -webkit-overflow-scrolling:touch;
  margin:0 calc(-1 * clamp(.7rem,3vw,1.4rem));
  padding:0 clamp(.7rem,3vw,1.4rem);
}
.tabs::-webkit-scrollbar{ display:none; }

.tabs button{
  flex:0 0 auto;
  position:relative;
  appearance:none;
  background:none;
  border:0;
  border-bottom:2px solid transparent;
  padding:.62rem .8rem .58rem;
  color:var(--muted);
  font-family:var(--mono);
  font-size:.78rem;
  font-weight:500;
  letter-spacing:.08em;
  text-transform:uppercase;
  white-space:nowrap;
  transition:color .15s ease, border-color .15s ease;
}
.tabs button:hover{ color:var(--ink); }

/* The active tab is marked three ways (colour, a lit underline, a glow) rather
   than one, because on a dark screen at a glance a colour change alone is easy
   to miss and this is the app's primary orientation cue. */
.tabs button.on{
  color:var(--brand);
  border-bottom-color:var(--brand);
  text-shadow:0 0 14px rgba(56,223,210,.4);
}


/* ============ 8. THE LEVEL CHIP ============
   Level, rank, the XP bar and the chip balance, painted only by
   paintLevelChip() in js/app.js. It is the one piece of persistent progression
   feedback on screen at all times, so it gets a plate of its own. */
.lvl-chip{
  grid-area:chip;
  justify-self:start;
  display:flex;
  align-items:center;
  gap:.6rem;
  min-width:0;
  padding:.34rem .55rem;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:999px;
}

.lvl-badge{
  flex:0 0 auto;
  width:30px;
  height:30px;
  display:grid;
  place-items:center;
  border-radius:50%;
  background:radial-gradient(circle at 35% 30%,var(--xp-2),var(--xp) 62%,#8A5F13);
  color:#241703;
  font-family:var(--mono);
  font-weight:700;
  font-size:.84rem;
  font-variant-numeric:tabular-nums;
  box-shadow:0 0 0 1px rgba(242,179,61,.35), 0 0 16px rgba(242,179,61,.25);
}

.lvl-meta{ min-width:96px; flex:0 1 168px; }
.lvl-rank{
  font-family:var(--mono);
  font-size:.66rem;
  letter-spacing:.11em;
  text-transform:uppercase;
  color:var(--muted);
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
.lvl-bar{
  margin-top:3px;
  height:5px;
  border-radius:999px;
  background:var(--surface-3);
  overflow:hidden;
}
/* The fill is an <i> whose width is set inline by the painter. */
.lvl-bar i{
  display:block;
  height:100%;
  border-radius:inherit;
  background:linear-gradient(90deg,var(--xp),var(--xp-2));
  box-shadow:0 0 10px rgba(242,179,61,.45);
  transition:width .45s cubic-bezier(.2,.7,.3,1);
}
.lvl-xp{
  font-family:var(--mono);
  font-size:.7rem;
  font-variant-numeric:tabular-nums;
  color:var(--faint);
  white-space:nowrap;
}

/* Chips: the spendable currency, so it wears the currency colour. */
.chips{
  display:inline-flex;
  align-items:center;
  gap:.3rem;
  padding:.2rem .5rem;
  border-radius:999px;
  background:rgba(242,179,61,.1);
  color:var(--xp-2);
  font-family:var(--mono);
  font-size:.78rem;
  font-weight:600;
  font-variant-numeric:tabular-nums;
  white-space:nowrap;
}
.chip-icon{ width:14px; height:14px; flex:0 0 auto; }

/* Cores are the hard currency (only a boss drops one), so they read as scarcer:
   violet, and never in the same warm family as chips. */
.cores{
  display:inline-flex;
  align-items:center;
  gap:.3rem;
  padding:.2rem .5rem;
  border-radius:999px;
  background:rgba(139,123,255,.14);
  border:1px solid rgba(139,123,255,.32);
  color:var(--violet);
  font-family:var(--mono);
  font-size:.78rem;
  font-weight:600;
  font-variant-numeric:tabular-nums;
  white-space:nowrap;
}


/* ============ 9. ACCOUNT AREA ============
   Hidden entirely when Firebase is not configured, which is why every one of
   these rules needs its [hidden] guard: each sets display in a class, and a
   class rule outranks the browser's own [hidden] rule. */
.acct-area{
  grid-area:acct;
  position:relative;
  display:flex;
  align-items:center;
}
.acct-area[hidden]{ display:none; }

.acct-btn{
  display:inline-flex;
  align-items:center;
  gap:.42rem;
  max-width:190px;
  padding:.4rem .7rem;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:999px;
  color:var(--muted);
  font-family:var(--mono);
  font-size:.76rem;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
  transition:border-color .15s ease, color .15s ease;
}
.acct-btn:hover{ border-color:var(--line-strong); color:var(--ink); }
.acct-btn:disabled{ opacity:.6; cursor:default; }

/* The sync light. Four states, set by renderAccountUI as a second class. */
.acct-dot{
  width:8px; height:8px;
  flex:0 0 auto;
  border-radius:50%;
  background:var(--faint);
}
.acct-dot.on{ background:var(--good); box-shadow:0 0 8px rgba(91,229,154,.6); }
.acct-dot.sync{ background:var(--brand); animation:btq-pulse 1.1s ease-in-out infinite; }
.acct-dot.err{ background:var(--bad); box-shadow:0 0 8px rgba(255,107,107,.55); }
.acct-dot.off{ background:var(--faint); }

.acct-menu{
  position:absolute;
  top:calc(100% + .5rem);
  right:0;
  z-index:70;
  display:block;
  min-width:230px;
  padding:.5rem;
  background:var(--surface-2);
  border:1px solid var(--line-strong);
  border-radius:var(--radius);
  box-shadow:var(--shadow-3);
}
.acct-menu[hidden]{ display:none; }

.acct-who{
  padding:.4rem .5rem .6rem;
  border-bottom:1px solid var(--line);
  margin-bottom:.35rem;
}
.acct-who__name{ font-weight:600; font-size:.92rem; }
.acct-who__email{ font-family:var(--mono); font-size:.72rem; color:var(--faint); word-break:break-all; }
.acct-who__sn{ margin-top:.35rem; font-size:.8rem; color:var(--muted); }
.acct-who__sn b{ color:var(--ink); font-family:var(--mono); font-weight:600; }
.acct-who__status{ margin-top:.3rem; font-family:var(--mono); font-size:.7rem; color:var(--faint); }

.acct-item{
  display:block;
  width:100%;
  text-align:left;
  padding:.5rem .55rem;
  background:none;
  border:0;
  border-radius:var(--radius-sm);
  color:var(--muted);
  font-size:.86rem;
  transition:background .12s ease, color .12s ease;
}
.acct-item:hover{ background:var(--surface-3); color:var(--ink); }
.acct-item[hidden]{ display:none; }

/* The verified checkmark, used by the account menu and by every leaderboard
   row. Green, because it means "the registry says this name is theirs" and
   green is the only thing on this site that means confirmed. */
.verified{
  display:inline-block;
  margin-left:.25rem;
  color:var(--good);
  font-size:.82em;
}


/* ============ 10. ICON BUTTONS ============
   The masthead's key row (music, settings) shares the grid's 'set' area
   through one flex group, so a new key is a button in the group rather than a
   new grid column. */
.head-keys{ grid-area:set; display:flex; gap:.4rem; align-items:center; }
.icon-btn{
  display:inline-grid;
  place-items:center;
  width:36px;
  height:36px;
  padding:0;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  color:var(--muted);
  transition:color .15s ease, border-color .15s ease, transform .15s ease;
}
.icon-btn:hover{ color:var(--brand); border-color:var(--brand-deep); }
.icon-btn:active{ transform:translateY(1px); }
.icon-btn svg{ width:19px; height:19px; }


/* ============ 11. LAYOUT ============ */
.wrap{
  max-width:1180px;
  margin:0 auto;
  padding:clamp(1rem,3vw,1.8rem) clamp(.7rem,3vw,1.4rem) 3rem;
}

.panel{ display:block; }
.panel[hidden]{ display:none; }

.foot{
  border-top:1px solid var(--line);
  padding:1.4rem clamp(.7rem,3vw,1.4rem) 2.4rem;
  color:var(--faint);
  font-size:.82rem;
}
.foot p{ max-width:70ch; margin:0 auto; }

/* Section headers used across the panels: a mono kicker over a serif title. */
.sec-head{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:1rem;
  flex-wrap:wrap;
  margin:0 0 .9rem;
}
.sec-head h2{ margin:0; }
.sec-head .label{ margin-bottom:.1rem; }


/* ============ 12. BUTTONS ============
   One base, three variants and two sizes. Cyan is reserved for .primary: if
   every button were cyan then nothing on the screen would be the obvious next
   move, which is the only job the accent has. */
.btn{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:.45rem;
  padding:.58rem 1.05rem;
  background:var(--surface-2);
  border:1px solid var(--line-strong);
  border-radius:var(--radius-sm);
  color:var(--ink);
  font-family:var(--mono);
  font-size:.82rem;
  font-weight:500;
  letter-spacing:.03em;
  text-decoration:none;
  white-space:nowrap;
  transition:background .15s ease, border-color .15s ease, color .15s ease, transform .1s ease;
}
.btn:hover{ background:var(--surface-3); border-color:var(--brand-deep); color:var(--ink); text-decoration:none; }
.btn:active{ transform:translateY(1px); }
.btn:disabled,
.btn[disabled]{ opacity:.42; cursor:default; transform:none; }
.btn:disabled:hover,
.btn[disabled]:hover{ background:var(--surface-2); border-color:var(--line-strong); }

.btn.primary{
  background:linear-gradient(180deg,var(--brand-2),var(--brand));
  border-color:var(--brand-2);
  color:var(--brand-ink);
  font-weight:600;
  box-shadow:0 0 22px rgba(56,223,210,.22);
}
.btn.primary:hover{
  background:linear-gradient(180deg,#9DFFF7,var(--brand-2));
  border-color:#9DFFF7;
  color:var(--brand-ink);
}

.btn.ghost{ background:transparent; border-color:var(--line); color:var(--muted); }
.btn.ghost:hover{ background:var(--surface); border-color:var(--line-strong); color:var(--ink); }

.btn.danger{ background:var(--bad-tint); border-color:rgba(255,107,107,.4); color:var(--bad); }
.btn.danger:hover{ background:rgba(255,107,107,.22); border-color:var(--bad); color:#FFD6D6; }

/* Both spellings are supported so a panel module can use whichever reads better
   in its markup without a surprise. */
.btn.sm,
.btn.small{ padding:.38rem .7rem; font-size:.74rem; }
.btn.tiny{ padding:.24rem .5rem; font-size:.68rem; letter-spacing:.02em; }
.btn.wide{ width:100%; }

.btn-row{ display:flex; flex-wrap:wrap; gap:.5rem; align-items:center; }


/* ============ 13. PLATES, CARDS AND STATS ============
   A plate is the site's unit of furniture: a slightly lifted surface with a
   hairline border. Cards are plates you can click. */
.plate{
  background:var(--plate);
  border:1px solid var(--line);
  border-radius:var(--radius);
  padding:clamp(.9rem,2.4vw,1.35rem);
  box-shadow:var(--shadow-1);
}
.plate + .plate{ margin-top:1rem; }

.card{
  display:block;
  width:100%;
  text-align:left;
  background:var(--plate);
  border:1px solid var(--line);
  border-radius:var(--radius);
  padding:1rem;
  color:inherit;
  box-shadow:var(--shadow-1);
  transition:border-color .16s ease, transform .16s ease, box-shadow .16s ease;
}
.card:hover{ border-color:var(--line-strong); transform:translateY(-2px); box-shadow:var(--shadow-2); }
.card.on{ border-color:var(--brand-deep); box-shadow:0 0 0 1px var(--brand-deep), var(--shadow-2); }

.grid{ display:grid; gap:1rem; }
.grid.cols2{ grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); }
.grid.cols3{ grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); }

/* A stat is a mono number over a mono label. It is used dozens of times, so it
   is deliberately the smallest possible thing that reads as a readout. */
.stat-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(112px,1fr));
  gap:.7rem;
}
.stat{
  padding:.65rem .75rem;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
}
.stat b{
  display:block;
  font-family:var(--mono);
  font-size:1.32rem;
  font-weight:600;
  font-variant-numeric:tabular-nums;
  line-height:1.15;
  color:var(--ink);
}
.stat span{
  display:block;
  margin-top:.15rem;
  font-family:var(--mono);
  font-size:.63rem;
  letter-spacing:.13em;
  text-transform:uppercase;
  color:var(--faint);
}
.stat.accent b{ color:var(--brand); }
.stat.xp b{ color:var(--xp); }
.stat.hazard b{ color:var(--hazard); }


/* ============ 14. PILLS, TAGS AND BARS ============ */
.pill{
  display:inline-flex;
  align-items:center;
  gap:.35rem;
  padding:.26rem .62rem;
  background:var(--surface-2);
  border:1px solid var(--line);
  border-radius:999px;
  color:var(--muted);
  font-family:var(--mono);
  font-size:.7rem;
  letter-spacing:.04em;
  white-space:nowrap;
  transition:border-color .14s ease, color .14s ease, background .14s ease;
}
button.pill:hover{ border-color:var(--line-strong); color:var(--ink); }
.pill.on{ background:var(--brand-tint); border-color:var(--brand-deep); color:var(--brand); }
.pill.xp{ background:rgba(242,179,61,.1); border-color:rgba(242,179,61,.32); color:var(--xp-2); }
.pill.hazard{ background:rgba(255,93,143,.1); border-color:rgba(255,93,143,.34); color:var(--hazard); }

.tag{
  display:inline-block;
  padding:.14rem .44rem;
  border-radius:4px;
  background:var(--surface-3);
  color:var(--muted);
  font-family:var(--mono);
  font-size:.62rem;
  letter-spacing:.11em;
  text-transform:uppercase;
}
.tag.new{ background:var(--brand-tint); color:var(--brand); }

/* The generic progress bar, reused by nodes, contracts, ranks and reports.
   Every fill in this file accepts EITHER an inline width or a --v custom
   property in the range 0 to 1, so a module can drive it whichever way suits
   its render, and an inline width always wins. */
.bar{
  height:7px;
  border-radius:999px;
  background:var(--surface-3);
  overflow:hidden;
}
.bar__fill{
  display:block;
  height:100%;
  width:calc(var(--v,0) * 100%);
  border-radius:inherit;
  background:linear-gradient(90deg,var(--brand-deep),var(--brand));
  transition:width .4s cubic-bezier(.2,.7,.3,1);
}
.bar.xp .bar__fill{ background:linear-gradient(90deg,var(--xp),var(--xp-2)); }
.bar.good .bar__fill{ background:linear-gradient(90deg,#2E9C68,var(--good)); }
.bar.hazard .bar__fill{ background:linear-gradient(90deg,#B33A63,var(--hazard)); }


/* ============ 15. DIALOGS ============
   The <dialog> element is stripped back to nothing and .dlg-in is the actual
   plate, which keeps the top-layer behaviour (and the native backdrop) without
   inheriting the UA's borders and centring quirks. */
.dlg{
  padding:0;
  border:0;
  background:transparent;
  color:var(--ink);
  max-width:min(92vw,520px);
  width:100%;
  max-height:88vh;
  overflow:visible;
}
.dlg.wide{ max-width:min(94vw,860px); }

.dlg::backdrop{
  background:rgba(4,6,12,.72);
  backdrop-filter:blur(5px);
  -webkit-backdrop-filter:blur(5px);
}

.dlg-in{
  max-height:88vh;
  overflow:auto;
  padding:clamp(1.1rem,3vw,1.6rem);
  background:var(--plate);
  border:1px solid var(--line-strong);
  border-radius:var(--radius);
  box-shadow:var(--shadow-3);
}
.dlg-in h2{ margin-top:0; }

.dlg-lede{ color:var(--muted); font-size:.9rem; max-width:56ch; }

/* A hairline rule with a mono caption sitting in the gap. */
.dlg-sep{
  display:flex;
  align-items:center;
  gap:.7rem;
  margin:1rem 0;
  color:var(--faint);
  font-family:var(--mono);
  font-size:.68rem;
  letter-spacing:.1em;
  text-transform:uppercase;
}
.dlg-sep::before,
.dlg-sep::after{ content:''; flex:1; height:1px; background:var(--line); }

.fld{ display:block; margin:.75rem 0; }
.fld span{
  display:block;
  margin-bottom:.28rem;
  font-family:var(--mono);
  font-size:.68rem;
  letter-spacing:.11em;
  text-transform:uppercase;
  color:var(--faint);
}
.fld input,
.fld select,
.fld textarea{
  width:100%;
  padding:.6rem .7rem;
  background:var(--bg);
  border:1px solid var(--line-strong);
  border-radius:var(--radius-sm);
  color:var(--ink);
  font-family:var(--mono);
  font-size:.92rem;
  transition:border-color .15s ease, box-shadow .15s ease;
}
.fld input:focus,
.fld select:focus,
.fld textarea:focus{
  border-color:var(--brand-deep);
  box-shadow:0 0 0 3px var(--brand-tint);
}

.dlg-hint{ margin:.4rem 0 0; font-size:.78rem; color:var(--faint); }
.dlg-hint.good{ color:var(--good); }
.dlg-hint.bad{ color:var(--bad); }

.dlg-err{
  margin:.7rem 0 0;
  padding:.55rem .7rem;
  background:var(--bad-tint);
  border:1px solid rgba(255,107,107,.35);
  border-radius:var(--radius-sm);
  color:var(--bad);
  font-size:.82rem;
}
.dlg-err[hidden]{ display:none; }

.dlg-actions{
  display:flex;
  flex-wrap:wrap;
  gap:.5rem;
  margin-top:1.1rem;
}

/* Settings rows, emitted by renderSettings() in js/app.js. */
.set-rows{ display:grid; gap:.2rem; }
.set-row{ padding:.7rem 0; border-bottom:1px solid var(--line); }
.set-row:last-child{ border-bottom:0; }
.set-row__main{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:1rem;
}
.set-row__label{ font-size:.92rem; }
.set-row__ctl{ flex:0 0 auto; }
.set-row__ctl select,
.set-row__ctl input[type=text]{
  padding:.34rem .5rem;
  background:var(--bg);
  border:1px solid var(--line-strong);
  border-radius:var(--radius-sm);
  font-family:var(--mono);
  font-size:.8rem;
}
.set-row__ctl input[type=checkbox]{ width:18px; height:18px; accent-color:var(--brand); }
.set-row__ctl input[type=range]{ width:150px; accent-color:var(--brand); }
.set-row__hint{ margin:.3rem 0 0; font-size:.78rem; color:var(--faint); max-width:60ch; }


/* ============ 16. TOAST ============
   One reused element. It has to sit ABOVE the CSS fullscreen overlay (z-index
   70) or it would be invisible for exactly the moments it matters most: a level
   up, a badge, a boss going down. */
.toast{
  position:fixed;
  left:50%;
  bottom:calc(1.4rem + env(safe-area-inset-bottom,0px));
  transform:translateX(-50%);
  z-index:95;
  display:block;
  max-width:min(90vw,420px);
  padding:.7rem 1.05rem;
  background:var(--surface-2);
  border:1px solid var(--line-strong);
  border-radius:999px;
  color:var(--ink);
  font-family:var(--mono);
  font-size:.82rem;
  text-align:center;
  box-shadow:var(--shadow-3);
  animation:btq-toast-in .22s ease-out;
}
.toast[hidden]{ display:none; }
.toast.good{ border-color:rgba(91,229,154,.45); color:var(--good); }
.toast.warn{ border-color:rgba(242,179,61,.45); color:var(--xp-2); }
.toast.bad{ border-color:rgba(255,107,107,.45); color:var(--bad); }


/* ============ 17. THE KEYBOARD CATCHER ============
   #kbCatch is the app's single focusable input and every typing surface focuses
   it so a phone raises its keyboard. It must stay FOCUSABLE, so it is moved off
   screen rather than display:none'd or visibility:hidden'd: neither of those can
   take focus, and a phone will not open a keyboard for an element that cannot.
   opacity 0 with a real size keeps iOS happy, which refuses to raise the
   keyboard for a zero-sized box. */
.kb-catch{
  position:fixed;
  left:-4px;
  bottom:-4px;
  width:2px;
  height:2px;
  padding:0;
  margin:0;
  border:0;
  opacity:0;
  z-index:-1;
  background:transparent;
  color:transparent;
  font-size:16px;              /* under 16px iOS zooms the page on focus */
  pointer-events:none;
  -webkit-user-select:none;
  user-select:none;
}


/* ============ 18. THE GAME HOST AND FULLSCREEN ============
   #gameHost is what a mini-game mounts into and the element the fullscreen
   toggle takes. Two fullscreen paths, exactly as the sibling sites learned:
   the real Fullscreen API where it behaves, and a CSS overlay (.fs-mode) on
   iOS and iPadOS, where WKFullScreenWindowController wires a downward swipe to
   dismissal and scores vertical touches as suspected keyboard phishing. Both
   of those target precisely what an arcade game does, so the fix is to not ask
   for a real fullscreen there at all. Every rule that styles the fullscreen
   state therefore names BOTH selectors. */
.game-host{
  display:block;
  position:relative;
  background:var(--bg);
  border:1px solid var(--line);
  border-radius:var(--radius);
  overflow:hidden;
}
.game-host[hidden]{ display:none; }
.game-host canvas{ display:block; width:100%; touch-action:none; }

.game-host:fullscreen{
  border:0;
  border-radius:0;
  width:100vw;
  height:100vh;
  display:flex;
  flex-direction:column;
}
.game-host.fs-mode{
  position:fixed;
  inset:0;
  z-index:70;
  border:0;
  border-radius:0;
  width:100vw;
  height:100vh;
  height:100dvh;
  display:flex;
  flex-direction:column;
  padding:env(safe-area-inset-top,0) env(safe-area-inset-right,0)
          env(safe-area-inset-bottom,0) env(safe-area-inset-left,0);
}
/* The CSS overlay needs the page under it to sit perfectly still: an iOS rubber
   band drags a fixed overlay along with it, so the overscroll has to go as well
   as the scroll. */
body.fs-lock{ overflow:hidden; overscroll-behavior:none; }

.fs-btn{
  position:absolute;
  top:.5rem;
  right:.5rem;
  z-index:8;
}


/* ============ 19. THE TYPING SURFACE ============
   The one place on the site where a character's own colour carries meaning:
   what you have typed correctly, what you got wrong, and where you are. It is
   mono, obviously, and it is generously spaced because a player is reading
   this while something is descending at them. */
.typed{
  font-family:var(--mono);
  font-size:clamp(1.05rem,2.6vw,1.4rem);
  line-height:1.7;
  letter-spacing:.02em;
  word-break:break-word;
  color:var(--faint);
}
.typed .correct{ color:var(--brand); }
.typed .wrong{
  color:var(--hazard);
  background:rgba(255,93,143,.16);
  border-radius:3px;
  text-decoration:underline wavy rgba(255,93,143,.6);
  text-underline-offset:3px;
}
.typed .rest{ color:var(--muted); }

/* The caret, in the three shapes state.settings.caret offers. A caret is set on
   the character it is sitting ON, so the block variant has to keep the glyph
   readable: it inverts rather than covering. min-width keeps a caret on a space
   the same width as a caret on a letter. */
.caret{
  position:relative;
  display:inline-block;
  min-width:.6em;
  animation:btq-caret 1.05s steps(1,end) infinite;
}
.caret.block{ background:var(--brand); color:var(--brand-ink); border-radius:2px; }
.caret.line{ box-shadow:inset 2px 0 0 var(--brand); }
.caret.underline{ box-shadow:inset 0 -2px 0 var(--brand); }


/* ============ 20. THE ARCADE HUB ============
   One card per mini-game. The art panel is a canvas or an SVG the game draws
   itself, so the card only owns its frame and its lighting. */
.hub-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(268px,1fr));
  gap:1rem;
}

.hub-card{
  display:flex;
  flex-direction:column;
  width:100%;
  padding:0;
  overflow:hidden;
  text-align:left;
  background:var(--plate);
  border:1px solid var(--line);
  border-radius:var(--radius);
  color:inherit;
  box-shadow:var(--shadow-1);
  transition:border-color .18s ease, transform .18s ease, box-shadow .18s ease;
}
.hub-card:hover{
  border-color:var(--brand-deep);
  transform:translateY(-3px);
  box-shadow:0 0 0 1px rgba(56,223,210,.18), var(--shadow-2);
}

.hub-card__art{
  position:relative;
  aspect-ratio:16/9;
  background:
    radial-gradient(120% 90% at 50% 110%,rgba(56,223,210,.16),transparent 60%),
    linear-gradient(180deg,#0A0E1B,#141B30);
  border-bottom:1px solid var(--line);
  overflow:hidden;
}
.hub-card__art canvas,
.hub-card__art svg,
.hub-card__art img{ display:block; width:100%; height:100%; object-fit:cover; }

.hub-card__body{ padding:.9rem 1rem 1rem; display:flex; flex-direction:column; gap:.4rem; flex:1; }
.hub-card__body h3{ margin:0; font-family:var(--serif); }
.hub-card__body p{ margin:0; color:var(--muted); font-size:.86rem; }

/* The progression signal on a hub card: a rank or a best, never a lock. */
.hub-badge{
  position:absolute;
  top:.55rem;
  left:.55rem;
  padding:.2rem .5rem;
  background:rgba(8,11,20,.78);
  border:1px solid var(--line-strong);
  border-radius:999px;
  color:var(--xp-2);
  font-family:var(--mono);
  font-size:.66rem;
  letter-spacing:.06em;
  backdrop-filter:blur(4px);
  -webkit-backdrop-filter:blur(4px);
}

/* "Coming soon" is the ONE thing that dims a card, and it is not a lock: it is
   a game that does not exist yet. Nothing built is ever gated. */
.soon{ opacity:.5; filter:saturate(.4); pointer-events:none; }
.soon .hub-badge{ color:var(--faint); }


/* ============ 21. CURRICULUM NODES AND STARS ============
   Progression is SHOWN, never gated: --done and --next are marks on a node, and
   every node is clickable at all times whichever mark it carries. */
.node-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(212px,1fr));
  gap:.8rem;
}

.node{
  display:flex;
  flex-direction:column;
  gap:.4rem;
  width:100%;
  padding:.85rem .9rem;
  text-align:left;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  color:inherit;
  transition:border-color .16s ease, background .16s ease, transform .12s ease;
}
.node:hover{ border-color:var(--line-strong); background:var(--surface-2); transform:translateY(-2px); }
.node h4{ margin:0; font-family:var(--sans); font-size:.94rem; font-weight:600; }
.node p{ margin:0; color:var(--faint); font-size:.78rem; }

.node--done{ border-color:rgba(91,229,154,.34); background:linear-gradient(180deg,var(--good-tint),transparent); }
/* The recommended next node. It is the only thing on the training screen wearing
   the accent, so "where do I go now" answers itself. */
.node--next{
  border-color:var(--brand-deep);
  box-shadow:0 0 0 1px rgba(56,223,210,.25), 0 0 26px rgba(56,223,210,.12);
}
.node--next::after{
  content:'NEXT';
  align-self:flex-start;
  padding:.1rem .4rem;
  border-radius:3px;
  background:var(--brand-tint);
  color:var(--brand);
  font-family:var(--mono);
  font-size:.6rem;
  letter-spacing:.14em;
}

.stars{ display:inline-flex; gap:.14rem; line-height:1; }
.star{
  font-size:.9rem;
  color:var(--surface-3);
  transition:color .2s ease, text-shadow .2s ease;
}
.star--on{ color:var(--xp); text-shadow:0 0 10px rgba(242,179,61,.5); }


/* ============ 22. RUN ROWS ============
   History and recent-run lists. The dot carries the outcome so the eye can scan
   a column of them without reading a word. */
.runrow{
  display:flex;
  align-items:center;
  gap:.7rem;
  padding:.5rem .2rem;
  border-bottom:1px solid var(--line);
  font-family:var(--mono);
  font-size:.8rem;
  font-variant-numeric:tabular-nums;
}
.runrow:last-child{ border-bottom:0; }
.runrow b{ font-weight:600; color:var(--ink); }
.runrow span{ color:var(--faint); }
.runrow .grow{ flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--muted); }

.rundot{
  width:8px;
  height:8px;
  flex:0 0 auto;
  border-radius:50%;
  background:var(--faint);
}
.rundot.good{ background:var(--good); box-shadow:0 0 8px rgba(91,229,154,.5); }
.rundot.best{ background:var(--xp); box-shadow:0 0 10px rgba(242,179,61,.55); }
.rundot.bad{ background:var(--bad); }


/* ============ 23. THE ON SCREEN KEYBOARD HEAT MAP ============
   The keyboard under a drill, tinted by the skill model. The ramp direction is
   the thing to remember: h0 is "no evidence yet", h1 is your STRONGEST and h5
   your WEAKEST. It runs good-green to hazard-magenta rather than one hue at
   five brightnesses, because the question a player asks this map is "which keys
   are letting me down" and a single-hue ramp answers that far more slowly. */
.kbd{
  display:flex;
  flex-direction:column;
  gap:.3rem;
  padding:.7rem;
  background:var(--bg);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  overflow-x:auto;
}
.kbd-row{ display:flex; gap:.3rem; justify-content:center; }

.kbd-key{
  flex:0 0 auto;
  min-width:30px;
  padding:.42rem .1rem;
  border-radius:5px;
  background:var(--surface);
  border:1px solid var(--line);
  color:var(--faint);
  font-family:var(--mono);
  font-size:.76rem;
  text-align:center;
  text-transform:uppercase;
  transition:background .3s ease, color .3s ease, border-color .3s ease;
}
.kbd-key.wide{ min-width:64px; }
.kbd-key.space{ min-width:180px; flex:1 1 auto; }

.kbd-key.h0{ background:var(--surface); border-color:var(--line); color:var(--faint); }
.kbd-key.h1{ background:rgba(91,229,154,.20); border-color:rgba(91,229,154,.42); color:var(--good); }
.kbd-key.h2{ background:rgba(56,223,210,.18); border-color:rgba(56,223,210,.40); color:var(--brand); }
.kbd-key.h3{ background:rgba(242,179,61,.18); border-color:rgba(242,179,61,.42); color:var(--xp-2); }
.kbd-key.h4{ background:rgba(255,140,110,.20); border-color:rgba(255,140,110,.44); color:#FFB59E; }
.kbd-key.h5{
  background:rgba(255,93,143,.24);
  border-color:rgba(255,93,143,.55);
  color:var(--hazard);
  box-shadow:0 0 12px rgba(255,93,143,.22);
}
/* The key the drill is asking for right now. */
.kbd-key.on{
  background:var(--brand);
  border-color:var(--brand-2);
  color:var(--brand-ink);
  font-weight:600;
}


/* ============ 24. LEADERBOARDS ============
   Two naming conventions on purpose: js/board.js emits the lb-* classes, and
   the panel modules use the board-* ones for their own wrappers. Both are
   styled here so a row looks identical whichever built it. */
.lb-wrap{ overflow-x:auto; }

.lb-table,
.board-table{
  width:100%;
  font-family:var(--mono);
  font-size:.84rem;
  font-variant-numeric:tabular-nums;
}
.lb-table th,
.board-table th{
  padding:.4rem .5rem;
  border-bottom:1px solid var(--line-strong);
  color:var(--faint);
  font-weight:500;
  font-size:.64rem;
  letter-spacing:.13em;
  text-transform:uppercase;
  text-align:left;
}
.lb-table td,
.board-table td,
.board-row td{
  padding:.48rem .5rem;
  border-bottom:1px solid var(--line);
  vertical-align:middle;
}

.lb-rank{ width:2.6rem; color:var(--faint); text-align:right; }
.lb-who{ min-width:0; }
.lb-score{ text-align:right; white-space:nowrap; font-weight:600; }
.lb-when{ text-align:right; color:var(--faint); font-size:.74rem; white-space:nowrap; }
.lb-unit{ color:var(--faint); font-size:.72rem; font-weight:400; }
.lb-sub{ margin-left:.4rem; color:var(--faint); font-size:.72rem; font-weight:400; }
.lb-you{
  margin-left:.35rem;
  padding:.05rem .3rem;
  border-radius:3px;
  background:var(--brand-tint);
  color:var(--brand);
  font-size:.62rem;
  letter-spacing:.1em;
}
.lb-flag{
  width:18px;
  height:13px;
  margin-right:.4rem;
  border-radius:2px;
  object-fit:cover;
  vertical-align:-1px;
  box-shadow:0 0 0 1px var(--line);
}
.lb-empty{ padding:1.2rem .5rem; color:var(--faint); text-align:center; }
.lb-tail{ margin:.6rem 0 0; color:var(--faint); font-size:.78rem; }

/* Your own row, in either naming convention. It is tinted rather than merely
   bolded, because finding yourself in a hundred rows is the single most common
   thing anybody does on a leaderboard. */
.lb-me td,
.board-you td{
  background:var(--brand-tint);
  border-bottom-color:var(--brand-deep);
}
.lb-me .lb-rank,
.board-you .lb-rank{ color:var(--brand); }

/* The name plate: a cosmetic slot, so it is a small plate the player earns
   rather than plain text. A rarity class on it paints its edge. */
.plate-name{
  display:inline-block;
  padding:.1rem .42rem;
  border-radius:4px;
  border:1px solid var(--line);
  background:var(--surface-2);
  color:var(--ink);
  font-weight:600;
  max-width:16ch;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  vertical-align:middle;
}


/* ============ 25. THE ARMORY ============
   Cosmetics, in a grid of swatches. Owned, equipped and not-yet-yours are three
   different states and none of them is a lock: a .cos--locked card still tells
   you what it is and what it costs, because a catalogue you cannot read is not
   a catalogue. */
.armory-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(168px,1fr));
  gap:.8rem;
}

.cos{
  display:flex;
  flex-direction:column;
  gap:.45rem;
  width:100%;
  padding:.7rem;
  text-align:left;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  color:inherit;
  transition:border-color .16s ease, transform .14s ease, box-shadow .16s ease;
}
.cos:hover{ border-color:var(--line-strong); transform:translateY(-2px); }
.cos b{ font-size:.86rem; font-weight:600; }
.cos small{ color:var(--faint); font-family:var(--mono); font-size:.68rem; }

/* The swatch is the item itself: a colour, a gradient, a trail or a keycap face,
   painted inline by js/cosmetics.js. All this owns is the frame. */
.cos__swatch{
  aspect-ratio:16/10;
  border-radius:6px;
  border:1px solid var(--line);
  background:var(--bg);
  overflow:hidden;
}
.cos__swatch canvas,
.cos__swatch svg{ display:block; width:100%; height:100%; }

.cos--owned{ border-color:var(--line-strong); }
.cos--equipped{
  border-color:var(--brand);
  box-shadow:0 0 0 1px var(--brand-deep), 0 0 22px rgba(56,223,210,.16);
}
.cos--equipped::after{
  content:'EQUIPPED';
  font-family:var(--mono);
  font-size:.58rem;
  letter-spacing:.15em;
  color:var(--brand);
}
/* Not owned yet: dimmed art, full-strength text. The price has to stay legible. */
.cos--locked .cos__swatch{ opacity:.4; filter:saturate(.5); }
.cos--locked b{ color:var(--muted); }


/* ============ 26. RARITY ============
   One set of edge treatments, shared by the loot cards, the vendor stock and the
   armory. A rarity class sets --r and everything downstream reads it, so the
   five looks stay in step and adding a sixth rarity is one block.

   The loudness ramp is deliberate and it is the whole point of a drop table:
   Standard is quiet (a hairline in the muted ink colour), Polished and Rare are
   clearly tinted, Elite reads across a table, and Legendary is unmistakable
   across a room. If everything shouted, nothing would. */
.r-standard{ --r:var(--r-standard); --r-soft:rgba(164,178,210,.14); }
.r-polished{ --r:var(--r-polished); --r-soft:rgba(56,223,210,.14); }
.r-rare{ --r:var(--r-rare); --r-soft:rgba(139,123,255,.16); }
.r-elite{ --r:var(--r-elite); --r-soft:rgba(242,179,61,.16); }
.r-legendary{ --r:var(--r-legendary); --r-soft:rgba(255,93,143,.18); }


/* ============ 27. SALVAGE: THE LOOT REVEAL ============
   The best moment in the game, so it is the one screen that gets real
   treatment. Cards turn over one at a time, each edged and lit in its rarity.
   The turn is a rotateY on a card that starts edge-on, so it reads as a card
   being flipped rather than a box fading in. */
.loot{
  display:flex;
  flex-wrap:wrap;
  gap:.8rem;
  justify-content:center;
  padding:.5rem 0;
  perspective:1000px;           /* the flip needs a vanishing point, and it has
                                   to live on the parent, not on the card */
}

.loot-card{
  position:relative;
  flex:0 0 auto;
  width:clamp(132px,26vw,168px);
  padding:.8rem .7rem .9rem;
  text-align:center;
  background:
    linear-gradient(180deg,var(--r-soft),transparent 62%),
    var(--plate);
  border:1px solid var(--r,var(--line-strong));
  border-radius:var(--radius);
  box-shadow:0 0 0 1px rgba(0,0,0,.35) inset, var(--shadow-2);
  transform-style:preserve-3d;
  backface-visibility:hidden;
}
.loot-card b{
  display:block;
  margin-top:.45rem;
  font-size:.88rem;
  font-weight:600;
  color:var(--ink);
  line-height:1.25;
}
.loot-card small{
  display:block;
  margin-top:.2rem;
  font-family:var(--mono);
  font-size:.62rem;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--r,var(--muted));
}
.loot-card .cos__swatch{ border-color:var(--r,var(--line)); }

/* The turn itself. It is applied by the module one card at a time (staggered
   with an animation-delay), so the reveal has a rhythm instead of arriving all
   at once. The base card is fully drawn without this class, so a module that
   never adds it degrades to a static row rather than an empty one. */
.loot-card--flip{ animation:btq-flip .5s cubic-bezier(.2,.75,.25,1) both; }

/* Rarity, from quiet to unmistakable. */
.loot-card.r-standard{ box-shadow:var(--shadow-1); }
.loot-card.r-polished{ box-shadow:0 0 18px rgba(56,223,210,.18), var(--shadow-2); }
.loot-card.r-rare{ box-shadow:0 0 26px rgba(139,123,255,.26), var(--shadow-2); }
.loot-card.r-elite{
  border-width:2px;
  box-shadow:0 0 34px rgba(242,179,61,.34), var(--shadow-2);
}
/* Legendary gets everything: a double-weight edge, a deep bloom, a light sweep
   across the face and a slow breathing halo. It should be identifiable from the
   other side of the room without reading a word of it. */
.loot-card.r-legendary{
  border-width:2px;
  box-shadow:0 0 48px rgba(255,93,143,.45), 0 0 100px rgba(255,93,143,.18), var(--shadow-3);
  animation:btq-legend-breathe 2.4s ease-in-out infinite;
}
.loot-card.r-legendary::after{
  content:'';
  position:absolute;
  inset:0;
  border-radius:inherit;
  pointer-events:none;
  background:linear-gradient(105deg,transparent 38%,rgba(255,255,255,.22) 50%,transparent 62%);
  background-size:280% 100%;
  animation:btq-sweep 2.6s linear infinite;
}
.loot-card.r-legendary.loot-card--flip{
  animation:btq-flip .5s cubic-bezier(.2,.75,.25,1) both,
            btq-legend-breathe 2.4s ease-in-out .5s infinite;
}

/* A duplicate is never nothing: it converts to chips, and the card says so
   plainly rather than looking like a failure. */
.loot-dupe{
  margin-top:.4rem;
  padding:.16rem .4rem;
  border-radius:999px;
  background:rgba(242,179,61,.12);
  color:var(--xp-2);
  font-family:var(--mono);
  font-size:.62rem;
  letter-spacing:.08em;
}
.loot-dupe::before{ content:'DUPLICATE '; color:var(--faint); }

/* A currency drop card: chips or cores rather than an item. */
.loot-cur{
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:.25rem;
  min-height:118px;
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
}
.loot-cur b{ font-size:1.5rem; margin:0; }
.loot-cur.chips b{ color:var(--xp-2); }
.loot-cur.cores b{ color:var(--violet); }


/* ============ 28. GEAR, SECTORS AND GAMBITS ============
   The loadout screen. Its whole job is to make the TRADE-OFF legible: every
   strong item in js/gear.js is worse at something, and a screen that showed only
   the upside would turn a build system into a shopping list. Positive deltas are
   green, negative ones red, and both are always shown. */
.loadout{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(230px,1fr));
  gap:.8rem;
}

.slot{
  display:flex;
  flex-direction:column;
  gap:.4rem;
  padding:.85rem;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  text-align:left;
  color:inherit;
  transition:border-color .16s ease;
}
.slot:hover{ border-color:var(--line-strong); }
.slot__name{
  font-family:var(--mono);
  font-size:.62rem;
  letter-spacing:.15em;
  text-transform:uppercase;
  color:var(--faint);
}
.slot__item{
  display:flex;
  align-items:center;
  gap:.4rem;
  font-weight:600;
  font-size:.94rem;
  color:var(--ink);
}
/* The upgrade level, as a +N pip beside the name. */
.slot__item .pip{
  padding:.06rem .34rem;
  border-radius:999px;
  background:rgba(242,179,61,.14);
  border:1px solid rgba(242,179,61,.34);
  color:var(--xp-2);
  font-family:var(--mono);
  font-size:.64rem;
  font-weight:600;
}
.slot__stats{
  display:flex;
  flex-wrap:wrap;
  gap:.3rem .55rem;
  font-family:var(--mono);
  font-size:.7rem;
  font-variant-numeric:tabular-nums;
}
.stat-up{ color:var(--good); }
.stat-down{ color:var(--bad); }
.stat-up::before{ content:'+'; }
/* No minus is prepended: a negative delta already carries its own sign, and a
   downside is more honest stated in the item's own numbers than decorated. */

/* The two halves of the accuracy bargain (damageFor() in js/gear.js): what one
   correct character is worth on a target you have not fumbled, and what the
   same character is worth after a slip. They are always shown as a PAIR, since
   either number alone is a half truth: a Railgun looks absurd until you see
   what it does foul, and a Repeater looks feeble until you see that it does not
   care. Good and bad colouring carries the whole legend, so no key is needed. */
.dmg-clean,
.dmg-foul{
  font-family:var(--mono);
  font-weight:600;
  font-variant-numeric:tabular-nums;
}
.dmg-clean{ color:var(--good); }
.dmg-foul{ color:var(--bad); }

/* ---- the two hardpoints ----
   A weapon rides on one of two mounts and is swapped mid-fight with Tab
   (WEAPON_SLOTS in js/gear.js), so "what am I holding right now" is a question
   the loadout screen and the playfield both have to answer without being read
   word by word. The card answers it on the loadout screen and the chip answers
   it over the game; they are kept in one block because they describe one
   system, and a change to the weapon rules should be one edit here.

   Only the LIVE mount's stats apply. The dormant card is therefore drawn quiet
   rather than merely un-highlighted: two cards stated with equal weight would
   read as though their numbers added together, which is the exact misreading
   the second hardpoint's design exists to avoid. */
.gun{
  display:flex;
  flex-direction:column;
  gap:.4rem;
  width:100%;
  padding:.8rem .85rem;
  text-align:left;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  color:var(--muted);
  transition:border-color .16s ease, color .16s ease, box-shadow .16s ease;
}
.gun:hover{ border-color:var(--line-strong); color:var(--ink); }
.gun b{ font-size:.94rem; font-weight:600; color:var(--ink); }
.gun small{ display:block; font-family:var(--mono); font-size:.68rem; color:var(--faint); }

/* The mount you are actually firing. Cyan, because on this screen that is the
   interactive fact: the other card is one keystroke away. */
.gun--live{
  border-color:var(--brand-deep);
  background:linear-gradient(180deg,var(--brand-tint),transparent);
  color:var(--ink);
  box-shadow:0 0 0 1px var(--brand-deep), 0 0 24px rgba(56,223,210,.12);
}

/* An empty mount is a CHOICE (it is the highest scoring build in the game),
   never a hole in the loadout, so it is dashed and calm rather than dimmed
   toward looking broken or unfinished. */
.gun--empty{ border-style:dashed; background:transparent; }
.gun--empty b{ color:var(--faint); font-weight:500; }

/* The spin-up after a swap: SWAP_MS of reduced damage, so it is a cost being
   paid down and it wears the caution colour rather than the accent. --v is the
   share of the spin-up STILL TO RUN (1 the instant you swap, 0 back at full
   damage), so the game drives it with one custom property per frame; an `i`
   child is supported as well, for a module that would rather render a real
   fill element than set a property. */
.gun__spin{
  height:4px;
  border-radius:999px;
  background:var(--surface-3);
  background-image:linear-gradient(90deg,var(--xp),var(--xp-2));
  background-repeat:no-repeat;
  background-size:calc(var(--v,0) * 100%) 100%;
}
.gun__spin i{
  display:block;
  height:100%;
  width:calc(var(--v,0) * 100%);
  border-radius:inherit;
  background:linear-gradient(90deg,var(--xp),var(--xp-2));
}

/* The HUD pair, drawn over the playfield by the game rather than by the armory.
   Small enough to be read in peripheral vision and deliberately quiet until it
   is the live one: a player checking which weapon is up is a player not
   watching the thing descending at their base. */
.gun-chip{
  display:inline-flex;
  align-items:center;
  gap:.3rem;
  padding:.14rem .45rem;
  border-radius:999px;
  background:rgba(8,11,20,.6);
  border:1px solid var(--line);
  color:var(--faint);
  font-family:var(--mono);
  font-size:.62rem;
  letter-spacing:.1em;
  text-transform:uppercase;
  white-space:nowrap;
  transition:border-color .14s ease, color .14s ease, background .14s ease;
}
.gun-chip--live{
  background:var(--brand-tint);
  border-color:var(--brand);
  color:var(--brand);
  box-shadow:0 0 14px rgba(56,223,210,.28);
}

/* Where the loadout sits between forgiving and precise (axisPosition() returns
   -1 to +1, and the panel maps that onto the track's left offset). A track with
   a pin rather than two decimals, because the fact worth reading at a glance is
   the POSITION, and neither end is the better one: the gradient runs violet to
   cyan, two equals, rather than red to green, which would read as a score.
   Cyan appears here as one end of a spectrum, not as something to press. */
.axis{
  display:flex;
  align-items:center;
  gap:.5rem;
  max-width:460px;               /* a scale, not a control: stretched across a
                                    full-width plate the pin's position stops
                                    being readable against either end */
  font-family:var(--mono);
  font-size:.62rem;
  letter-spacing:.12em;
  text-transform:uppercase;
  color:var(--faint);
}
.axis__end{ flex:0 0 auto; white-space:nowrap; }
.axis__track{
  position:relative;
  flex:1 1 auto;
  min-width:80px;
  height:6px;
  border-radius:999px;
  border:1px solid var(--line);
  background:linear-gradient(90deg,rgba(139,123,255,.4),var(--surface-3) 50%,rgba(56,223,210,.4));
}
.axis__pin{
  position:absolute;
  top:50%;
  left:50%;                      /* the panel overrides this per loadout */
  width:3px;
  height:14px;
  margin-left:-1.5px;
  border-radius:2px;
  background:var(--ink);
  box-shadow:0 0 10px rgba(233,239,255,.55);
  transform:translateY(-50%);
  transition:left .25s ease;     /* swapping an item slides the pin, so the
                                    trade-off is visible as a MOVE */
}

/* The single power rating. One honest number, because a count of items says
   nothing about whether a loadout can survive a sector. */
.power{
  display:flex;
  align-items:baseline;
  gap:.5rem;
  font-family:var(--mono);
  font-size:.7rem;
  letter-spacing:.13em;
  text-transform:uppercase;
  color:var(--faint);
}
.power__num{
  font-size:2rem;
  font-weight:600;
  letter-spacing:-.02em;
  text-transform:none;
  color:var(--brand);
  font-variant-numeric:tabular-nums;
  text-shadow:0 0 22px rgba(56,223,210,.3);
}

.sector{
  display:flex;
  flex-direction:column;
  gap:.35rem;
  width:100%;
  padding:.85rem;
  text-align:left;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  color:inherit;
  transition:border-color .16s ease, background .16s ease;
}
.sector:hover{ border-color:var(--line-strong); background:var(--surface-2); }
.sector b{ font-size:.95rem; }
.sector p{ margin:0; color:var(--muted); font-size:.8rem; }
.sector--on{
  border-color:var(--brand);
  background:linear-gradient(180deg,var(--brand-tint),transparent);
  box-shadow:0 0 0 1px var(--brand-deep);
}
.sector__power{
  font-family:var(--mono);
  font-size:.68rem;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--faint);
}
/* Under-geared is ADVICE, never a lock: every sector is enterable at any power,
   because a good typist genuinely clears above their gear. Amber (a caution)
   rather than red (a refusal) says exactly that. */
.sector-warn{
  margin-top:.3rem;
  padding:.4rem .55rem;
  background:rgba(242,179,61,.1);
  border-left:2px solid var(--xp);
  border-radius:0 4px 4px 0;
  color:var(--xp-2);
  font-size:.78rem;
  line-height:1.45;
}

/* A threat's vulnerability profile, from PROFILES in js/games/events.js. This
   is the badge that turns a loadout into a counter-pick, so it states a fact
   about the ENEMY and is deliberately not cyan: cyan means "you can press
   this" everywhere else on the site. Each profile keeps the colour of the
   thing it punishes, so the three are told apart across the room: plating is
   violet (cold and inert), a swarm is hazard pink (it is the threat that
   arrives in numbers), a volatile one is amber (unstable, and it feeds on the
   mistakes that heat the turret). */
.profile{
  display:inline-flex;
  align-items:center;
  gap:.34rem;
  padding:.16rem .52rem;
  border-radius:999px;
  border:1px solid var(--line-strong);
  background:var(--surface-2);
  color:var(--muted);
  font-family:var(--mono);
  font-size:.63rem;
  font-weight:600;
  letter-spacing:.13em;
  text-transform:uppercase;
}
.profile--armoured{ border-color:rgba(139,123,255,.5); background:rgba(139,123,255,.13); color:var(--violet); }
.profile--swarm{    border-color:rgba(255,93,143,.5);  background:rgba(255,93,143,.12);  color:var(--hazard); }
.profile--volatile{ border-color:rgba(242,179,61,.5);  background:rgba(242,179,61,.12);  color:var(--xp-2); }

/* The verdict from loadoutAdvice(): does what you are flying suit what you are
   about to meet. It is ADVICE and never a lock, which is the same call
   .sector-warn makes and the reason a poor fit is amber (a caution) rather
   than red (a refusal). Nothing here stops the run: a mismatched loadout is a
   harder fight, and a good typist genuinely clears one. */
.fit{
  padding:.5rem .6rem;
  border-left:2px solid var(--line-strong);
  border-radius:0 4px 4px 0;
  background:var(--surface-2);
  color:var(--muted);
  font-size:.8rem;
  line-height:1.45;
}
.fit--good{ border-left-color:var(--good); background:var(--good-tint); color:var(--ink); }
.fit--ok{   border-left-color:var(--line-strong); }
.fit--poor{ border-left-color:var(--xp); background:rgba(242,179,61,.1); color:var(--xp-2); }

/* The pre-fight briefing: what is in the sector, what it is vulnerable to, and
   what that means for the loadout. Short labelled rows rather than a paragraph,
   because this is read in the few seconds before a run starts, and mono so the
   numbers in the right-hand column line up down the block. */
.intel{
  display:flex;
  flex-direction:column;
  padding:.55rem .8rem;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
}
.intel__row{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:.8rem;
  padding:.36rem 0;
  border-bottom:1px solid var(--line);
  font-family:var(--mono);
  font-size:.76rem;
  font-variant-numeric:tabular-nums;
  color:var(--ink);
}
.intel__row:last-child{ border-bottom:0; }
/* The label side is the quiet one: the row exists for its value. */
.intel__row > :first-child{
  flex:0 0 auto;
  color:var(--faint);
  font-size:.62rem;
  letter-spacing:.11em;
  text-transform:uppercase;
}

/* A gambit is a handicap you opt into for a multiplier. Taking one is the
   player choosing to make it harder, so the ON state is hazard-coloured: it is
   the only place on the site where lighting something up means danger. */
.gambit{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:.6rem;
  width:100%;
  padding:.55rem .7rem;
  text-align:left;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  color:var(--muted);
  transition:border-color .15s ease, color .15s ease, background .15s ease;
}
.gambit:hover{ border-color:var(--line-strong); color:var(--ink); }
.gambit small{ display:block; color:var(--faint); font-size:.72rem; font-family:var(--sans); }
.gambit--on{
  border-color:var(--hazard);
  background:rgba(255,93,143,.09);
  color:var(--ink);
}
.gambit__mult{
  flex:0 0 auto;
  font-family:var(--mono);
  font-size:.8rem;
  font-weight:600;
  font-variant-numeric:tabular-nums;
  color:var(--xp-2);
}
.gambit--on .gambit__mult{ color:var(--hazard); text-shadow:0 0 12px rgba(255,93,143,.4); }


/* ============ 29. CONTRACTS, STREAK AND VENDORS ============ */
.contract{
  display:flex;
  flex-direction:column;
  gap:.5rem;
  padding:.85rem;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
}
.contract b{ font-size:.94rem; }
.contract p{ margin:0; color:var(--muted); font-size:.82rem; }
.contract--done{
  border-color:rgba(91,229,154,.4);
  background:linear-gradient(180deg,var(--good-tint),transparent);
}

/* The progress track. Like every bar in this file it takes either an inline
   width on a child fill or a --v on the track itself, so a module can render it
   with or without an inner element. */
.contract__bar{
  height:7px;
  border-radius:999px;
  background:var(--surface-3);
  overflow:hidden;
  background-image:linear-gradient(90deg,var(--brand-deep),var(--brand));
  background-repeat:no-repeat;
  background-size:calc(var(--v,0) * 100%) 100%;
  transition:background-size .4s cubic-bezier(.2,.7,.3,1);
}
.contract__bar i{
  display:block;
  height:100%;
  width:calc(var(--v,0) * 100%);
  border-radius:inherit;
  background:linear-gradient(90deg,var(--brand-deep),var(--brand));
}
.contract--done .contract__bar{ background-image:linear-gradient(90deg,#2E9C68,var(--good)); }

/* A claim is a reward you collect, and collecting is the reason to open the
   panel tomorrow, so the button is the loudest thing on the card. */
.contract__claim{
  align-self:flex-start;
  padding:.4rem .8rem;
  background:linear-gradient(180deg,var(--xp-2),var(--xp));
  border:1px solid var(--xp-2);
  border-radius:var(--radius-sm);
  color:#241703;
  font-family:var(--mono);
  font-size:.76rem;
  font-weight:600;
  letter-spacing:.05em;
  box-shadow:0 0 20px rgba(242,179,61,.28);
  animation:btq-pulse 2s ease-in-out infinite;
}
.contract__claim:disabled{
  background:var(--surface-2);
  border-color:var(--line);
  color:var(--faint);
  box-shadow:none;
  animation:none;
}

/* The streak. A flame that grows with the run of days, plus its insurance pips:
   a safety net nobody notices is a safety net that teaches nothing, so the
   tokens are always visible rather than surfacing only when spent. */
.streak{
  display:flex;
  align-items:center;
  gap:.6rem;
  padding:.6rem .8rem;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:999px;
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
}
.streak b{ font-size:1.1rem; color:var(--xp-2); }
.streak small{ color:var(--faint); font-size:.68rem; letter-spacing:.1em; text-transform:uppercase; }
.streak__flame{
  font-size:1.3rem;
  line-height:1;
  filter:drop-shadow(0 0 10px rgba(242,179,61,.6));
  animation:btq-flame 1.9s ease-in-out infinite;
}
.streak__pip{
  width:7px;
  height:7px;
  border-radius:50%;
  background:var(--surface-3);
  border:1px solid var(--line-strong);
}
.streak__pip.on{
  background:var(--violet);
  border-color:var(--violet);
  box-shadow:0 0 8px rgba(139,123,255,.55);
}

/* Three shopfronts, three personalities. The Quartermaster is plain, the
   Salvager carries a restock countdown (its stock is gone tomorrow, and that is
   the entire retention loop), and the Archivist is priced in cores, so it is
   violet and reads as scarcer than the other two. */
.vendor{
  display:flex;
  flex-direction:column;
  gap:.4rem;
  width:100%;
  padding:.9rem 1rem;
  text-align:left;
  background:var(--plate);
  border:1px solid var(--line);
  border-radius:var(--radius);
  color:inherit;
  transition:border-color .16s ease, transform .16s ease;
}
.vendor:hover{ border-color:var(--line-strong); transform:translateY(-2px); }
.vendor b{ font-family:var(--serif); font-size:1.05rem; font-weight:600; }
.vendor p{ margin:0; color:var(--muted); font-size:.83rem; }
.vendor--on{ border-color:var(--brand-deep); box-shadow:0 0 0 1px var(--brand-deep); }

/* What the shopkeeper says. It is a voice, so it is the one italic serif line
   in the app and it is set apart from the descriptive copy above it. */
.vendor__voice{
  margin-top:.2rem;
  padding-left:.65rem;
  border-left:2px solid var(--line-strong);
  color:var(--faint);
  font-family:var(--serif);
  font-style:italic;
  font-size:.88rem;
}
.vendor--on .vendor__voice{ border-left-color:var(--brand-deep); color:var(--muted); }

.restock{
  display:inline-flex;
  align-items:center;
  gap:.35rem;
  align-self:flex-start;
  padding:.16rem .5rem;
  border-radius:999px;
  background:rgba(242,179,61,.1);
  border:1px solid rgba(242,179,61,.3);
  color:var(--xp-2);
  font-family:var(--mono);
  font-size:.66rem;
  font-variant-numeric:tabular-nums;
  letter-spacing:.06em;
}
.restock::before{ content:'RESTOCK'; color:var(--faint); }

/* The Archivist's stock, priced in cores. */
.vendor.cores-only{ border-color:rgba(139,123,255,.34); }
.vendor.cores-only b{ color:var(--violet); }


/* ============ 30. THE ACADEMY ============
   Coverage and accuracy, never speed. The cards show the per-key evidence,
   because a beginner should always be able to read WHY a group is not certified
   yet rather than being told a bare no. */
.academy-grp{
  display:flex;
  flex-direction:column;
  gap:.5rem;
  padding:.9rem;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  transition:border-color .16s ease;
}
.academy-grp:hover{ border-color:var(--line-strong); }
.academy-grp h4{ margin:0; font-family:var(--sans); font-size:.95rem; }
.academy-grp p{ margin:0; color:var(--muted); font-size:.8rem; }
.academy-grp ul{ margin:0; padding-left:1.1rem; color:var(--faint); font-size:.78rem; }

.academy-grp--cert{
  border-color:rgba(91,229,154,.4);
  background:linear-gradient(180deg,var(--good-tint),transparent);
}

/* A certification is a stamp, not a checkbox: it is the biggest single reward a
   slow typist reaches on their first evening and it should look like something
   pressed onto the card. It lands with a slight rotation, off the horizontal,
   the way a real stamp does. */
.cert-stamp{
  align-self:flex-start;
  padding:.24rem .6rem;
  border:2px solid var(--good);
  border-radius:5px;
  color:var(--good);
  font-family:var(--mono);
  font-size:.66rem;
  font-weight:700;
  letter-spacing:.2em;
  text-transform:uppercase;
  transform:rotate(-3deg);
  box-shadow:0 0 18px rgba(91,229,154,.2);
  animation:btq-stamp .38s cubic-bezier(.2,1.5,.4,1) both;
}

/* The diploma: the whole keyboard, under your fingers. It is recognition and
   nothing else (it unlocks nothing, deliberately), so it is allowed to be the
   most decorated block on the site. */
.diploma{
  padding:clamp(1.2rem,4vw,2rem);
  text-align:center;
  background:
    radial-gradient(90% 120% at 50% 0%,rgba(242,179,61,.12),transparent 62%),
    var(--plate);
  border:1px solid var(--xp);
  border-radius:var(--radius);
  box-shadow:0 0 46px rgba(242,179,61,.18), var(--shadow-2);
}
.diploma h2{ font-family:var(--serif); color:var(--xp-2); }
.diploma .label{ color:var(--xp); }
.diploma p{ color:var(--muted); max-width:52ch; margin-left:auto; margin-right:auto; }

/* The key rows on an Academy card: the actual keys the group teaches, drawn as
   keycaps so the group is recognisable before its name is read. */
.keycap-row{
  display:flex;
  flex-wrap:wrap;
  gap:.3rem;
  align-items:center;
}
.keycap{
  min-width:26px;
  padding:.24rem .3rem;
  border-radius:5px;
  background:var(--surface-2);
  border:1px solid var(--line-strong);
  border-bottom-width:2px;
  color:var(--ink);
  font-family:var(--mono);
  font-size:.76rem;
  text-align:center;
  line-height:1.1;
}
.keycap.good{ border-color:rgba(91,229,154,.45); color:var(--good); }
.keycap.weak{ border-color:rgba(255,93,143,.5); color:var(--hazard); background:rgba(255,93,143,.08); }
.keycap.unseen{ border-style:dashed; color:var(--faint); }


/* ============ 31. THE IN-GAME HUD ============
   Sits over the playfield, so everything in it is mono, tabular and readable in
   peripheral vision: a player looking at the HUD is a player not looking at the
   thing descending toward their base. */
.hud{
  display:flex;
  align-items:center;
  gap:clamp(.5rem,2vw,1.2rem);
  flex-wrap:wrap;
  padding:.5rem .75rem;
  background:rgba(8,11,20,.7);
  border-bottom:1px solid var(--line);
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
  backdrop-filter:blur(6px);
  -webkit-backdrop-filter:blur(6px);
}
.hud__stat{ display:flex; flex-direction:column; line-height:1.15; }
.hud__stat b{ font-size:1.02rem; font-weight:600; }
.hud__stat span{ font-size:.58rem; letter-spacing:.14em; text-transform:uppercase; color:var(--faint); }

/* The combo, which drives the score multiplier and the SFX pitch. It grows as
   it climbs, so the number itself is the feedback. */
.combo{
  font-family:var(--mono);
  font-size:1.3rem;
  font-weight:700;
  color:var(--brand);
  text-shadow:0 0 16px rgba(56,223,210,.45);
  transition:color .2s ease, text-shadow .2s ease, transform .12s ease;
}
.combo.hot{ color:var(--xp); text-shadow:0 0 20px rgba(242,179,61,.55); transform:scale(1.06); }
.combo.max{ color:var(--hazard); text-shadow:0 0 26px rgba(255,93,143,.6); transform:scale(1.12); }

/* Heat. It rises on a mistype and jams the turret when it fills, so it runs
   cyan to magenta: the accent means fine, the hazard colour means about to
   cost you. */
.heat{
  flex:1 1 90px;
  min-width:70px;
  max-width:180px;
  height:7px;
  border-radius:999px;
  background:var(--surface-3);
  overflow:hidden;
}
.heat__fill{
  display:block;
  height:100%;
  width:calc(var(--v,0) * 100%);
  border-radius:inherit;
  background:linear-gradient(90deg,var(--brand),var(--xp) 62%,var(--hazard));
  transition:width .12s linear;
}
.heat.jam{ box-shadow:0 0 0 1px var(--hazard), 0 0 16px rgba(255,93,143,.45); }
.heat.jam .heat__fill{ animation:btq-pulse .4s ease-in-out infinite; }

/* Base health. Green while it is fine and red when it is not, which is the one
   place on this site where the traffic-light reading is the right one. */
.basebar{
  flex:1 1 110px;
  min-width:80px;
  max-width:240px;
  height:9px;
  border-radius:999px;
  background:var(--surface-3);
  border:1px solid var(--line);
  overflow:hidden;
}
.basebar__fill{
  display:block;
  height:100%;
  width:calc(var(--v,1) * 100%);
  border-radius:inherit;
  background:linear-gradient(90deg,#2E9C68,var(--good));
  transition:width .3s ease;
}
.basebar.low .basebar__fill{ background:linear-gradient(90deg,#B37A1F,var(--xp)); }
.basebar.crit .basebar__fill{ background:linear-gradient(90deg,#B33A63,var(--hazard)); animation:btq-pulse .7s ease-in-out infinite; }

/* The Zone: flow, named and lit. It is a state class the game puts on the HUD
   or on the host, so it is written to work on either. The treatment is an inner
   glow rather than anything that moves, because the whole point of the Zone is
   that the player is concentrating and must not be interrupted. */
.zone-on{ box-shadow:inset 0 0 0 1px rgba(139,123,255,.5), inset 0 0 60px rgba(139,123,255,.14); }
.zone-on .combo{ color:var(--violet); text-shadow:0 0 22px rgba(139,123,255,.6); }


/* ============ 32. EVENTS AND BOSSES ============
   The rhythm layer over the wave counter. An event banner slides in over the
   playfield, says what is happening and what it is worth, and drains a timer
   while it lasts. */
.ev-banner{
  position:absolute;
  left:50%;
  top:12%;
  transform:translateX(-50%);
  z-index:6;
  width:min(90%,460px);
  padding:.7rem .9rem .55rem;
  background:rgba(18,24,41,.9);
  border:1px solid var(--violet);
  border-radius:var(--radius);
  box-shadow:0 0 40px rgba(139,123,255,.28), var(--shadow-3);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
  text-align:center;
  pointer-events:none;          /* it is information over a live playfield and
                                   must never eat a click meant for the game */
  animation:btq-ev-in .34s cubic-bezier(.2,.9,.25,1) both;
}
.ev-banner p{ margin:.2rem 0 .5rem; color:var(--muted); font-size:.82rem; }

.ev-banner__name{
  font-family:var(--serif);
  font-size:1.15rem;
  font-weight:700;
  letter-spacing:.01em;
  color:var(--ink);
}

/* The draining clock. It empties right to left off --t (1 at the start, 0 at
   the end), so the module can drive it with one custom property per frame and
   nothing else. */
.ev-banner__timer{
  height:4px;
  border-radius:999px;
  background:var(--surface-3);
  background-image:linear-gradient(90deg,var(--violet),#C8BFFF);
  background-repeat:no-repeat;
  background-size:calc(var(--t,1) * 100%) 100%;
}
.ev-banner__timer i{
  display:block;
  height:100%;
  width:calc(var(--t,1) * 100%);
  border-radius:inherit;
  background:linear-gradient(90deg,var(--violet),#C8BFFF);
}

/* What the event is worth. It is the reason to fight through one rather than
   hide from it, so it is stated in the currency colour and stated large. */
.ev-mult{
  display:inline-block;
  padding:.1rem .45rem;
  border-radius:999px;
  background:rgba(242,179,61,.14);
  border:1px solid rgba(242,179,61,.4);
  color:var(--xp-2);
  font-family:var(--mono);
  font-size:.78rem;
  font-weight:700;
  font-variant-numeric:tabular-nums;
}

/* ---- the boss health bar ----
   Across the top of the playfield, because a boss is the only threat that is
   the whole screen's business. */
.bossbar{
  position:relative;
  height:14px;
  border-radius:999px;
  background:var(--surface-3);
  border:1px solid var(--hazard);
  overflow:hidden;
  box-shadow:0 0 26px rgba(255,93,143,.24);
}
.bossbar__fill{
  display:block;
  height:100%;
  width:calc(var(--v,1) * 100%);
  border-radius:inherit;
  background:linear-gradient(90deg,#8C1F42,var(--hazard) 70%,#FF9CBD);
  transition:width .28s ease;
}
/* Phase pips ride ON the bar, marking where each plate falls, so the fight's
   shape is readable at a glance: how many phases are left is a different
   question from how much health is left. */
.bossbar__pips{
  position:absolute;
  inset:0;
  display:flex;
  align-items:center;
  justify-content:space-evenly;
  pointer-events:none;
}
.bossbar__pips i{
  width:6px;
  height:6px;
  border-radius:50%;
  background:rgba(8,11,20,.75);
  box-shadow:0 0 0 1px rgba(255,255,255,.25);
}
.bossbar__pips i.done{ background:var(--hazard); box-shadow:0 0 10px rgba(255,93,143,.7); }

.boss-name{
  font-family:var(--serif);
  font-size:1.05rem;
  font-weight:700;
  color:var(--hazard);
  letter-spacing:.02em;
  text-shadow:0 0 18px rgba(255,93,143,.4);
}
.boss-name small{
  display:block;
  font-family:var(--mono);
  font-size:.62rem;
  font-weight:400;
  letter-spacing:.16em;
  text-transform:uppercase;
  color:var(--faint);
}

/* The command word that drops a shield. It is a single word you have to type
   exactly, so it is set apart from the transmission entirely: caps, wide
   tracking, and a frame that says "this is a lock". */
.boss-shield,
.tx-shield{
  display:inline-flex;
  align-items:center;
  gap:.5rem;
  padding:.34rem .8rem;
  background:rgba(139,123,255,.12);
  border:1px dashed var(--violet);
  border-radius:var(--radius-sm);
  color:var(--violet);
  font-family:var(--mono);
  font-size:.95rem;
  font-weight:700;
  letter-spacing:.28em;
  text-transform:uppercase;
}
.boss-shield::before,
.tx-shield::before{
  content:'SHIELD';
  font-size:.58rem;
  letter-spacing:.14em;
  color:var(--faint);
}

/* ---- the transmission ----
   A boss is typed as PROSE, not as words, and that is the whole texture change
   the fight exists for: no target switching, one long unbroken line of text
   under fire. So this is a reading surface first and a game element second. It
   gets a calm backdrop, a generous line height, a measure capped near 62
   characters (past that the eye loses the line return) and mono at a size you
   could read across a desk, because things are still descending behind it. */
.tx{
  position:absolute;
  left:50%;
  top:50%;
  transform:translate(-50%,-50%);
  z-index:5;
  width:min(94%,760px);
  padding:clamp(.9rem,3vw,1.4rem);
  background:rgba(8,11,20,.86);
  border:1px solid var(--line-strong);
  border-radius:var(--radius);
  box-shadow:0 0 0 1px rgba(255,93,143,.16), var(--shadow-3);
  backdrop-filter:blur(9px);
  -webkit-backdrop-filter:blur(9px);
  text-align:left;
}
.tx__text{
  max-width:62ch;
  margin:0 auto;
  font-family:var(--mono);
  font-size:clamp(1rem,1.9vw,1.22rem);
  line-height:1.9;
  letter-spacing:.01em;
  word-break:break-word;
  color:var(--faint);
}
/* Typed so far. Cyan, and slightly dimmed against the ink of what is next, so
   the eye is pulled forward rather than back over work already done. */
.tx__done{ color:var(--brand); }
/* Where you are. A block that inverts rather than covers, because on a
   paragraph the current character has to stay readable. */
.tx__cur{
  display:inline-block;
  min-width:.62em;
  background:var(--brand);
  color:var(--brand-ink);
  border-radius:2px;
  box-shadow:0 0 14px rgba(56,223,210,.5);
}
.tx__rest{ color:var(--muted); }
.tx__bad{
  color:var(--brand-ink);
  background:var(--hazard);
  border-radius:2px;
}
.tx__bar{
  height:5px;
  margin-top:.9rem;
  border-radius:999px;
  background:var(--surface-3);
  background-image:linear-gradient(90deg,var(--brand-deep),var(--brand));
  background-repeat:no-repeat;
  background-size:calc(var(--v,0) * 100%) 100%;
  transition:background-size .16s linear;
}
.tx__bar i{
  display:block;
  height:100%;
  width:calc(var(--v,0) * 100%);
  border-radius:inherit;
  background:linear-gradient(90deg,var(--brand-deep),var(--brand));
}


/* ============ 33. THE REPORT (END SCREEN) ============
   What a run was worth, itemised. The breakdown is the point: a beginner's XP
   is mostly flat bonuses and an expert's is mostly pace, and seeing WHICH is
   what makes the progression legible rather than magic. */
.report{
  max-width:620px;
  margin:0 auto;
  padding:clamp(1rem,3vw,1.6rem);
  background:var(--plate);
  border:1px solid var(--line-strong);
  border-radius:var(--radius);
  box-shadow:var(--shadow-2);
  text-align:center;
}
.report__xp{
  font-family:var(--mono);
  font-size:clamp(2.2rem,7vw,3.2rem);
  font-weight:700;
  font-variant-numeric:tabular-nums;
  line-height:1;
  color:var(--xp);
  text-shadow:0 0 34px rgba(242,179,61,.34);
}
.report__xp::after{
  content:' XP';
  font-size:.34em;
  letter-spacing:.2em;
  color:var(--faint);
  text-shadow:none;
}
.report__line{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:1rem;
  padding:.34rem 0;
  border-bottom:1px dashed var(--line);
  font-family:var(--mono);
  font-size:.82rem;
  font-variant-numeric:tabular-nums;
  text-align:left;
  color:var(--muted);
}
.report__line:last-child{ border-bottom:0; }
.report__line b{ color:var(--xp-2); font-weight:600; }

/* A level up is the loudest good news the game has, so it arrives rather than
   appearing. */
.levelup{
  margin:.9rem 0;
  padding:.8rem 1rem;
  background:linear-gradient(180deg,rgba(242,179,61,.18),transparent);
  border:1px solid var(--xp);
  border-radius:var(--radius);
  color:var(--xp-2);
  font-family:var(--mono);
  font-size:1.02rem;
  font-weight:600;
  letter-spacing:.06em;
  box-shadow:0 0 40px rgba(242,179,61,.22);
  animation:btq-levelup .5s cubic-bezier(.2,1.3,.35,1) both;
}
.levelup b{ font-size:1.4em; color:var(--ink); }


/* ============ 34. KEYFRAMES ============
   All animation lives here so the reduced-motion block at the end has one
   place to switch off. */
@keyframes btq-pulse{
  0%,100%{ opacity:1; }
  50%{ opacity:.5; }
}
@keyframes btq-caret{
  0%,49%{ opacity:1; }
  50%,100%{ opacity:.25; }
}
@keyframes btq-toast-in{
  from{ opacity:0; transform:translate(-50%,10px); }
  to{ opacity:1; transform:translate(-50%,0); }
}
@keyframes btq-flip{
  from{ opacity:0; transform:rotateY(90deg) translateY(8px); }
  60%{ opacity:1; }
  to{ opacity:1; transform:rotateY(0) translateY(0); }
}
@keyframes btq-legend-breathe{
  0%,100%{ box-shadow:0 0 48px rgba(255,93,143,.45), 0 0 100px rgba(255,93,143,.18), var(--shadow-3); }
  50%{ box-shadow:0 0 66px rgba(255,93,143,.62), 0 0 130px rgba(255,93,143,.26), var(--shadow-3); }
}
@keyframes btq-sweep{
  from{ background-position:180% 0; }
  to{ background-position:-80% 0; }
}
@keyframes btq-ev-in{
  from{ opacity:0; transform:translate(-50%,-14px); }
  to{ opacity:1; transform:translate(-50%,0); }
}
@keyframes btq-flame{
  0%,100%{ transform:scale(1) rotate(-2deg); }
  50%{ transform:scale(1.12) rotate(2deg); }
}
@keyframes btq-stamp{
  from{ opacity:0; transform:rotate(-3deg) scale(1.7); }
  to{ opacity:1; transform:rotate(-3deg) scale(1); }
}
@keyframes btq-levelup{
  from{ opacity:0; transform:translateY(12px) scale(.96); }
  to{ opacity:1; transform:translateY(0) scale(1); }
}


/* ============ 35. THE OPERATIONS DECK ============
   The arcade's mission select, staged as a map room: a nebula out the window,
   a perspective floor grid, a holographic galaxy tilted on the chart table
   (real CSS 3D: the disc is rotated in space and the beacons counter-rotate to
   billboard their labels), and the briefing panel beside it. Every image here
   is one of the generated pieces named in ART_DIRECTION.md; every colour is a
   token or an rgba mix of one, same as the rest of the file. */
.ops{
  position:relative;
  margin:0 0 1.4rem;
  border:1px solid var(--line);
  border-radius:var(--radius);
  overflow:hidden;
  background:var(--bg);
  box-shadow:var(--shadow-2);
}
.ops-sky{
  position:absolute;
  inset:0;
  background-size:cover;
  background-position:center;
  opacity:.85;
}
.ops-sky::after{
  content:'';
  position:absolute;
  inset:0;
  background:
    radial-gradient(90% 70% at 50% 8%,transparent 40%,rgba(8,11,20,.8) 95%),
    linear-gradient(180deg,rgba(8,11,20,.12),rgba(8,11,20,.72));
}
.ops-head{
  position:relative;
  z-index:1;
  padding:1.2rem 1.3rem 0;
}
.ops-head h2{ margin:.1rem 0 .2rem; }
.ops-head p:last-child{ margin:0; color:var(--muted); font-size:.86rem; max-width:52ch; }
.ops-floor{
  position:absolute;
  left:-24%;
  right:-24%;
  bottom:-2%;
  height:56%;
  background:
    repeating-linear-gradient(90deg,rgba(56,223,210,.08) 0 1px,transparent 1px 62px),
    repeating-linear-gradient(0deg,rgba(56,223,210,.05) 0 1px,transparent 1px 42px);
  transform:perspective(620px) rotateX(60deg);
  transform-origin:50% 100%;
  opacity:.5;
  -webkit-mask-image:linear-gradient(180deg,transparent,#000 40%);
  mask-image:linear-gradient(180deg,transparent,#000 40%);
}

.ops-scene{
  position:relative;
  display:grid;
  grid-template-columns:1.12fr .88fr;
  gap:1rem;
  align-items:center;
  padding:.4rem 1.1rem 1.1rem;
  min-height:520px;
}

/* The chart table rides sticky while the briefing scrolls, so the galaxy is
   in view for every decision it informs. Offset by the masthead's height,
   which is already a token. */
.ops-table{
  position:sticky;
  top:calc(var(--head-h) - 44px);
  align-self:start;
  display:flex;
  align-items:center;
  justify-content:center;
  padding:.4rem 0 2.2rem;
  perspective:1050px;
}
.ops-disc{
  position:relative;
  width:min(100%,640px);
  aspect-ratio:1;
  transform:rotateX(49deg);
  transform-style:preserve-3d;
}
.ops-disc::before{
  content:'';
  position:absolute;
  inset:-3.5%;
  border:1px solid rgba(56,223,210,.28);
  border-radius:50%;
  box-shadow:inset 0 0 44px rgba(56,223,210,.1);
}
.ops-galaxy{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  mix-blend-mode:screen;
  pointer-events:none;
  user-select:none;
  animation:ops-spin 300s linear infinite;
}
@keyframes ops-spin{ to{ transform:rotate(360deg); } }
.ops-holo{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  mix-blend-mode:screen;
  pointer-events:none;
}
.ops-pedestal{
  position:absolute;
  left:50%;
  bottom:.6rem;
  width:46%;
  height:1.6rem;
  transform:translateX(-50%);
  background:radial-gradient(50% 100% at 50% 0,rgba(56,223,210,.35),transparent 72%);
  filter:blur(7px);
}

/* Sector beacons live IN the tilted plane and counter-rotate so their labels
   stand up out of the table like projected tags. Every one is enabled at every
   power: the selected one glows, the rest invite. */
.ops-beacon{
  position:absolute;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:.35rem;
  padding:.3rem;
  background:none;
  border:0;
  color:var(--ink);
  transform:translate(-50%,-50%) rotateX(-52deg);
  transform-origin:50% 80%;
  cursor:pointer;
}
.ops-beacon i{
  width:13px;
  height:13px;
  border-radius:50%;
  background:var(--brand);
  box-shadow:0 0 12px var(--brand),0 0 28px rgba(56,223,210,.55);
  position:relative;
}
.ops-beacon i::after{
  content:'';
  position:absolute;
  inset:-7px;
  border:1px solid rgba(56,223,210,.5);
  border-radius:50%;
  animation:beacon-ping 2.6s ease-out infinite;
}
@keyframes beacon-ping{
  0%{ transform:scale(.5); opacity:.9; }
  80%,100%{ transform:scale(1.7); opacity:0; }
}
.ops-beacon span{
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:.05rem;
  padding:.26rem .55rem;
  background:rgba(8,11,20,.82);
  border:1px solid var(--line-strong);
  border-radius:var(--radius-sm);
  font-family:var(--mono);
  font-size:.68rem;
  letter-spacing:.08em;
  text-transform:uppercase;
  white-space:nowrap;
  backdrop-filter:blur(3px);
  -webkit-backdrop-filter:blur(3px);
}
.ops-beacon span small{ font-size:.56rem; color:var(--faint); letter-spacing:.12em; }
.ops-beacon:hover i{ box-shadow:0 0 16px var(--brand-2),0 0 36px rgba(56,223,210,.7); }
.ops-beacon:hover span{ border-color:var(--brand-deep); }
.ops-beacon--on i{
  background:var(--brand-2);
  box-shadow:0 0 8px var(--ink),0 0 18px var(--brand-2),0 0 40px rgba(56,223,210,.8);
}
.ops-beacon--on span{
  border-color:var(--brand);
  color:var(--brand-2);
  box-shadow:0 0 0 1px rgba(56,223,210,.25);
}

/* the mission briefing */
.ops-brief{
  position:relative;
  background:linear-gradient(180deg,rgba(8,11,20,.86),rgba(18,24,41,.92));
  border:1px solid var(--line-strong);
  border-radius:var(--radius);
  overflow:hidden;
  box-shadow:var(--shadow-2);
  backdrop-filter:blur(6px);
  -webkit-backdrop-filter:blur(6px);
}
.ops-brief::before,
.ops-brief::after{
  content:'';
  position:absolute;
  width:15px;
  height:15px;
  opacity:.75;
  pointer-events:none;
  z-index:2;
}
.ops-brief::before{ top:6px; left:6px; border-top:2px solid var(--brand); border-left:2px solid var(--brand); }
.ops-brief::after{ bottom:6px; right:6px; border-bottom:2px solid var(--brand); border-right:2px solid var(--brand); }

.ops-vista{ position:relative; aspect-ratio:21/9; border-bottom:1px solid var(--line-strong); }
.ops-vista img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.ops-vista::after{
  content:'';
  position:absolute;
  inset:0;
  background:linear-gradient(180deg,rgba(8,11,20,.1) 30%,rgba(8,11,20,.88));
}
.ops-vista__name{ position:absolute; left:.9rem; bottom:.5rem; z-index:1; }
.ops-vista__name .label{ margin:0; }
.ops-vista__name b{ display:block; font-family:var(--serif); font-size:1.35rem; line-height:1.15; }

.ops-brief__body{ padding:.85rem 1rem 1.05rem; display:flex; flex-direction:column; gap:.45rem; }
.ops-row{ display:flex; align-items:center; gap:.6rem; flex-wrap:wrap; }
.ops-req{ font-size:.66rem; color:var(--faint); letter-spacing:.12em; }
.ops-blurb{ margin:0; color:var(--muted); font-size:.88rem; }

.ops-rota{ display:flex; flex-direction:column; gap:.4rem; }
.ops-boss{
  display:flex;
  align-items:center;
  gap:.6rem;
  padding:.35rem .5rem;
  background:rgba(8,11,20,.55);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
}
.ops-boss img{ width:46px; height:46px; object-fit:contain; filter:drop-shadow(0 0 8px rgba(255,93,143,.35)); flex:none; }
.ops-boss b{ display:block; font-size:.84rem; }
.ops-boss small{ color:var(--faint); font-size:.7rem; }
.ops-fitdot{ margin-left:auto; width:9px; height:9px; border-radius:50%; flex:none; }
.ops-fitdot--good{ background:var(--good); box-shadow:0 0 8px rgba(91,229,154,.7); }
.ops-fitdot--ok{ background:var(--xp); box-shadow:0 0 8px rgba(242,179,61,.6); }
.ops-fitdot--poor{ background:var(--hazard); box-shadow:0 0 8px rgba(255,93,143,.7); }

.ops-guns{ display:flex; flex-direction:column; gap:.4rem; }
.ops-gun{
  display:flex;
  align-items:center;
  gap:.55rem;
  padding:.35rem .5rem;
  background:rgba(8,11,20,.55);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
}
.ops-gun--live{ border-color:var(--brand-deep); box-shadow:0 0 0 1px rgba(56,223,210,.18); }
.ops-gun--empty{ opacity:.6; }
.ops-gun img{ width:40px; height:40px; mix-blend-mode:screen; flex:none; }
.ops-gun b{ display:block; font-size:.82rem; }
.ops-gun small{ display:block; color:var(--faint); font-size:.68rem; }

.ops-kitrow{ display:flex; align-items:center; gap:.35rem; flex-wrap:wrap; }
.ops-kit{
  width:30px;
  height:30px;
  padding:2px;
  background:rgba(8,11,20,.8);
  border:1px solid var(--line);
  border-radius:6px;
  mix-blend-mode:screen;
}

.ops-protocols{ margin-top:.3rem; border-top:1px solid var(--line); padding-top:.5rem; }
.ops-protocols summary{
  cursor:pointer;
  font-family:var(--mono);
  font-size:.7rem;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--muted);
}
.ops-protocols summary b{ color:var(--xp-2); }
.ops-protogrid{ display:grid; grid-template-columns:1fr 1fr; gap:.45rem; margin-top:.45rem; }

.ops-launch{
  margin-top:.65rem;
  width:100%;
  padding:.8rem 1rem;
  font-size:.98rem;
  letter-spacing:.08em;
  text-transform:uppercase;
  box-shadow:0 0 0 1px rgba(56,223,210,.3),0 0 30px rgba(56,223,210,.25);
}
.ops-note{ margin:.4rem 0 0; font-size:.62rem; color:var(--faint); letter-spacing:.1em; text-transform:uppercase; }

.hub-badge--rec{ color:var(--brand-2); border-color:var(--brand-deep); }

@media (max-width:980px){
  .ops-scene{ grid-template-columns:1fr; min-height:0; }
  .ops-table{ position:static; align-self:auto; padding-bottom:1.6rem; }
  .ops-disc{ width:min(96%,470px); }
}
@media (max-width:460px){
  .ops-scene{ padding:.7rem; }
  .ops-beacon span{ font-size:.56rem; }
  .ops-protogrid{ grid-template-columns:1fr; }
}


/* ============ 36. THE HANGAR BAY ============
   The Armory's loadout, staged: the equipped hull's render floating over a lit
   pad in the bay, service nodes down both flanks. A node opens exactly the
   picker its card's Change button opens; the stage is presentation over the
   same slots, never a second system. */
.hangar{
  position:relative;
  margin:0 0 1rem;
  border:1px solid var(--line);
  border-radius:var(--radius);
  overflow:hidden;
  box-shadow:var(--shadow-2);
  background:var(--bg);
}
.hangar-bay{
  position:absolute;
  inset:0;
  background-size:cover;
  background-position:center 68%;
  opacity:.6;
}
.hangar-bay::after{
  content:'';
  position:absolute;
  inset:0;
  background:linear-gradient(180deg,rgba(8,11,20,.55),rgba(8,11,20,.25) 45%,rgba(8,11,20,.82));
}
.hangar-grid{
  position:relative;
  display:grid;
  grid-template-columns:minmax(190px,1fr) minmax(250px,1.5fr) minmax(190px,1fr);
  gap:.8rem;
  align-items:center;
  padding:1rem;
  min-height:430px;
}
.hangar-col{ display:flex; flex-direction:column; gap:.5rem; }

.hg-node{
  display:flex;
  align-items:center;
  gap:.55rem;
  padding:.42rem .55rem;
  background:rgba(8,11,20,.74);
  border:1px solid var(--line-strong);
  border-radius:var(--radius-sm);
  color:inherit;
  text-align:left;
  cursor:pointer;
  backdrop-filter:blur(3px);
  -webkit-backdrop-filter:blur(3px);
  transition:border-color .16s ease, transform .16s ease, box-shadow .16s ease;
}
.hg-node:hover{ border-color:var(--brand-deep); transform:translateY(-1px); }
.hg-node--open{ border-color:var(--brand); box-shadow:0 0 0 1px rgba(56,223,210,.3); }
.hg-node img{ width:44px; height:44px; mix-blend-mode:screen; flex:none; }
.hg-node small{
  display:block;
  color:var(--faint);
  font-family:var(--mono);
  font-size:.56rem;
  letter-spacing:.12em;
  text-transform:uppercase;
}
.hg-node b{ font-size:.8rem; font-weight:600; }

.hangar-stage{
  position:relative;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:flex-end;
  min-height:390px;
}
.hangar-pad{
  position:absolute;
  bottom:3rem;
  left:50%;
  width:74%;
  height:24%;
  transform:translateX(-50%);
  background:radial-gradient(50% 60% at 50% 50%,rgba(56,223,210,.32),transparent 72%);
  filter:blur(9px);
}
.hangar-ship{
  position:relative;
  width:min(100%,360px);
  filter:drop-shadow(0 20px 28px rgba(0,0,0,.65)) drop-shadow(0 0 26px rgba(56,223,210,.16));
  animation:hangar-hover 7s ease-in-out infinite;
  user-select:none;
}
@keyframes hangar-hover{
  0%,100%{ transform:translateY(0); }
  50%{ transform:translateY(-10px); }
}
.hangar-plate{
  position:relative;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:.05rem;
  margin-top:.4rem;
  padding:.4rem .95rem;
  background:rgba(8,11,20,.82);
  border:1px solid var(--line-strong);
  border-radius:var(--radius-sm);
}
.hangar-plate .label{ margin:0; }
.hangar-plate b{ font-family:var(--serif); font-size:1.02rem; }
.hangar-plate .mono{ font-size:.66rem; color:var(--brand); letter-spacing:.14em; }

@media (max-width:980px){
  .hangar-grid{ grid-template-columns:1fr 1fr; }
  .hangar-stage{ grid-column:1 / -1; order:-1; min-height:320px; }
}
@media (max-width:560px){
  .hangar-grid{ grid-template-columns:1fr; }
}


/* ============ 37. RENDERED GEAR, VENDOR FACES AND THE MUSIC KEY ============
   The small chrome that carries the art everywhere else: hologram icons on
   cards (screen-blended, so their black plates vanish into any dark tile),
   vendor portraits, boss faces in the fight strip, currency marks, and the
   masthead's music toggle. */
.gear-ico{ width:46px; height:46px; mix-blend-mode:screen; flex:none; }
.gun__head{ display:flex; align-items:center; gap:.6rem; }
.slot__id{ display:flex; align-items:center; gap:.5rem; }

.cos__swatch--gear{
  display:flex;
  align-items:center;
  justify-content:center;
  min-height:64px;
  background:radial-gradient(70% 70% at 50% 45%,rgba(56,223,210,.1),transparent),rgba(8,11,20,.92);
}
.cos__swatch--gear img{ width:74%; height:74%; max-height:96px; object-fit:contain; mix-blend-mode:screen; }

.vendor__face{
  width:100%;
  aspect-ratio:5/4;
  object-fit:cover;
  object-position:50% 18%;
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  margin-bottom:.45rem;
  opacity:.92;
}

.boss-face{
  width:44px;
  height:44px;
  object-fit:contain;
  flex:none;
  filter:drop-shadow(0 0 7px rgba(255,93,143,.5));
}

.cur-ico{ width:15px; height:15px; vertical-align:-3px; margin-right:.28rem; mix-blend-mode:screen; }

#musicBtn[aria-pressed="false"]{ opacity:.4; }
#musicBtn[aria-pressed="true"]{ color:var(--brand); }


/* ============ 38. RESPONSIVE ============
   Two steps. Under 720px the masthead gives up its two-column identity row and
   the dense grids collapse; under 460px everything that can shed a column does,
   and the level chip drops its bar rather than its number. */
@media (max-width:720px){
  .masthead-inner{
    grid-template-columns:auto 1fr auto;
    grid-template-areas:
      'brand acct set'
      'chip  chip chip'
      'tabs  tabs tabs';
    gap:.45rem .6rem;
  }
  .lvl-chip{ width:100%; justify-self:stretch; justify-content:space-between; }
  .lvl-meta{ flex:1 1 auto; }
  .brand-name{ font-size:.98rem; }

  .hub-grid{ grid-template-columns:1fr; }
  .loadout,
  .node-grid{ grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); }
  .armory-grid{ grid-template-columns:repeat(auto-fill,minmax(136px,1fr)); }
  .stat-grid{ grid-template-columns:repeat(auto-fit,minmax(96px,1fr)); }

  .tx{ width:96%; }
  .tx__text{ font-size:1rem; line-height:1.8; }
  .ev-banner{ top:8%; width:94%; }

  .lb-when{ display:none; }         /* the timestamp is the first thing a
                                       narrow table can afford to lose */
  .kbd-key{ min-width:26px; font-size:.68rem; }
  .kbd-key.space{ min-width:120px; }
}

@media (max-width:460px){
  body{ font-size:15px; }
  .wrap{ padding-left:.6rem; padding-right:.6rem; }

  .brand-name{ display:none; }      /* the mark alone carries the identity at
                                       this width, and the tabs need the room */
  .acct-btn span:not(.acct-dot){ max-width:9ch; overflow:hidden; text-overflow:ellipsis; }

  .lvl-xp{ display:none; }
  .lvl-meta{ min-width:0; }

  .node-grid,
  .loadout{ grid-template-columns:1fr; }
  .armory-grid{ grid-template-columns:repeat(auto-fill,minmax(118px,1fr)); }

  .loot{ gap:.5rem; }
  .loot-card{ width:calc(50% - .25rem); }

  /* Both briefing blocks are label-plus-value rows, and at this width the value
     is the part worth the room: the axis shrinks its end labels rather than its
     track, and an intel row stacks instead of squeezing its value to two
     characters. */
  .axis{ gap:.35rem; font-size:.56rem; }
  .intel__row{ flex-direction:column; align-items:flex-start; gap:.05rem; }

  .btn{ padding:.52rem .85rem; font-size:.78rem; }
  .dlg-actions .btn{ flex:1 1 auto; }

  .report__line{ font-size:.76rem; }
  .hud{ gap:.5rem; padding:.4rem .5rem; }
  .hud__stat b{ font-size:.9rem; }
  /* The hardpoint chips are the first HUD element to give up room: which
     weapon is live still reads from the lit one alone. */
  .gun-chip{ padding:.1rem .34rem; font-size:.56rem; letter-spacing:.06em; }
  .kbd{ padding:.4rem; }
  .kbd-key{ min-width:22px; padding:.34rem .06rem; font-size:.62rem; }
  .kbd-key.space{ min-width:90px; }
}


/* ============ 39. REDUCED MOTION ============
   The player asked for calm. Everything above is decoration over a page that
   works without it, so the switch can be blunt: no transitions, no animations,
   no smooth scrolling. The canvas game reads the same preference itself through
   reducedMotion() in js/util.js and drops its particles and screen shake. */
@media (prefers-reduced-motion:reduce){
  *,
  *::before,
  *::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    scroll-behavior:auto !important;
  }
  .loot-card--flip{ opacity:1; transform:none; }
  .caret{ opacity:1; }
}
