@import url('./ability-lessons.css');
/* ============================================================================
   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;

  /* The threat severity ramp, mirrored in PALETTE_FALLBACK in
     js/games/starfall.js exactly like the tokens above it. Monotonic in
     warmth AND luminance, so a band step reads in grayscale; band 0 is the
     calm neutral that never glows. --charge is RESERVED for the capacitor
     (the bottom edge and the under-word bar), --deadline for the per-word
     arrival underline, --lost for triaged words and broken streak grays. */
  --threat-0:#8FA8C8;
  --threat-1:#2E6FBF;
  --threat-2:#D9A21B;
  --threat-3:#E4572E;
  --threat-4:#FF2E4D;
  --charge:#2BB8A0;
  --deadline:#FF9F1C;
  --lost:#8A8F98;

  /* 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 scrap 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;
}

/* Scrap: the spendable currency, so it wears the currency colour. */
.scrap{
  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;
}
.scrap-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 scrap. */
.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; }
/* The build stamp. Quiet enough to ignore for as long as nothing is wrong, and
   selectable, because the first thing anybody does with it is copy it into a
   message asking why the site looks stale. */
.foot-build{
  margin-top:.7rem;
  font-family:var(--mono);
  font-size:.72rem;
  letter-spacing:.06em;
  color:var(--faint);
  opacity:.62;
}
.foot-build code{
  font-family:inherit;
  font-size:inherit;
  color:var(--muted);
  user-select:all;
}

/* 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; }

/* The slot picker (js/modes/picker.js), which is a dialog rather than a panel
   because both the Armory and the Arcade open it over whatever you were
   reading. It reuses .armory-grid and .cos for the cards themselves, so an item
   looks the same here as it does in the collection; what is added is the header
   that says what is fitted right now and the three group captions.

   The groups are ownership, never a stat, and the caption is deliberately a
   sentence rather than a bare word: "Further out" alone reads as a wall, and
   nothing on this site is one. */
/* Its own header rather than the panels' .sec-head, whose display-sized serif
   heading is right for a page and wrong for a popup: it pushed the Close button
   onto a line of its own and spent a third of the dialog before the first item. */
.pick-head{
  display:flex;
  align-items:flex-start;
  justify-content:space-between;
  gap:1rem;
  margin-bottom:.9rem;
}
.pick-head h2{ margin:.15rem 0 0; font-size:1.02rem; line-height:1.3; max-width:52ch; }
.pick-head .btn{ flex:none; }

.pick-now{
  display:flex;
  align-items:center;
  gap:.55rem;
  margin:.2rem 0 1rem;
  padding:.5rem .6rem;
  background:var(--brand-tint);
  border:1px solid var(--brand-deep);
  border-radius:var(--radius-sm);
  font-size:.85rem;
}
.pick-now img{ width:34px; height:34px; mix-blend-mode:screen; flex:none; }
.pick-now span:first-of-type{ flex:1; color:var(--muted); }
.pick-now b{ display:block; color:var(--ink); }
.pick-now .mono{ font-size:.7rem; letter-spacing:.08em; }

.pick-group{
  margin:1.2rem 0 .5rem;
  font-family:var(--mono);
  font-size:.68rem;
  letter-spacing:.15em;
  text-transform:uppercase;
  color:var(--brand);
}
.pick-group span{
  display:block;
  margin-top:.2rem;
  font-family:var(--sans);
  font-size:.78rem;
  letter-spacing:normal;
  text-transform:none;
  color:var(--faint);
}
.pick-foot{ margin:1.1rem 0 0; font-size:.8rem; color:var(--faint); max-width:64ch; }

/* One line under the header saying how to READ the grid: what PWR is, and (at a
   hardpoint) which way the axis runs. It is here rather than repeated on every
   card because a legend belongs to the list, not to the item. */
.pick-legend{
  margin:-.4rem 0 .2rem;
  font-size:.8rem;
  line-height:1.5;
  color:var(--muted);
  max-width:76ch;
}
.axis-lo{ color:var(--violet); }
.axis-hi{ color:var(--brand-2); }

/* ---- the card, at scanning width ----
   A picker card is READ IN A DOZEN. What it shows above the fold is the four
   things a choice turns on (what it is, what tier it is, what it is worth, and
   for a weapon which way it points); the dossier is one tap down. The cards are
   a little wider than the cosmetics grid they inherit from, because a stat chip
   is a phrase and a 168px column breaks every one of them onto two lines. */
/* Doubled up on purpose: .armory-grid is declared later in this file (section
   25) and would otherwise win on source order at equal specificity. */
.armory-grid.armory-grid--pick{ grid-template-columns:repeat(auto-fill,minmax(246px,1fr)); }
.pick-card{ gap:.4rem; }
.pick-card__head{
  display:flex;
  align-items:center;
  gap:.5rem;
}
.pick-card__meters{
  display:flex;
  align-items:center;
  gap:.5rem;
  min-height:20px;
}
/* The swatch shrinks from a 16/10 display panel to a recognition icon. It is
   what the eye lands on first when scanning a grid, so it stays; what it does
   not need is a third of the card's height. */
.pick-card .cos__swatch--gear{
  flex:none;
  width:46px;
  height:46px;
  aspect-ratio:auto;
}
.pick-card .cos__swatch--gear img{ max-height:none; }
.pick-card__id{ flex:1 1 auto; min-width:0; }
.pick-card__id b{ display:block; line-height:1.25; }
.pick-card__id small{ display:block; }
/* The EQUIPPED tag is emitted after everything else, so on a flex column card
   it lands under the footer rather than beside the name. That is fine on a tall
   cosmetics card and stranded on a short one. */
.pick-card.cos--equipped::after{ margin-top:-.1rem; }

/* ONE NUMBER FOR WHAT AN ITEM IS WORTH (itemPower in js/gear.js), on the same
   scale as the pad's PWR and a sector's recommendation. Amber like every other
   quantity the player banks, and it carries its own sign: an item that trades
   rating away for something the rating cannot see is an ordinary thing here,
   and a number that could only ever rise would be the ladder this catalogue is
   deliberately not. */
.pwr{
  flex:none;
  align-self:center;
  padding:.16rem .38rem;
  border-radius:999px;
  border:1px solid var(--line-strong);
  background:var(--surface-2);
  font-family:var(--mono);
  font-size:.58rem;
  letter-spacing:.1em;
  color:var(--faint);
  white-space:nowrap;
}
.pwr b{ font-size:.76rem; letter-spacing:0; font-variant-numeric:tabular-nums; }
.pwr--up{ color:var(--xp-2); border-color:rgba(242,179,61,.34); }
.pwr--up b{ color:var(--xp-2); }
.pwr--down{ color:var(--muted); }
.pwr--down b{ color:var(--bad); }
.pick-now .pwr{ align-self:center; }

/* The accuracy axis at card size: the same track and pin the loadout screen
   draws, with the end labels dropped (the legend above the grid says which way
   it runs) and the position named in a word instead. */
.qaxis{
  display:flex;
  align-items:center;
  gap:.45rem;
  flex:1 1 auto;
  min-width:0;
}
/* The track yields to the label rather than the other way round: the WORD is
   what makes a grid scannable and the pin is what makes it precise, so at a
   narrow column the pin gets shorter and the word stays whole. */
.qaxis .axis__track{ flex:1 1 30px; min-width:30px; }
.qaxis__label{
  flex:0 1 auto;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  font-family:var(--mono);
  font-size:.58rem;
  letter-spacing:.08em;
  text-transform:uppercase;
  color:var(--muted);
}
.pick-card .demand{ margin:0; font-size:.66rem; }

/* The dossier. Closed it costs one line; open it is exactly what the card used
   to print at all times. */
.pick-more{ margin-top:.1rem; }
.pick-more > summary{
  cursor:pointer;
  font-family:var(--mono);
  font-size:.62rem;
  letter-spacing:.12em;
  text-transform:uppercase;
  color:var(--brand);
  list-style:none;
}
.pick-more > summary::-webkit-details-marker{ display:none; }
.pick-more > summary::before{ content:'▸ '; }
.pick-more[open] > summary::before{ content:'▾ '; }
.pick-more > summary:hover{ color:var(--brand-2); }
.pick-more > *:not(summary){ margin-top:.35rem; }
.pick-more small{ display:block; color:var(--muted); font-family:var(--sans); font-size:.76rem; line-height:1.45; }


/* ============ 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. It MUST stay a plain inline: an
   inline-block is an atomic inline, which hands the line breaker a legal wrap
   point on either side of it, so a word near the right edge would split at
   whichever letter the caret was passing through (and re-join a keystroke
   later). A caret on a space needs no min-width here because the passage font
   is mono: every glyph, the space included, already spans the full cell. */
.caret{
  position:relative;
  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;
}

.level-baselines th:not(:first-child){ text-align:right; }

.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 scrap, 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: scrap 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.scrap 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); }

/* WHAT IS FITTED IS THE BUTTON THAT CHANGES IT. A card carrying .pick-me opens
   its slot's picker when clicked anywhere that is not one of its own controls,
   because reaching for the item itself is what everybody tries first. It is a
   container rather than a <button> (it holds buttons of its own, and a button
   inside a button is not a thing), so the pointer and the hover edge are the
   whole affordance and the card's real buttons remain the keyboard route. */
.pick-me{ cursor:pointer; }
.pick-me:hover{ border-color:var(--brand-deep); }
.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);
}
/* An area weapon carries a mark rather than a word: the chip is already two
   numbers wide and "which of my two mounts is the wide one" has to be
   answerable without reading. */
.gun-chip__area{ color:var(--violet); font-size:.8em; }

/* 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. */
/* THE WEAPON'S OWN STREAK BONUS, beside the score multiplier and deliberately
   not styled like it: they are two different mechanics and the whole point of
   showing both is that a player can tell them apart. A dash means this weapon
   has no streak stat at all, which is most of the catalogue. */
.wbonus{
  font-variant-numeric:tabular-nums;
  color:var(--violet);
}
.wbonus.max{ color:var(--brand-2); text-shadow:0 0 16px rgba(123,245,236,.5); }

.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; }

/* ---- the capacitor ----
   The arcade's fire control, and the readout the player's eye actually lives
   on: how much charge is banked, and what the thing under the lock costs. It
   goes GREEN when the two have met, because "press space now" is the one piece
   of information that must never need reading. Amber (the currency colour) is
   deliberately not used here: charge is not money, it is ammunition. */
.charge{
  position:relative;
  display:inline-flex;
  align-items:center;
  gap:.28rem;
  flex:0 0 auto;
  min-width:74px;
  padding:.16rem .45rem;
  border-radius:999px;
  background:var(--surface-3);
  border:1px solid var(--line);
  overflow:hidden;
  font-family:var(--mono);
  font-size:.72rem;
  font-variant-numeric:tabular-nums;
  color:var(--muted);
  white-space:nowrap;
}
.charge__fill{
  position:absolute;
  inset:0 auto 0 0;
  width:calc(var(--v,0) * 100%);
  background:linear-gradient(90deg,rgba(56,223,210,.16),rgba(56,223,210,.36));
  transition:width .1s linear;
  pointer-events:none;
}
.charge b, .charge span{ position:relative; }
.charge b{ color:var(--brand-2); font-weight:700; }
.charge span{ color:var(--faint); font-size:.62rem; }
.charge.ready{
  border-color:var(--good);
  box-shadow:0 0 14px rgba(91,229,154,.34);
}
.charge.ready b{ color:var(--good); }
.charge.ready .charge__fill{ background:linear-gradient(90deg,rgba(91,229,154,.18),rgba(91,229,154,.4)); }
/* An area weapon spends the WHOLE meter, so its readout says so with a dashed
   edge rather than with a word: the HUD has no room for a sentence. */
.charge.area{ border-style:dashed; }
/* A discharge the meter could not pay for. It is refused, never punished, so
   this is a shake and nothing else. */
.charge.short{ animation:btq-short .3s ease-out 1; }
@keyframes btq-short{
  0%,100%{ transform:translateX(0); }
  25%{ transform:translateX(-3px); }
  75%{ transform:translateX(3px); }
}

/* Each wreck pays a readable receipt. Amber anchors the reward; the two
   typing measurements stay white so accuracy is not communicated by color. */
.kill-pop{
  position:absolute;
  left:0;
  top:0;
  font-family:var(--mono);
  width:168px;
  box-sizing:border-box;
  padding:7px 10px 6px;
  border:1px solid rgba(114,210,231,.3);
  border-radius:7px;
  background:rgba(8,15,26,.94);
  box-shadow:0 3px 12px rgba(0,0,0,.25);
  color:#eef8ff;
  line-height:1.1;
  pointer-events:none;
  will-change:transform,opacity;
}
.kill-pop__stats{display:flex;justify-content:space-between;gap:12px;}
.kill-pop__stats>span{display:flex;flex-direction:column;gap:2px;}
.kill-pop__stats>span:last-child{text-align:right;}
.kill-pop__stats b{font-size:22px;font-weight:750;letter-spacing:-1px;}
.kill-pop__stats small{font-size:9px;letter-spacing:.7px;color:#bfd0e0;}
.kill-pop__salvage{display:flex;align-items:baseline;gap:6px;margin-top:5px;color:#ffd07a;}
.kill-pop__salvage b{font-size:26px;font-weight:800;letter-spacing:-1px;}
.kill-pop__salvage span{font-size:10px;letter-spacing:1px;}
.kill-pop__salvage i{margin-left:auto;font-style:normal;font-size:17px;}
.kill-pop__note{display:block;color:#bfd0e0;font-size:9px;margin-top:3px;}
.scrap-run{ color:var(--xp-2); }

/* ---- the launch briefing ----
   The four rules of the game, on the one screen where nobody is under time
   pressure. Sized to be READ rather than glanced at: this is the only place a
   player who has never seen the arcade finds out that typing a word is what
   destroys the thing carrying it, and a hint nobody can read is a hint that
   was not given. */
.sf-brief{
  list-style:none;
  margin:.7rem 0 0;
  padding:0;
  text-align:left;
  display:grid;
  gap:.4rem;
  font-size:.82rem;
  line-height:1.45;
  color:var(--muted);
}
.sf-brief li{
  padding-left:1.1rem;
  position:relative;
}
.sf-brief li::before{
  content:"\203A";
  position:absolute;
  left:.2rem;
  top:-.05em;
  color:var(--brand-2);
  font-weight:700;
}
.sf-brief b{ color:var(--ink); font-weight:600; }

/* ---- the arming strip ----
   The fire control, over the base, one pip per unit the next discharge costs.
   It replaced two numbers with a slash between them in the HUD along the top,
   which asked the player to compare a moving number with a still one, under
   time pressure, to answer a yes or no question, twelve centimetres away from
   where their eyes were. Pips are countable at a glance and need no legend:
   they light up as you type, and when the last one lights the strip goes green
   and names the key. */
.arm{
  position:absolute;
  left:50%;
  bottom:calc(clamp(.5rem,3.5vh,1.3rem) + 2.5rem);
  transform:translateX(-50%);
  z-index:4;
  display:flex;
  align-items:center;
  gap:.5rem;
  max-width:min(92%,540px);
  padding:.22rem .55rem;
  border-radius:999px;
  background:rgba(8,11,20,.7);
  border:1px solid var(--line);
  backdrop-filter:blur(6px);
  -webkit-backdrop-filter:blur(6px);
  pointer-events:none;      /* a readout over a live playfield */
}
.arm__pips{ display:flex; gap:3px; align-items:center; }
.arm__pips i{
  display:block;
  width:6px;
  height:13px;
  border-radius:2px;
  background:var(--surface-3);
  box-shadow:inset 0 0 0 1px var(--line);
}
.arm__pips i.on{
  background:var(--brand-2);
  box-shadow:0 0 7px rgba(56,223,210,.55);
}
.arm__key{
  font-family:var(--mono);
  font-size:.62rem;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--faint);
  white-space:nowrap;
}
.arm__key:empty{ display:none; }
/* ARMED. The one state that has to read from the corner of an eye, so it is the
   only thing on the playfield that changes colour and glows at once. */
.arm.ready{
  border-color:var(--good);
  box-shadow:0 0 16px rgba(91,229,154,.4);
}
.arm.ready .arm__pips i{
  background:var(--good);
  box-shadow:0 0 8px rgba(91,229,154,.7);
}
.arm.ready .arm__key{ color:var(--good); font-weight:700; }
.arm.short{ animation:btq-short .3s ease-out 1; }

/* ---- the typing box ----
   Over the base, because that is where the player is looking when the field is
   busy. It shows exactly what has been typed, in order, with anything mistyped
   and not yet taken back in red at the end, and nothing else: the word itself
   is already on screen attached to the craft carrying it. When there is nothing
   typed and nothing to correct it is not there at all. */
.typebox{
  position:absolute;
  left:50%;
  bottom:clamp(.5rem,3.5vh,1.3rem);
  transform:translateX(-50%);
  z-index:4;
  display:flex;
  align-items:center;
  gap:.5rem;
  max-width:min(92%,540px);
  padding:.3rem .6rem;
  border-radius:999px;
  background:rgba(8,11,20,.82);
  border:1px solid var(--line-strong);
  box-shadow:var(--shadow-2);
  backdrop-filter:blur(6px);
  -webkit-backdrop-filter:blur(6px);
  font-family:var(--mono);
  font-size:clamp(.95rem,2.4vw,1.25rem);
  letter-spacing:.06em;
  white-space:nowrap;
  overflow:hidden;
  pointer-events:none;      /* it is a readout over a live playfield */
}
/* The guard, and this box is the reason it is spelled out beside every rule
   that sets display: without it the class above beats [hidden] on source order
   and the box never goes away, so the last thing painted into it (the word
   minus the character that just killed the craft) sits over the base until the
   next keystroke. A readout that keeps reporting a target that no longer
   exists is worse than no readout. */
.typebox[hidden]{ display:none; }
.typebox{ display:grid; grid-template-columns:minmax(1ch,1fr) auto; grid-template-areas:'word fix' 'meter meter'; width:min(92%,540px); gap:.15rem .5rem; border-radius:var(--radius); }
.typebox__word{ grid-area:word; min-width:1ch; white-space:pre; overflow-x:auto; scrollbar-width:none; }
.typebox__word::-webkit-scrollbar{ display:none; }
.typebox__meter{ grid-area:meter; }
.typebox__fix{ grid-area:fix; }
.typebox__word::after{ content:''; border-right:2px solid currentColor; margin-left:1px; }
.tx .typebox{ position:relative; inset:auto; transform:none; margin:.65rem auto 0; max-width:100%; width:min(100%,540px); }
.tx .typebox__meter{ white-space:normal; }
.typebox__done{ color:var(--brand); }
.typebox__rest{ color:var(--muted); }
.typebox__rest.dim{ opacity:.45; }
/* The mistake, and it is the loudest thing in the box on purpose: it is the
   only part of the game that will not proceed until the player acts. */
.typebox__bad{
  color:var(--brand-ink);
  background:var(--bad);
  border-radius:3px;
  padding:0 .1em;
}
.typebox.bad{ border-color:var(--bad); box-shadow:0 0 18px rgba(255,107,107,.35); }
.typebox__meter{
  flex-shrink:0;
  font-size:.7em;
  color:var(--faint);
  font-variant-numeric:tabular-nums;
}
.typebox__meter.ready{ color:var(--good); }
/* The one control in here, and it exists for touch: a phone reports nothing for
   Backspace, so without it a mistype on a phone could not be cleared at all,
   and a word could never be backed out of, which is what releases the target.
   It is offered whenever the box has anything in it and it is QUIET until there
   is red to take back: an alarm-coloured button standing next to a perfectly
   good word would read as an error the player has not made. */
.typebox__fix{
  flex-shrink:0;
  pointer-events:auto;
  cursor:pointer;
  padding:.05rem .45rem;
  border-radius:999px;
  border:1px solid var(--line-strong);
  background:transparent;
  color:var(--muted);
  font-family:inherit;
  font-size:.8em;
  line-height:1.5;
}
.typebox__fix:hover{ background:var(--surface-3); color:var(--ink); }
.typebox.bad .typebox__fix{
  border-color:var(--bad);
  background:rgba(255,107,107,.12);
  color:var(--bad);
}
.typebox.bad .typebox__fix:hover{ background:rgba(255,107,107,.24); color:var(--bad); }

/* The boss briefing, which is read once and then gets out of the way of the
   fight it describes. */
.boss-brief{ animation:btq-ev-in .3s ease both; }

/* ---- the two numbers a threat level is priced in ----
   Power is what the gear covers; pace is what the hands would have to cover
   instead. They are the same recommendation said two ways, and the second one
   is the one a player can check against themselves, so it wears the fit colours
   the loadout advice already uses. */
.sector__pace{
  display:block;
  margin-top:.15rem;
  font-family:var(--mono);
  font-size:.66rem;
  letter-spacing:.06em;
  text-transform:uppercase;
  /* Colour only. `.fit--*` carries a plate elsewhere, and a full width amber
     block inside a card reads as a warning about the card rather than as a
     number on it. */
  background:none;
  border:0;
  padding:0;
}

/* Where a weapon starts being worth carrying, in accuracy. See accuracyDemand()
   in js/gear.js: it is the four numbers of the bargain (clean, fouled, meter
   rate, stall) collapsed into the one question a player can answer about
   themselves. */
.demand{
  display:block;
  margin:.35rem 0 .1rem;
  font-family:var(--mono);
  font-size:.7rem;
  letter-spacing:.05em;
  color:var(--muted);
}
.demand--above{ color:var(--brand-2); }
.demand--below{ color:var(--good); }
.demand--crowd{ color:var(--violet); }
.demand--any{ color:var(--xp-2); }
.demand--even, .demand--never{ color:var(--faint); }

/* An area discharge, wherever a stat line lists one. */
.dmg-area{ color:var(--violet); }

/* ---- what an item actually does ----
   A stat row is unreadable to somebody who has not learned the mechanics, and
   learning the mechanics is what a new player is doing. So every item card
   carries the same information as plain sentences, derived from the same deltas
   (explainGear in js/gear.js). Quiet, because it is reference rather than
   headline, but present rather than hidden behind a hover: a hover does not
   exist on a phone. */
/* Which item the sentences under it belong to. A weapon card carries two lists
   (the gun and whatever is bolted to it) and without this they read as one. */
.plain-who{
  margin:.5rem 0 0;
  font-family:var(--mono);
  font-size:.62rem;
  letter-spacing:.12em;
  text-transform:uppercase;
  color:var(--faint);
}
.plain-what{
  list-style:none;
  margin:.4rem 0 .1rem;
  padding:0;
  display:flex;
  flex-direction:column;
  gap:.22rem;
  font-size:.74rem;
  line-height:1.45;
  color:var(--muted);
}
.plain-what li{
  position:relative;
  padding-left:.8rem;
}
.plain-what li::before{
  content:'';
  position:absolute;
  left:0;
  top:.5em;
  width:4px;
  height:4px;
  border-radius:50%;
  background:var(--line-strong);
}

/* 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;
}

/* What the banner leaves behind: the live encounter as one HUD chip, with its
   clock draining along the bottom edge. It carries everything the player still
   needs (what is running, what it pays, how long is left) and none of the
   playfield, which is the whole reason the banner dismisses itself. */
.ev-live{
  position:relative;
  display:inline-flex;
  align-items:center;
  gap:.35rem;
  padding:.16rem .5rem .3rem;
  border-radius:999px;
  background:rgba(139,123,255,.12);
  border:1px solid rgba(139,123,255,.5);
  color:var(--violet);
  font-family:var(--mono);
  font-size:.66rem;
  letter-spacing:.08em;
  text-transform:uppercase;
  white-space:nowrap;
  overflow:hidden;
}
.ev-live::after{
  content:'';
  position:absolute;
  left:0;
  bottom:0;
  height:2px;
  width:calc(var(--t,1) * 100%);
  background:linear-gradient(90deg,var(--violet),#C8BFFF);
  transition:width .2s linear;
}
.ev-live .ev-mult{ font-size:.66rem; padding:0 .3rem; }

/* ---- 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 boss arrival, and the reward review ----
   Two PROTECTED cards, and they look like it: they sit over the field, they own
   the keyboard, and neither is dismissed by typing. The arrival is read while
   nothing is moving; the review is read for as long as the player likes, at a
   cost of exactly nothing, because the simulation is frozen behind it. */
.boss-intro,
.boss-win{
  position:absolute;
  left:50%;
  top:50%;
  transform:translate(-50%,-50%);
  width:min(94%,560px);
  max-height:92%;
  overflow:auto;
  padding:1.1rem 1.2rem;
  text-align:center;
  background:rgba(8,11,20,.94);
  border:1px solid var(--line-strong);
  border-radius:var(--radius);
  box-shadow:var(--shadow-3);
  backdrop-filter:blur(10px);
  -webkit-backdrop-filter:blur(10px);
  pointer-events:auto;
}
.boss-intro__head,
.boss-win .boss-intro__head{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:.8rem;
  margin:.5rem 0 .6rem;
  text-align:left;
}
.boss-intro__head h2{ margin:0; font-size:1.4rem; }
.boss-intro__head p{ margin:0; font-size:.8rem; }
.boss-intro__head img{ border-radius:var(--radius-sm); }
.boss-intro__brief{ margin:.2rem 0 .7rem; color:var(--muted); font-size:.85rem; }
/* WHERE THE TYPING WILL HAPPEN, said before it starts. A player should never
   have to discover a new place on the screen while a boss is descending. */
.boss-intro__where{
  margin:.7rem 0 0;
  padding:.55rem .7rem;
  border:1px dashed var(--line-strong);
  border-radius:var(--radius-sm);
  color:var(--muted);
  font-size:.78rem;
}
.boss-intro__hint{ margin:.5rem 0 0; font-size:.7rem; }
/* The last boss reward, reachable from the result screen. Closed by default:
   it is a place to look something up rather than something to read. */
.report__last{
  margin:.8rem 0 0;
  padding:.4rem .6rem;
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  text-align:left;
  font-size:.84rem;
}
.report__last summary{ cursor:pointer; color:var(--muted); }
.boss-win__loot{
  margin:.7rem 0 0;
  padding:0;
  list-style:none;
  text-align:left;
  font-size:.84rem;
}
.boss-win__loot li{
  padding:.3rem .1rem;
  border-bottom:1px solid var(--line);
}
.boss-win__loot li:last-child{ border-bottom:0; }

/* THE PRE-FLIGHT DIFFICULTY SUMMARY, and it is the first thing on the briefing
   because everything under it (the difficulty, the sector, the checkpoint, the
   loadout) is a way of changing it. Small and readable rather than a headline:
   it is a promise about what is coming, not an advertisement. */
.ops-summary{
  margin:.6rem 0 .8rem;
  padding:.6rem .7rem;
  border:1px solid var(--line-strong);
  border-radius:var(--radius-sm);
  background:rgba(8,11,20,.5);
}
.ops-summary__line{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:.6rem;
  padding:.18rem 0;
  font-size:.82rem;
}
.ops-summary__note{
  margin:.45rem 0 0;
  color:var(--faint);
  font-size:.74rem;
  line-height:1.5;
}

/* ---- 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. */
/* SUBTITLES, NOT A PANEL OVER THE GAME. A transmission is live for a minute or
   more and the escorts keep descending behind it, so a card in the middle of
   the field hid the fight it was part of: the player read a paragraph while
   things they could not see arrived at their base. It sits along the bottom
   instead, the way subtitles do, fading upward into the playfield rather than
   cutting a hard edge across it, and the field above it stays watchable. */
.tx{
  position:absolute;
  left:0;
  right:0;
  bottom:0;
  z-index:5;
  padding:clamp(1.6rem,5vh,2.6rem) clamp(.7rem,3vw,1.4rem) clamp(.6rem,2vh,1rem);
  background:linear-gradient(180deg,rgba(8,11,20,0),rgba(8,11,20,.82) 26%,rgba(8,11,20,.95));
  text-align:left;
}
.tx__text{
  max-width:62ch;
  margin:0 auto;
  font-family:var(--mono);
  font-size:clamp(.94rem,1.7vw,1.12rem);
  --tx-lh:1.7;
  line-height:var(--tx-lh);
  letter-spacing:.01em;
  /* ONE WHITESPACE POLICY, AND IT DOES NOT CHANGE AS THE CURSOR MOVES.
     `pre-wrap` is what makes a literal U+0020 visible and countable, which is
     what let js/games/transmission-view.js stop substituting a NBSP for the
     space under the cursor. Word breaking stays NORMAL so the paragraph wraps
     between words like prose; `overflow-wrap` is the emergency valve for a
     token genuinely wider than the measure, which `word-break:break-word`
     (an alias for breaking anywhere) was applying to every word. */
  white-space:pre-wrap;
  word-break:normal;
  overflow-wrap:break-word;
  /* A ligature spans two characters, so a boundary between a typed character
     and an untyped one would change the glyph advance of both. Off here. */
  font-variant-ligatures:none;
  font-kerning:none;
  /* The passage viewport keeps its height through a phase, so a short reissue
     does not jump the health rail and the offline note up the screen. */
  min-height:calc(3 * var(--tx-lh) * 1em);
  color:var(--faint);
  text-shadow:0 1px 4px rgba(8,11,20,.9);
}
/* ---- one character, four states, ONE BOX ----
   Every rule below changes colour, background or an inset shadow and NOTHING
   else. No display, no width, no padding, no border, no letter-spacing, no
   weight, no transform, and never a different glyph. That is the whole of the
   layout fix: advancing the cursor may not move a single character on screen.
   See the header of js/games/transmission-view.js for why an inline-block and
   a min-width were two separate ways of breaking it. */
.tx__ch{
  color:var(--muted);
  border-radius:2px;
}
/* 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__ch.is-done{ color:var(--brand); }
.tx__ch[data-countdown]{position:relative;box-shadow:inset 0 -3px var(--xp)}
.tx__ch[data-countdown]::after{content:attr(data-countdown);position:absolute;bottom:1.3em;left:0;z-index:4;white-space:nowrap;background:var(--xp);color:#101321;padding:2px 6px;border-radius:4px;font:700 12px/1.2 system-ui;pointer-events:none}
.tx__ch[data-countdown][data-countdown-right]::after{left:auto;right:0}
.tx__ch.is-extra{ color:var(--faint); opacity:.65; }
.tx__ch.is-endword{ background:color-mix(in srgb,var(--brand) 13%,transparent);box-shadow:inset 0 -2px var(--brand); }
.tx__goal:has(.tx__goal-text){ font-weight:600; }
.tx--finishing .tx__goal-text{color:var(--xp)}
.tx--finishing .tx__ch.is-endword{background:color-mix(in srgb,var(--xp) 15%,transparent);box-shadow:inset 0 -2px var(--xp)}
#sfTx.is-handoff .tx__text{ opacity:.55; }
.sf-handoff{position:absolute;z-index:12;left:50%;bottom:1rem;transform:translateX(-50%);width:max-content;max-width:calc(100% - 2rem);padding:.7rem 1rem;border:1px solid var(--xp);border-radius:10px;background:var(--surface);box-shadow:0 8px 30px #0008;text-align:center;pointer-events:none}
.sf-handoff[hidden]{display:none}
.sf-handoff strong,.sf-handoff span,.sf-handoff small{display:block}
.sf-handoff strong{color:var(--xp);font-size:1rem}
.sf-handoff span{font-size:.78rem;margin:.2rem 0}
.sf-handoff small{color:var(--muted);font-variant-numeric:tabular-nums}
.sf-handoff.is-ready{border-color:var(--brand)}
.sf-handoff.is-ready strong{color:var(--brand)}
.tx__goal{ display:flex; justify-content:space-between; gap:.5rem; white-space:nowrap; margin:.35rem 0; min-height:1.4em; color:var(--brand); font-size:.72rem; line-height:1.4; }
/* Where you are. It inverts rather than covers, because on a paragraph the
   current character has to stay readable. The glow is a box-shadow, which
   paints outside the box without enlarging it. */
.tx__ch.is-cur{
  background:var(--brand);
  color:var(--brand-ink);
  box-shadow:0 0 14px rgba(56,223,210,.5);
}
/* Restore the pre-September space cue: the same plain cyan block as any
   other cursor character, without an added open-box symbol. Keep the literal
   space and inline box so line wrapping stays stable. */
.tx__ch--space.is-cur{ background-image:none; }
.tx__ch.is-bad{
  color:var(--brand-ink);
  background:var(--hazard);
}
/* The command word's own card. The transmission is subtitles along the bottom;
   a shield is one short token and gets a beat of its own in the middle. */
.tx__card{
  position:absolute;
  left:50%;
  top:50%;
  transform:translate(-50%,-50%);
  width:min(92%,520px);
  padding:1rem 1.1rem;
  text-align:center;
  background:rgba(8,11,20,.9);
  border:1px solid var(--line-strong);
  border-radius:var(--radius);
  box-shadow:var(--shadow-3);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
}
/* The command card centers against the full playfield, not the subtitle
   wrapper's padding-height box. Prose keeps its bottom alignment. */
.tx.tx--shield{ inset:0; width:auto; padding:0; background:none; }
.tx--shield .tx-shield{ display:inline-block; font-size:clamp(1.1rem,3.8vw,2rem); letter-spacing:.12em; white-space:nowrap; }
.tx--shield .tx-shield::before{ display:none; }
.tx__shieldrow{ margin:.7rem 0; }

.tx__note{ margin:0; font-size:.84rem; }
.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;
}
/* Why the field went quiet. While a transmission is live NOTHING descending can
   be typed, so both renderers dim those words, and this line says what is
   clearing them instead. */
.tx__offline{
  margin:.7rem 0 0;
  font-family:var(--mono);
  font-size:.68rem;
  letter-spacing:.04em;
  line-height:1.5;
  color:var(--faint);
  text-align:center;
}
.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;
}

/* Face-on navigation keeps every destination readable over the hologram. */
.ops-table{
  position:sticky;
  top:calc(var(--head-h) + .75rem);
  align-self:start;
  min-width:0;
  padding:1rem;
  border:1px solid var(--line-strong);
  border-radius:var(--radius);
  background:var(--bg);
  box-shadow:var(--shadow-2);
}
.ops-chart-head{ display:flex; justify-content:space-between; gap:.75rem; align-items:center; margin-bottom:1rem; }
.ops-chart-head .label{ color:var(--brand-2); }
.ops-chart-head > span:last-child{ color:var(--muted); font-size:.75rem; }
.ops-disc{ position:relative; width:100%; isolation:isolate; }
.ops-disc::before{
  content:''; position:absolute; inset:-.5rem; z-index:-1;
  background:repeating-linear-gradient(0deg,var(--brand-tint) 0 1px,transparent 1px 38px),
    repeating-linear-gradient(90deg,var(--brand-tint) 0 1px,transparent 1px 38px);
}
.ops-galaxy,.ops-holo{ position:absolute; inset:0; width:100%; height:100%; pointer-events:none; z-index:-1; }
.ops-galaxy{ object-fit:contain; opacity:.4; }
.ops-holo{ opacity:.4; }
.ops-sector-grid{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1.4rem 1rem; padding:.4rem; }
.ops-beacon{
  position:relative; display:flex; flex-direction:column; align-items:stretch; gap:.45rem;
  min-width:0; padding:.9rem; text-align:left;
  background:var(--surface); border:1px solid var(--line-strong);
  border-radius:var(--radius-sm); color:var(--ink); cursor:pointer;
  box-shadow:var(--shadow-1);
}
.ops-sector-top{ display:flex; align-items:center; justify-content:space-between; gap:.4rem; }
.ops-sector-top i{
  display:grid; place-items:center; width:2rem; height:2rem; flex-shrink:0;
  border:1px solid var(--brand-deep); border-radius:50%; background:var(--bg);
  color:var(--brand-2); font:normal .8rem var(--mono);
}
.ops-sector-top small{ color:var(--muted); font-size:.75rem; }
.ops-sector-art{ width:100%; height:72px; object-fit:cover; border-radius:4px; opacity:.85; }
.ops-beacon strong{ font:600 1.08rem var(--sans); line-height:1.25; }
.ops-sector-guide{ color:var(--muted); font-size:.8rem; line-height:1.5; min-height:3em; }
.ops-sector-meta{ color:var(--muted); font: .72rem var(--mono); padding-top:.45rem; border-top:1px solid var(--line-strong); }
.ops-beacon:hover{ border-color:var(--brand); background:var(--surface-2); }
.ops-beacon:focus-visible{ outline:3px solid var(--ink); outline-offset:4px; }
.ops-beacon--on{ border:2px solid var(--brand); padding:calc(.9rem - 1px); background:var(--surface-2); }
.ops-beacon--on .ops-sector-top small{ color:var(--brand-2); font-weight:700; }
.ops-beacon--on .ops-sector-top i{ background:var(--brand); color:var(--brand-ink); }
.ops-beacon--locked{border-style:dashed;cursor:not-allowed;}
.ops-beacon--locked .ops-sector-art{filter:grayscale(.85);opacity:.42;}
.ops-beacon--locked .ops-sector-top small,.ops-beacon--locked .ops-sector-meta{color:var(--xp-2);}
.ops-beacon--locked:hover{border-color:var(--line-strong);background:var(--surface);}
.ops-chart-note{ color:var(--muted); font-size:.8rem; line-height:1.6; margin:1rem 0 0; }
.ops-chart-note b{ color:var(--ink); }

/* One keyboard-accessible scale replaces the tier button wall. */
.ops-tier-scale{ padding:1rem; border:1px solid var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); }
.ops-tier-scale label{ display:flex; justify-content:space-between; gap:1rem; align-items:center; color:var(--ink); font-weight:600; }
.ops-tier-scale output{ color:var(--brand-2); font:600 1.1rem var(--mono); }
.ops-tier-scale input{ display:block; width:100%; margin:.7rem 0 .2rem; min-height:32px; cursor:pointer; accent-color:var(--brand); }
.ops-tier-scale input:focus-visible{ outline:2px solid var(--ink); outline-offset:4px; border-radius:var(--radius-sm); }
.ops-tier-ends{ display:flex; justify-content:space-between; color:var(--muted); font-size:.75rem; }
.ops-tier-scale p{ color:var(--muted); font-size:.8rem; line-height:1.5; margin:.7rem 0 0; }
.ops-tier-scale p b{ color:var(--ink); }

/* 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; }

/* The opening-wave stepper, sitting on the end of the preset row. The number
   between the keys is the whole readout, so it is mono and tabular: the row
   reflows if 9 and 40 are different widths. */
.ops-wave{ display:inline-flex; align-items:center; gap:.2rem; }
.ops-wave__num{
  min-width:2.4em; text-align:center;
  font-variant-numeric:tabular-nums; color:var(--ink);
}

.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); }

/* Both of these are BUTTONS that open the slot picker over this screen, which
   is why they carry a full reset: wanting a different weapon while reading the
   boss rota that made you want it is the whole point of putting the loadout on
   the pre-flight, and walking to another tab to do it loses the screen. */
.ops-guns{ display:flex; flex-direction:column; gap:.4rem; }
.ops-gun{
  display:flex;
  align-items:center;
  gap:.55rem;
  width:100%;
  padding:.35rem .5rem;
  background:rgba(8,11,20,.55);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  font:inherit;
  color:inherit;
  text-align:left;
  cursor:pointer;
  transition:border-color .16s ease;
}
.ops-gun:hover{ border-color:var(--brand-deep); }
.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;
  cursor:pointer;
  transition:border-color .16s ease;
}
.ops-kit:hover{ border-color:var(--brand-deep); }
.ops-kit img{ width:100%; height:100%; mix-blend-mode:screen; display:block; }

.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; }
  .ops-disc{ width:100%; }
}
@media (max-width:460px){
  .ops-scene{ padding:.7rem; }
  .ops-sector-grid{ gap:.6rem; padding:0; }
  .ops-beacon{ padding:.6rem; }
  .ops-beacon strong{ font-size:.95rem; }
  .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 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; }

/* ---- the weapon bay ----
   The two hardpoints, across the base of the hangar, split by a slash. They are
   deliberately the largest controls on the screen and deliberately NOT in the
   flank columns with the vents and the sensors: a weapon is the only slot with
   a whole second axis behind it and the only one a boss rota is a counter-pick
   against, so it gets the room and the reading order.

   The slash is the point of the layout rather than decoration. Two equal cards
   in a row read as a list of two guns you are carrying; PRIMARY / SECONDARY
   reads as one live mount and one you can Tab to, which is what they are. */
/* The streak readout on a weapon card: the landmarks this build reaches and at
   what combo, plus the control that plays them. Quiet, because it is reference
   rather than a headline. */
.streakline{
  margin:.5rem 0 .3rem;
  padding:.45rem .55rem;
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
}
.streakline .pill{ font-size:.66rem; }
.streakline .btn{ margin-top:.35rem; }
.wbay{
  position:relative;
  display:grid;
  grid-template-columns:1fr auto 1fr;
  align-items:stretch;
  gap:.6rem;
  padding:0 1rem 1rem;
}
/* Mono rather than the serif the rest of the bay is set in: a serif solidus is
   a hairline, and at this size against a near-black plate it simply was not
   there. The stroke has to read as a divider from across the screen. */
.wbay__slash{
  align-self:center;
  font-family:var(--mono);
  font-size:2.4rem;
  font-weight:700;
  line-height:1;
  color:var(--muted);
  opacity:.55;
  user-select:none;
}
.wbay__slot{
  display:flex;
  align-items:center;
  gap:.7rem;
  padding:.6rem .75rem;
  text-align:left;
  background:rgba(8,11,20,.8);
  border:1px solid var(--line-strong);
  border-radius:var(--radius-sm);
  color:inherit;
  cursor:pointer;
  backdrop-filter:blur(3px);
  -webkit-backdrop-filter:blur(3px);
  transition:border-color .16s ease, transform .16s ease, box-shadow .16s ease;
}
.wbay__slot:hover{ border-color:var(--brand-deep); transform:translateY(-1px); }
/* The live mount, which is what a run starts holding. The other is drawn quiet
   on purpose: two cards stated with equal weight read as though their numbers
   added together, which is exactly what the second hardpoint does not do. */
.wbay__slot--live{ border-color:var(--brand-deep); box-shadow:0 0 20px rgba(56,223,210,.1); }
.wbay__slot--empty{ border-style:dashed; }
.wbay__ico{
  flex:none;
  width:72px;
  height:72px;
  object-fit:contain;
  mix-blend-mode:screen;
}
.wbay__body{ display:flex; flex-direction:column; gap:.12rem; min-width:0; flex:1 1 auto; }
.wbay__body .label{ margin:0; }
.wbay__body b{ font-size:1rem; font-weight:600; line-height:1.25; }
.wbay__note{ font-size:.76rem; color:var(--faint); line-height:1.4; }
.wbay__meters{ display:flex; align-items:center; gap:.45rem; margin-top:.18rem; }
.wbay__meters .axis__track{ flex:1 1 auto; min-width:40px; max-width:120px; }
.wbay__axis{
  flex:none;
  font-family:var(--mono);
  font-size:.6rem;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--muted);
}
.wbay__slot .demand{ margin:.15rem 0 0; font-size:.66rem; }

@media (max-width:720px){
  .wbay{ grid-template-columns:1fr; }
  .wbay__slash{ justify-self:center; font-size:1.4rem; }
  .wbay__ico{ width:56px; height:56px; }
}

@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; }

/* A vendor's card carries a SHOPFRONT: the vendor standing in their own place,
   drawn full frame with its own dark backdrop rather than cut out on nothing.
   So it is shown at full strength. The 8% it used to be knocked back by was
   there to stop a floating figure competing with the panel, and a scene that
   brought its own near-black room does not need help sitting down. Cropped 5:4
   from the 4:3 the pack ships, biased up the frame, because the face is the
   part of a shopfront that has to survive the crop. */
.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;
}

.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; --tx-lh: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; }
  /* The running scrap total is the first readout to go: every kill already
     pops its own number over the wreck, and on a phone the strip is competing
     with the playfield for the same pixels. The capacitor stays, because it is
     the one number the next keystroke depends on. */
  .hud__stat:has(.scrap-run){ display:none; }
  /* A transmission is subtitles, and on a short screen subtitles have to be
     subtitles rather than a wall: tighter and smaller, so the field above them
     is still a field. */
  .tx{ padding-top:clamp(.9rem,3vh,1.6rem); }
  .tx__text{ font-size:.86rem; --tx-lh:1.55; }
  .kbd{ padding:.4rem; }
  .kbd-key{ min-width:22px; padding:.34rem .06rem; font-size:.62rem; }
  .kbd-key.space{ min-width:90px; }
}


/* ============ 38b. THE FITTED CRAFT ============
   js/shipview.js composites a hull with one overlay per mounted item, pinned
   to anchors that come out of the art pack's own manifest. Everything here is
   geometry: the container holds the hull's aspect ratio, every overlay is
   positioned as a PERCENTAGE of it, and nothing needs a media query because a
   percentage is already responsive. The same markup is the 420px hero on the
   Hangar pad and the 150px thumbnail on the pre-flight card.

   A pack with no mounted overlays never emits this markup at all (shipview
   answers with the bare hull image the panels drew before), so these rules are
   inert under rendered, pixel and vector rather than wrong. */
.rigship{
  position:relative;
  display:block;
  width:100%;
  margin-inline:auto;
}
.rigship__hull{
  display:block;
  width:100%;
  height:auto;
}
.rigship__fit{
  position:absolute;
  /* Sized by HEIGHT, which is the part's length along the ship, and left to
     find its own width from the art's aspect. js/shipview.js writes both, and
     the reason is the scale contract: a weapon's size is a fraction of the
     hull's LENGTH, so length is the dimension that must be set. */
  width:auto;
  pointer-events:none;
}
/* ---- the craft as a control ----
   Every part on the ship is a hit target for its own slot, and the hull under
   them is one for the hull. The rings are drawn faintly AT REST rather than
   only on hover, because an invisible affordance is one nobody finds: what they
   should read as is service points on a craft in a bay, which is what the
   picture is already about. `pointer-events` is set explicitly because the
   overlays themselves are inert (see .rigship__fit) and these must not be. */
.rigship__spot{
  position:absolute;
  aspect-ratio:1;
  min-width:34px;
  min-height:34px;
  padding:0;
  transform:translate(-50%,-50%);
  border:1px dashed rgba(56,223,210,.24);
  border-radius:50%;
  background:transparent;
  color:transparent;
  cursor:pointer;
  pointer-events:auto;
  transition:border-color .16s ease, background-color .16s ease, box-shadow .16s ease;
}
.rigship__spot:hover,
.rigship__spot:focus-visible{
  border-color:var(--brand);
  border-style:solid;
  background:rgba(56,223,210,.12);
  box-shadow:0 0 16px rgba(56,223,210,.28);
  outline:none;
}
/* The hull's own target is a box round the whole craft rather than a ring, and
   it stays invisible until asked for: a rectangle drawn permanently round the
   hero of the screen would be a frame, not an affordance. */
.rigship__spot--hull{
  aspect-ratio:auto;
  border-radius:var(--radius-sm);
  border-color:transparent;
  transform:none;
}
.rigship__spot--hull:hover,
.rigship__spot--hull:focus-visible{
  border-color:var(--brand-deep);
  border-style:dashed;
  background:rgba(56,223,210,.05);
  box-shadow:none;
}
/* The label a spot carries for a screen reader, kept out of the layout without
   display:none, which would take it off the accessibility tree as well. */
.rigship__spot span{
  position:absolute;
  width:1px;
  height:1px;
  overflow:hidden;
  clip-path:inset(50%);
  white-space:nowrap;
}

/* The pad's craft is the hero of the Hangar, so it gets the room. */
.rigship--hangar{ width:min(100%,420px); filter:drop-shadow(0 12px 18px rgba(0,0,0,.72)); }
/* The pre-flight's is a thumbnail beside the numbers: big enough to tell a
   Railgun from a Scatter Array, small enough that the cards stay the content. */
.ops-rig{ display:flex; justify-content:center; margin:.1rem 0 .55rem; }
.rigship--ops{ width:min(100%,168px); }
@media (max-width:820px){ .rigship--ops{ width:min(100%,132px); } }


/* ============ 39. THE LAYERED ART STYLES ============
   Everything above is written for the RENDERED pack, whose icons are glowing
   objects on pure black composited additively (mix-blend-mode:screen). BOTH of
   the layered packs are built the other way: real alpha cutouts. So the first
   rule below is shared by ink and pixel, and only the second (nearest
   neighbour) is a pixel rule, because ink is smooth art scaled DOWN and a
   pixelated resample would eat its ink line.

   The attribute is written on the root element by applyArtStyle() in
   js/app.js, so a style is one selector away everywhere and no panel module
   has to know which pack is running. */
:root[data-art="ink"] .pick-now img,
:root[data-art="ink"] .ops-gun img,
:root[data-art="ink"] .ops-kit img,
:root[data-art="ink"] .hg-node img,
:root[data-art="ink"] .gear-ico,
:root[data-art="ink"] .cur-ico,
:root[data-art="ink"] .cos__swatch--gear img,
:root[data-art="ink"] .ops-galaxy{
  mix-blend-mode:normal;
}
/* The ink pack's craft are dark masses with a lit rim, which is exactly right
   on the near-black stage and slightly under-separated on a panel, so the
   hangar's hull gets a plain drop shadow instead of the rendered pack's glow. */
:root[data-art="ink"] .hangar-ship{
  filter:drop-shadow(0 12px 18px rgba(0,0,0,.72));
}


/* ============ 39b. THE PIXEL ART STYLE ============
   Everything above is written for the RENDERED pack, whose icons are glowing
   objects on pure black composited additively (mix-blend-mode:screen). The
   pixel pack is built the other way: real alpha cutouts, small, and drawn on a
   grid. Two things therefore have to change under [data-art="pixel"], and only
   these two, which is why the whole style costs one short section:

     1. THE BLEND GOES BACK TO NORMAL. Screen-blending a cutout throws away its
        darks, so a pixel hull painted as a dark mass with a lit rim would come
        out as the rim alone.
     2. SCALING GOES NEAREST NEIGHBOUR. A 64 pixel icon shown at 46 CSS pixels
        is resampled by the browser, and a smoothed pixel sprite is a blurred
        pixel sprite. `image-rendering:pixelated` is what keeps the squares
        square, and it is exactly what js/rig.js does on the canvas side with
        imageSmoothingEnabled.

   The attribute is written on the root element by applyArtStyle() in
   js/app.js, so a style is one selector away everywhere and no panel module
   has to know which pack is running. */
:root[data-art="pixel"] .pick-now img,
:root[data-art="pixel"] .ops-gun img,
:root[data-art="pixel"] .ops-kit img,
:root[data-art="pixel"] .hg-node img,
:root[data-art="pixel"] .gear-ico,
:root[data-art="pixel"] .cur-ico,
:root[data-art="pixel"] .cos__swatch--gear img,
:root[data-art="pixel"] .ops-galaxy{
  mix-blend-mode:normal;
}
:root[data-art="pixel"] img,
:root[data-art="pixel"] .ops-sky,
:root[data-art="pixel"] .hangar-bay{
  image-rendering:pixelated;
}
/* The hangar's beauty render is a soft 3D hull in the rendered pack and a
   crisp sprite here, so the glow that flattered the former only muddies the
   latter. Kept small rather than removed: the ship still has to sit on a lit
   platform rather than float in a void, and 216 is nine whole source pixels
   per screen pixel, which reads as a deliberately big sprite instead of as a
   small one that got stretched. */
:root[data-art="pixel"] .hangar-ship{
  filter:drop-shadow(0 10px 14px rgba(0,0,0,.7));
  width:min(100%,216px);
}


/* ============ 40. 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; }
}


/* ============ 41. STARFALL: THREAT, CHARGE, PAUSE AND THE FLIGHT RECORDER ============
   The peripheral HUD. The player's fovea is reserved for the word being
   typed, so everything here is designed to be read WITHOUT looking at it:
   the threat pill's number confirms what the pips and the edge vignette
   already said, charge owns the bottom edge, and the pause and debug
   surfaces sit over a dimmed, frozen field. Every animated cue holds a
   static form under reduced motion that carries the same information. */

/* ---- the threat pill, top-centre ---- */
.sf-threat{
  position:absolute; top:.45rem; left:50%; transform:translateX(-50%);
  display:flex; flex-direction:column; align-items:center; gap:3px;
  pointer-events:none; z-index:4; transition:top .3s ease;
  --tb:var(--threat-0);
}
.sf-threat.lowered{ top:6.4rem; }
.sf-threat.band-1{ --tb:var(--threat-1); }
.sf-threat.band-2{ --tb:var(--threat-2); }
.sf-threat.band-3{ --tb:var(--threat-3); }
.sf-threat.band-4{ --tb:var(--threat-4); }
/* The tag that keeps the two WPM numbers apart. Small, quiet and always there:
   a label that only appears on hover is a label the player reads once, and this
   one has to be readable in peripheral vision at the moment it matters. */
.sf-threat__tag{
  font-family:var(--mono);
  font-size:.54rem;
  letter-spacing:.14em;
  color:var(--faint);
  font-style:normal;
  margin-right:.18rem;
}
.sf-threat__pill{
  display:inline-flex; align-items:baseline; gap:.3rem;
  padding:.18rem .6rem .22rem; border-radius:8px;
  background:rgba(8,11,20,.72); border:1px solid var(--line-strong);
  font-family:var(--mono); font-variant-numeric:tabular-nums;
  transition:border-color .4s ease;
}
.band-1 .sf-threat__pill, .band-2 .sf-threat__pill,
.band-3 .sf-threat__pill, .band-4 .sf-threat__pill{ border-color:var(--tb); }
.sf-threat__pill b{ font-size:1.2rem; font-weight:700; color:rgba(234,242,255,.92); }
.sf-threat__pill small{ font-size:.62rem; color:var(--muted); opacity:.7; letter-spacing:.06em; }
.sf-threat__pill.pop{ animation:sfPillPop .16s ease-out; }
@keyframes sfPillPop{ 0%{ transform:scale(1); } 45%{ transform:scale(1.12); } 100%{ transform:scale(1); } }
.sf-threat__pips{ display:inline-flex; gap:4px; margin-left:.35rem; align-self:center; }
.sf-threat__pips i{
  width:6px; height:6px; background:transparent;
  border:1px solid var(--line-strong); border-radius:1px;
}
.band-1 .sf-threat__pips i:nth-child(-n+1),
.band-2 .sf-threat__pips i:nth-child(-n+2),
.band-3 .sf-threat__pips i:nth-child(-n+3),
.band-4 .sf-threat__pips i:nth-child(-n+4){ background:var(--tb); border-color:var(--tb); }

/* The boss readout: same pill at 0.8 scale, diamond-marked, its band on its
   own border. A hollow diamond is the honest "cannot be won in time". */
.sf-threat__pill--boss{ transform:scale(.82); transform-origin:top center; --tb:var(--threat-0); }
/* The class's display would silently beat the hidden attribute's UA rule. */
.sf-threat__pill--boss[hidden]{ display:none; }
.sf-threat__pill--boss em{ font-style:normal; color:var(--violet); font-size:.8rem; }
.sf-threat__pill--boss b{ font-size:1rem; }
.sf-threat__pill--boss.bband-1{ --tb:var(--threat-1); border-color:var(--tb); }
.sf-threat__pill--boss.bband-2{ --tb:var(--threat-2); border-color:var(--tb); }
.sf-threat__pill--boss.bband-3{ --tb:var(--threat-3); border-color:var(--tb); }
.sf-threat__pill--boss.bband-4{ --tb:var(--threat-4); border-color:var(--tb); }

/* ---- the ambient vignette: top and side edges; the bottom belongs to charge ---- */
.sf-vignette{
  position:absolute; inset:0; pointer-events:none; z-index:3;
  --tb:var(--threat-1); --vo:0; --vt:5%;
  opacity:var(--vo);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--tb) 55%, transparent), transparent var(--vt)),
    linear-gradient(90deg,  color-mix(in srgb, var(--tb) 40%, transparent), transparent var(--vt)),
    linear-gradient(270deg, color-mix(in srgb, var(--tb) 40%, transparent), transparent var(--vt));
  transition:opacity .5s ease;
}
.sf-vignette.band-0{ --vo:0; }
.sf-vignette.band-1{ --tb:var(--threat-1); --vo:.24; --vt:4%; }
.sf-vignette.band-2{ --tb:var(--threat-2); --vo:.38; --vt:6%; }
.sf-vignette.band-3{ --tb:var(--threat-3); --vo:.55; --vt:8%; }
.sf-vignette.band-4{ --tb:var(--threat-4); --vo:.75; --vt:10%; }
/* Bounded breathing: only while the .breathe class is held (a few seconds
   after a band change, managed by the game), then the vignette stands still.
   Faster and deeper as the band rises; never above the flash budget. */
.sf-vignette.breathe.band-1{ animation:sfBreathe 4s ease-in-out infinite; }
.sf-vignette.breathe.band-2{ animation:sfBreathe 2s ease-in-out infinite; }
.sf-vignette.breathe.band-3{ animation:sfBreathe 1s ease-in-out infinite; }
.sf-vignette.breathe.band-4{ animation:sfBreathe .6s ease-in-out infinite; }
@keyframes sfBreathe{
  0%, 100%{ opacity:var(--vo); }
  50%{ opacity:calc(var(--vo) * .55); }
}

/* ---- the typebox and the arming strip wear the equipped weapon ---- */
#sfType{ box-shadow:inset 3px 0 0 var(--wep-halo, transparent); }
#sfType .typebox__done{ color:var(--wep-core, var(--brand)); }
#sfType .typebox__meter.ready{ color:var(--charge); }
#sfArm .arm__pips i.on{ background:var(--wep-core, var(--brand-2)); }
/* Armed overrides the tint, deliberately: every weapon has to say "now" in the
   same colour, or the one state that must read instantly reads differently
   depending on what is fitted. */
#sfArm.ready .arm__pips i{ background:var(--good); }

/* ---- pause ---- */
.sf-paused #sfCanvas, .sf-paused #sfCanvas3d{
  filter:saturate(.35) brightness(.62);
  transition:filter .25s ease;
}
.sf-pause{
  position:absolute; inset:0; z-index:8; pointer-events:auto;
  display:flex; align-items:center; justify-content:center;
  background:rgba(4,8,20,.55);
}
.sf-pausebox{
  width:min(520px, 92%); max-height:94%; overflow:auto;
  padding:1.1rem 1.2rem; text-align:center; border-radius:16px;
  background:rgba(10,16,32,.72);
  border:1px solid rgba(120,190,255,.22);
  box-shadow:var(--shadow-3);
  backdrop-filter:blur(14px) saturate(1.3);
  -webkit-backdrop-filter:blur(14px) saturate(1.3);
  background-image:repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0 1px, transparent 1px 3px);
}
.sf-pausebox__title{ margin:.15rem 0 .2rem; font-size:1.6rem; letter-spacing:.14em; }
.sf-spark{ width:100%; height:64px; display:block; margin:.3rem 0 .5rem; }
.sf-legend{ display:flex; flex-direction:column; gap:.3rem; text-align:left; }
.sf-legend__row{
  display:flex; align-items:center; gap:.55rem;
  padding:.3rem .5rem; border-radius:8px;
  background:rgba(8,11,20,.5); border-left:3px solid transparent;
  font-size:.8rem;
}
.sf-legend__row.live{ background:rgba(8,11,20,.8); }
.sf-legend__row b{ min-width:9.5em; }
.sf-legend__row span{ font-size:.66rem; }
.sf-legend__swatch{
  flex:none; width:26px; height:14px; border-radius:4px;
  background:linear-gradient(100deg, var(--wep-core) 0 30%, var(--wep-halo) 70% 100%);
  box-shadow:0 0 8px var(--wep-halo);
}

/* ---- the resume countdown ---- */
.sf-countwrap{
  position:absolute; inset:0; z-index:9; pointer-events:none;
  display:flex; align-items:center; justify-content:center;
}
.sf-count{
  font-family:var(--mono); font-weight:800; font-size:clamp(64px, 14vw, 96px);
  color:#4FD8FF; text-shadow:0 0 24px rgba(79,216,255,.6);
  animation:sfCountIn .18s cubic-bezier(.22,1,.36,1);
}
.sf-count--go{ color:var(--good); text-shadow:0 0 24px var(--good-tint); }
@keyframes sfCountIn{ from{ transform:scale(1.35); opacity:.4; } to{ transform:scale(1); opacity:1; } }

/* ---- the flight recorder overlay (F8) ---- */
.sf-debug{
  position:absolute; left:0; right:0; bottom:0; z-index:10; height:300px;
  display:flex; flex-direction:column;
  background:rgba(6,10,20,.94); border-top:1px solid #1E2C44;
  font-family:var(--mono); font-size:.68rem; pointer-events:auto;
}
.sf-debug__bar{
  display:flex; gap:.4rem; align-items:center; flex-wrap:wrap;
  padding:.3rem .5rem; border-bottom:1px solid var(--line);
}
.sf-debug__bar .pill{ cursor:pointer; user-select:none; }
.sf-debug__bar .pill.off{ opacity:.35; }
.sf-debug__canvas{ flex:1 1 auto; min-height:0; width:100%; cursor:crosshair; }

/* Reduced motion: every cue's static form carries the same information. */
@media (prefers-reduced-motion: reduce){
  .sf-vignette{ animation:none !important; transition:none; }
  .sf-threat__pill.pop{ animation:none; }
  .sf-count{ animation:none; }
  .sf-paused #sfCanvas, .sf-paused #sfCanvas3d{ transition:none; }
}

/* Weapon rules remain readable on narrow vendor cards and during combat. */
.weapon-facts { padding: .65rem; border: 1px solid var(--line); border-radius: 8px; font-size: .82rem; text-align: left; }
.weapon-facts p { margin: .45rem 0; line-height: 1.5; }
.weapon-facts__row { display: flex; flex-wrap: wrap; justify-content: space-between; gap: .25rem .6rem; padding: .25rem 0; }
.weapon-facts__row strong { color: var(--brand); }
.weapon-preview summary { cursor: pointer; color: var(--brand); padding: .5rem 0; }
.sf-weapon-readout { display:flex; flex-direction:column; gap:.25rem; max-width:290px; font-size:.72rem; }
.sf-reserve { display:flex; align-items:center; gap:.35rem; flex:0 0 auto; padding:.3rem .5rem; border-top:1px solid var(--line); background:var(--bg); }
.sf-reserve__slots { flex:1; min-width:0; display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:.35rem; }
.sf-power-slot { position:relative; overflow:hidden; min-height:44px; display:flex; flex-direction:column; align-items:flex-start; gap:.15rem; min-width:0; padding:.4rem .5rem; border:1px solid var(--line); border-radius:8px; background:var(--bg); color:var(--muted); font:inherit; font-size:.75rem; text-align:left; cursor:pointer; }
.sf-reserve__help { flex:0 0 28px; min-height:44px; padding:.2rem; }
.sf-power-slot.is-active::after { content:""; position:absolute; left:0; bottom:0; height:3px; width:calc(var(--power-life)*100%); background:var(--brand); }
.sf-power-slot.is-active small { color:var(--brand); }
.sf-power-slot kbd { color:var(--brand); font:inherit; font-size:.68rem; }
.sf-power-slot b { color:var(--ink); }
.sf-power-slot small, .sf-power-slot__status { font-size:.62rem; line-height:1.3; }
.sf-power-slot.is-filled { border-color:var(--xp); background:color-mix(in srgb,var(--xp) 8%,var(--bg)); }
.sf-power-slot.is-filled .sf-power-slot__status { color:var(--xp); }
.sf-power-slot:disabled { cursor:default; opacity:.8; }
.sf-power-slot:focus-visible, .sf-power-drop:focus-visible { outline:2px solid var(--brand); outline-offset:2px; }
.sf-power-drops { position:absolute; inset:0; pointer-events:none; z-index:3; }
.sf-power-drops[hidden] { display:none; }
.sf-power-drop, .sf-power-flight { position:absolute; transform:translate(-50%,-50%); text-align:center; }
.sf-power-drop { display:flex; flex-direction:column; align-items:center; min-width:140px; max-width:210px; gap:.15rem; padding:.4rem .6rem .55rem; border:1px solid var(--xp); border-radius:10px; background:var(--bg); color:var(--xp); font:inherit; pointer-events:auto; cursor:pointer; overflow:hidden; box-shadow:0 4px 18px color-mix(in srgb,var(--bg) 80%,transparent); }
.sf-power-drop:disabled { opacity:1; cursor:default; }
.sf-power-drop small { font-size:.6rem; line-height:1.3; }
.sf-power-drop b { font-size:.95rem; letter-spacing:.08em; }
.sf-power-drop.is-selected { border-color:var(--brand); outline:2px solid var(--brand); outline-offset:3px; }
.sf-power-flight { color:var(--xp); font-size:.8rem; font-weight:700; padding:.3rem .5rem; background:var(--bg); border:1px solid var(--xp); border-radius:8px; white-space:nowrap; }
.power-intro__effects { padding-left:1.2rem; }
.target-protection { color:var(--xp); font:600 .65rem/1.5 var(--mono); border-top:3px double currentColor; padding-top:.15rem; }
#sfTypeProtection { grid-column:1 / -1; }
.target-protection[hidden] { display:none; }
.typebox--protected .typebox__done { text-decoration:overline; text-decoration-color:var(--xp); }
.typebox--type-only .typebox__done { text-decoration-style:double; }
.tx__protection { display:block; width:fit-content; margin:.4rem auto; }
.tx--type-only .tx__text, .tx--type-only .tx__shieldrow { border-inline-start:3px double var(--xp); }
.power-intro__effects li { margin:.6rem 0; line-height:1.5; }
@media (max-width:640px) {
  .sf-weapon-readout { max-width:none; flex:1 1 180px; }
  .sf-reserve { padding:.3rem; }
  .sf-reserve__slots { gap:.2rem; }
  .sf-power-slot { padding:.35rem .25rem; font-size:.68rem; }
  .sf-power-slot > span:first-child { display:flex; flex-direction:column; }
  .sf-power-slot small, .sf-power-slot__status { font-size:.55rem; }
  .sf-power-drop { min-width:125px; max-width:175px; }
}

/* Protected cards fit the stage even when the HUD wraps. Their own scroll
   area keeps every reward and the Continue control reachable on a phone. */
#sfCard { position: absolute; inset: 0; z-index: 8; pointer-events: none; }
.boss-win { top: 12px; bottom: 12px; transform: translateX(-50%); max-height: none; overscroll-behavior: contain; }

/* A briefing has an identity, a tactical read and one deliberate action.
   Explicit gaps keep these groups separate at every text size. */
.boss-intro{
  top:50%; bottom:auto; transform:translate(-50%,-50%);
  width:min(94%,640px); max-height:calc(100% - 24px);
  padding:clamp(1rem,3vw,1.75rem);
  display:flex; flex-direction:column; gap:1.35rem;
  text-align:left; overscroll-behavior:contain;
  background:var(--bg);
}
.boss-intro__identity{ display:grid; gap:.8rem; }
.boss-intro > *{ flex-shrink:0; }
.boss-intro .boss-intro__head{ justify-content:flex-start; gap:1.2rem; margin:0; }
.boss-intro .boss-intro__head h2{ font-size:clamp(1.5rem,3vw,2rem); line-height:1.15; }
.boss-intro .boss-intro__head p{ margin-top:.45rem; font-size:.88rem; }
.boss-intro__portrait{
  flex:0 0 112px; height:112px; display:grid; place-items:center;
  border-radius:var(--radius); background:var(--surface);
}
.boss-intro__portrait img{ width:100%; height:100%; object-fit:contain; }
.boss-intro__stats{
  display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1rem;
  margin:0; padding:1rem; background:var(--surface); border-radius:var(--radius-sm);
}
.boss-intro__stats dt{ color:var(--muted); font-size:.68rem; margin-bottom:.45rem; }
.boss-intro__stats dd{ margin:0; font-family:var(--mono); font-size:.95rem; font-weight:600; overflow-wrap:anywhere; }
.boss-intro__stats dd small{ font-size:.7rem; color:var(--muted); }
.boss-intro__section{ display:grid; gap:.7rem; }
.boss-intro__section h3{ margin:0; font-size:.88rem; color:var(--ink); }
.boss-intro__section p{ margin:0; color:var(--muted); font-size:.83rem; line-height:1.6; }
.boss-intro .boss-intro__loadout{
  display:grid; gap:.3rem; margin:0; padding:.8rem 1rem;
  font-size:.8rem; line-height:1.5; border-radius:var(--radius-sm);
}
.boss-intro__loadout b{ font-size:.7rem; }
.boss-intro__how{ border-top:1px solid var(--line); padding-top:1.1rem; }
.boss-intro__how ol{ display:grid; gap:.6rem; margin:0; padding-left:1.2rem; color:var(--muted); font-size:.8rem; line-height:1.5; }
.boss-intro__how b{ color:var(--ink); font-weight:600; }
.boss-intro__footer{ display:grid; justify-items:center; gap:.8rem; padding-top:.2rem; }
.boss-intro__footer .btn{ min-height:48px; padding:.8rem 1.5rem; }
.boss-intro .boss-intro__hint{ margin:0; text-align:center; font-size:.72rem; line-height:1.5; }
@media(max-width:480px){
  .boss-intro{ gap:1rem; }
  .boss-intro__portrait{ flex-basis:80px; height:80px; }
  .boss-intro__stats{ padding:.8rem; gap:.6rem; }
  .boss-intro__stats dd{ font-size:.8rem; }
  .boss-intro__footer .btn{ width:100%; }
}
.boss-card__actions { position: sticky; bottom: -1.1rem; padding: .6rem 0; background: var(--bg); z-index: 1; }
.arm[hidden], .typebox[hidden] { display: none; }

/* Discovery is a reading break. Let its explanation use the viewport on a
   phone instead of squeezing it below a wrapped combat HUD. */
#sfCard:has(.power-intro) { position:fixed; inset:0; z-index:100; pointer-events:auto; background:color-mix(in srgb,var(--bg) 78%,transparent); }
.power-intro { top:50%; bottom:auto; width:min(94%,620px); max-height:calc(100dvh - 24px); transform:translate(-50%,-50%); }
/* Equipment decisions: readable comparisons, explicit costs, progressive detail. */
.item-filters{display:grid;grid-template-columns:2fr repeat(3,1fr);gap:12px;padding:16px;background:var(--bg,#101522);border:1px solid #344056;border-radius:12px;margin:18px 0}
.item-filters label{display:flex;flex-direction:column;gap:7px;font-size:.76rem;color:#c5d0e1;min-width:0}
.item-filters input,.item-filters select{width:100%;min-width:0;min-height:42px;background:#101827;color:#edf3ff;border:1px solid #526079;border-radius:7px;padding:8px 10px;font:inherit}
.item-filters :focus-visible,.item-details summary:focus-visible,.loadout-details summary:focus-visible{outline:2px solid #38dfd2;outline-offset:3px}
.item-count{font-size:.8rem;color:#adbad0;margin:8px 0 16px}
.item-decision{min-width:0;width:100%;text-align:left}
.item-role{font-size:.7rem;text-transform:uppercase;letter-spacing:.07em;color:#38dfd2;font-weight:700;margin:8px 0}
.item-purpose{font-size:.87rem;line-height:1.55;min-height:3em;margin:8px 0 16px}
.item-against{font-size:.75rem;line-height:1.5;color:#adbad0;border-top:1px solid #344056;padding-top:12px}
.item-against b{color:#edf3ff}
.item-compare{width:100%;border-collapse:collapse;table-layout:fixed;font-size:.76rem;margin:10px 0}
.item-compare th,.item-compare td{padding:9px 5px;vertical-align:top;text-align:right;border-bottom:1px solid #344056;overflow-wrap:anywhere}
.item-compare th:first-child{width:50%;text-align:left;font-weight:500}
.item-compare thead th{font-size:.66rem;color:#adbad0}
.item-compare th small{display:none}
.item-compare td small{display:block;font-size:.62rem;letter-spacing:.03em;margin-top:4px}
.item-details .item-compare th small{display:block;color:#adbad0;font-weight:400;font-size:.69rem;line-height:1.5;margin-top:5px}
.item-details summary,.loadout-details>summary{cursor:pointer;color:#a9e9e3;font-size:.8rem;padding:12px 0;min-height:42px}
.item-details p{font-size:.75rem;line-height:1.6;color:#c5d0e1}
.item-details[open]{border-top:1px solid #344056;margin-top:8px}
.equipment-path{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin:18px 0 24px}
.equipment-path>div{padding:20px;border:1px solid #344056;border-radius:12px;background:linear-gradient(150deg,#172536,#111722)}
.equipment-path span{font-size:.7rem;color:#38dfd2;text-transform:uppercase;letter-spacing:.06em}
.equipment-path b{display:block;font-size:1.1rem;margin:10px 0}
.equipment-path p{font-size:.82rem;color:#b4c0d4;line-height:1.6;margin:0}
.loadout-details{margin:18px 0;border:1px solid #344056;border-radius:10px;padding:4px 16px}
.armory-grid:has(.item-decision),.pick-grid:has(.item-decision){grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}
.pick-card:has(.item-decision){padding:18px;min-width:0}
.pick-card .btn,.armory-grid .btn{min-height:40px;margin-top:auto}
.pick-now{background:#111927;border:1px solid #344056;border-radius:8px;padding:12px}
.shop-wallet{display:flex;flex-wrap:wrap;gap:.35rem 1rem;font-size:.8rem;font-variant-numeric:tabular-nums}
.shop-wallet>span{display:inline-flex;align-items:center;gap:.3rem;white-space:nowrap}
.shop-wallet__scrap{color:var(--xp-2)}
.shop-wallet__cores{color:var(--violet)}
.pick-toolbar,.armory-wallet{position:sticky;z-index:4;background:var(--plate);border-bottom:1px solid var(--line-strong);padding:.65rem 0;margin-bottom:.9rem}
.pick-toolbar{top:0;display:flex;align-items:center;justify-content:space-between;gap:.5rem}
.pick-toolbar>.btn{flex:none}
/* Hidden overflow on body otherwise creates a non-scrolling sticky ancestor. */
body:has(#panel-armory:not([hidden])){overflow-x:clip}
.armory-wallet{top:var(--shop-head-h,var(--head-h))}
@media(min-width:1500px){.armory-grid:has(.item-decision){grid-template-columns:repeat(3,minmax(0,1fr))}}
@media(max-width:760px){.item-filters{grid-template-columns:repeat(2,minmax(0,1fr))}.item-search{grid-column:1/-1}.equipment-path{grid-template-columns:1fr}.armory-grid:has(.item-decision),.pick-grid:has(.item-decision){grid-template-columns:1fr}.item-purpose{min-height:0}.pick-now{position:static}}
.vendor.vendor--compact{display:flex;flex-direction:row;gap:12px;align-items:center;padding:14px;min-height:116px;text-align:left}
.vendor--compact .vendor__face{width:72px;height:84px;object-fit:cover;flex-shrink:0;margin:0}
.vendor-copy{display:flex;flex-direction:column;gap:6px;min-width:0}
.vendor-copy>b{font-size:.95rem}.vendor-copy>small{font-size:.7rem;color:#b4c0d4}.vendor-copy .label{font-size:.62rem;margin:0}
.armory-grid>.cos:has(.item-decision)>.cos__swatch--gear{width:76px;height:76px;min-height:0;aspect-ratio:auto;margin:0 0 12px;align-self:flex-start}
.armory-grid>.cos:has(.item-decision)>.cos__swatch--gear img{width:64px;height:64px;object-fit:contain}
.item-purpose{font-weight:400}

/* Narrative is read on calm surfaces. Longer cards scroll within the stage. */
.story-brief, .story-dispatch { text-align:left; border-left:2px solid var(--brand); padding:.6rem .8rem; margin:.7rem 0; background:var(--brand-tint); }
.story-brief h3, .story-dispatch h3 { margin:.25rem 0 .4rem; font-size:1rem; }
.story-brief p, .story-dispatch p { margin:.4rem 0; font-size:.85rem; line-height:1.55; }
.story-question { color:var(--brand); }
.story-journal summary { cursor:pointer; padding:.5rem 0; }
.story-journal h4 { margin:.8rem 0 .2rem; }
.boss-intro, .boss-win { max-height:calc(100% - 1rem); overflow-y:auto; }

/* Ship-side meters carry live charge; keep the duplicate HUD control compact. */
#sfHud .sf-weapon-readout > span, #sfHud .sf-weapon-readout > small { display:none; }
#sfHud .sf-weapon-readout { max-width:240px; gap:0; }

.tx__goal-text { min-width:0; overflow:hidden; text-overflow:ellipsis; }
.tx__hull { flex:none; color:var(--brand); }
.tx__hull.is-low { color:var(--hazard); }
.tx__hull.is-critical { color:var(--bad); font-weight:700; }
@media(max-width:640px){ .sf-stage:has(.tx) .sf-threat { display:none; } }
/* A stable line below the health rail keeps attack intent readable while typing. */
.boss-intent{margin:.35rem 0;font-size:.76rem;font-variant-numeric:tabular-nums;color:var(--muted)}
.boss-intent.is-charging{color:var(--hazard)}
.switch-lesson{width:min(94vw,580px);max-height:90dvh;overflow:auto;padding:1.2rem;color:var(--ink);background:var(--surface);border:1px solid var(--brand);border-radius:var(--radius);}
.switch-lesson::backdrop{background:rgba(0,0,0,.78)}
.switch-lesson__ships{display:flex;gap:1rem;margin:1rem 0;}
.switch-lesson__ships>div{flex:1;padding:.7rem;background:var(--bg);border-radius:var(--radius)}
.switch-lesson__ships small,.switch-lesson__ships strong{display:block}
.switch-lesson__ships strong{font-family:var(--mono);margin-top:.5rem}
.switch-lesson__ships .is-active{color:var(--brand)}
.switch-lesson input{display:block;width:100%;margin:.5rem 0;padding:.65rem;color:var(--ink);background:var(--bg);border:1px solid var(--line);border-radius:var(--radius-sm);font:1rem var(--mono)}
.switch-lesson input[aria-invalid=true]{border-color:var(--bad)}
.typebox__meter{white-space:normal;letter-spacing:0}
#sfTx:has(.tx){ flex:0 0 auto; position:relative; background:var(--bg); border-top:1px solid var(--line); }
#sfTx > .tx{ position:relative; inset:auto; padding:.7rem; background:var(--bg); }
#sfTx .tx__card{ position:relative; inset:auto; transform:none; margin:0 auto; }
.sf-root:has(.tx) #sfBossBrief{ display:none; }
.tx__accuracy{margin:.3rem 0 0;font-size:.72rem;min-height:1.3em;font-variant-numeric:tabular-nums}
/* Short desktop play keeps the reading, correction and reserves together.
   Only the passage viewport scrolls; its caret follows by whole text lines. */
@media(min-width:900px){
  body:has(#gameHost:not([hidden]) .sf-root) .masthead{position:static}
  .sf-root{min-height:calc(100dvh - 52px)!important}
  #sfTx>.tx{padding:.2rem .65rem}
  #sfTx .tx__text{--reading-height:clamp(97px,calc(100dvh - 850px),240px);max-width:80ch;font-size:17px;--tx-lh:1.5;min-height:var(--reading-height);height:var(--reading-height);max-height:var(--reading-height);padding-top:20px;overflow-y:auto;box-sizing:border-box}
  #sfTx .tx__goal{margin:.1rem 0 .2rem}
  #sfTx .tx__accuracy{display:inline-block;width:64%;vertical-align:top;margin:.1rem 0 0}
  #sfTx .tx__offline{display:inline-block;width:36%;text-align:right;vertical-align:top;margin:.1rem 0 0;padding:0;font-size:.65rem}
  #sfTx .tx__bar{margin-top:.1rem}
  #sfTx .typebox{margin:.2rem auto 0;padding:.1rem .5rem;font-size:17px;height:30px;min-height:30px;grid-template-areas:'word meter fix';grid-template-columns:minmax(0,1fr) auto auto;align-items:center}
  #sfTx .typebox.bad{margin:.2rem auto 0;padding:.1rem .5rem;font-size:17px}
  .sf-root:has(#sfTx .tx) .sf-performance{display:none}
  .sf-root:has(#sfTx .tx) .sf-mechanic{padding:.25rem .65rem}
  .sf-root:has(#sfTx .tx) .sf-reserve{padding:.1rem .5rem}
  .sf-root:has(#sfTx .tx) .sf-power-slot{padding:.2rem .5rem}
  .sf-root:has(#sfTx .tx) #sfHud{row-gap:.25rem;padding:.25rem .75rem}
  .sf-root:has(#sfTx .tx) #sfBossTop{padding:.3rem .75rem!important}
  .sf-root:has(#sfTx .tx) .boss-face{width:28px;height:28px}
  .sf-root:has(#sfTx .tx) .boss-name small{display:none}
  .sf-root:has(#sfTx .tx) .boss-intent{margin:.1rem 0}
}
@media(min-width:900px) and (max-height:740px){
  .sf-root:has(#sfTx .tx) .sf-stage{min-height:220px!important}
  #sfTx .tx__text{min-height:72px;height:72px;max-height:72px}
}
/* Run analysis uses native controls, readable tables and labelled SVG charts. */
.analytics-report { margin-top:1.2rem; text-align:left; max-width:100%; }
.analytics-report summary, #runAnalysis summary { cursor:pointer; padding:.7rem 0; color:var(--brand); }
.analytics-report details[open], #runAnalysis [data-detail]:not(:empty) { border-top:1px solid var(--line); padding-top:1rem; }
.analytics-stats { grid-template-columns:repeat(auto-fit,minmax(105px,1fr)); margin:1rem 0; }
.analytics-charts { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1rem; margin:1rem 0; }
.analytics-chart { margin:0; min-width:0; }
.analytics-chart figcaption { font-weight:600; margin-bottom:.4rem; }
.analytics-chart svg { display:block; width:100%; color:var(--muted); }
.analytics-chart text { fill:currentColor; font:16px monospace; }
.analytics-point circle { pointer-events:all; cursor:crosshair; }
.analytics-tooltip { visibility:hidden; pointer-events:none; }
.analytics-tooltip rect { fill:var(--bg,#101525); stroke:var(--muted); stroke-width:1; }
.analytics-chart .analytics-tooltip text { fill:var(--text,#fff); font:14px monospace; }
.analytics-chart .analytics-tooltip text:first-of-type { font-weight:700; }
.analytics-point:hover .analytics-tooltip, .analytics-point:focus-within .analytics-tooltip { visibility:visible; }
.analytics-note { font-size:.8rem; line-height:1.6; }
.analytics-patterns { padding-left:1.1rem; }
.analytics-patterns li { margin:.5rem 0; }
.analytics-patterns b { font-family:monospace; color:var(--brand); }
.analytics-patterns span { font-size:.8rem; }
#runAnalysis { scroll-margin-top:1rem; }
#runAnalysis label { display:flex; flex-wrap:wrap; align-items:center; gap:.4rem; min-width:0; max-width:100%; }
#runAnalysis [data-group] { width:100%; }
#runAnalysis select { max-width:100%; min-width:0; color:var(--ink); background:var(--surface); border:1px solid var(--line-strong); border-radius:6px; padding:.5rem; }
.sf-reporting #sfEnd { position:absolute; inset:0; z-index:30; }
.run-summary-shell{position:absolute;inset:0;overflow:auto;padding:clamp(.5rem,2vw,1.2rem);background:rgba(8,11,20,.97);pointer-events:auto}
.report.run-summary{max-width:780px;text-align:left;padding:clamp(1rem,2.6vw,1.65rem);border-top:2px solid var(--brand)}
.run-summary__header,.run-xp__head,.run-xp__caption,.run-salvage__head{display:flex;align-items:center;justify-content:space-between;gap:1rem}
.run-summary h2{margin:.3rem 0 .85rem;font:700 1.75rem var(--mono);color:var(--ink)}
.run-summary h2 small{font-size:.75rem;color:var(--muted);margin-left:.7rem}
.run-duration{text-align:right;white-space:nowrap;font-family:var(--mono)}
.run-duration span,.run-duration b{display:block}
.run-duration span{color:var(--muted);font-size:.7rem}
.run-duration b{font-size:1.1rem}
.run-performance{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.6rem;margin:0}
.run-metric{padding:.7rem .85rem;background:var(--surface);border:1px solid var(--line);border-radius:8px;min-width:0}
.run-metric dt{color:var(--muted);font-size:.76rem}
.run-metric dd{margin:.3rem 0 .15rem;font:700 clamp(1.4rem,3vw,1.8rem) var(--mono);font-variant-numeric:tabular-nums}
.run-metric small{display:block;color:var(--muted);font-size:.66rem}
.run-metric--hero{border-color:var(--line-strong);background:linear-gradient(120deg,var(--brand-tint),var(--surface))}
.run-metric--hero dd{font-size:clamp(2rem,5vw,2.65rem);color:var(--brand)}
.run-summary details{font-size:.78rem}
.run-summary summary{cursor:pointer;padding:.55rem 0;color:var(--muted)}
.run-summary details p{font-size:.78rem;line-height:1.6;margin:.4rem 0 .65rem}
.run-method{margin:.2rem 0 .65rem}
.run-rewards{border-top:1px solid var(--line);padding-top:.8rem}
.run-rewards>h3{font-size:.72rem;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);margin:0 0 .6rem}
.run-rewards__totals{display:flex;gap:2rem}
.run-rewards__totals span{display:block;font-size:.75rem;color:var(--muted)}
.run-rewards__totals b{display:block;font:600 1.3rem var(--mono);color:var(--xp-2);margin-top:.2rem}
.run-rewards__totals small{font-size:.7rem;color:var(--muted)}
.run-scrap-equation{font-size:.72rem;color:var(--muted);line-height:1.6;margin:.6rem 0 .85rem}
.run-scrap-equation span{font-size:.68rem}
.run-xp{border:1px solid var(--line-strong);border-radius:8px;padding:.85rem;background:linear-gradient(110deg,rgba(242,179,61,.07),transparent)}
.run-xp h3{font:600 1.1rem var(--mono);margin:.3rem 0 .7rem}
.run-xp h3 small{font-size:.7rem;color:var(--muted)}
.run-xp__earned{text-align:right;white-space:nowrap}
.run-xp__earned b{display:block;font:700 1.2rem var(--mono);color:var(--xp-2)}
.run-xp__earned span{font-size:.72rem;color:var(--xp)}
.run-xp__bar{position:relative;height:12px;border-radius:4px;background:var(--bg);border:1px solid var(--line-strong)}
.run-xp__bar i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--xp),var(--xp-2));border-radius:3px}
.run-xp__bar>span{position:absolute;top:-3px;bottom:-3px;width:2px;background:var(--ink);box-shadow:0 0 0 1px var(--bg)}
.run-xp__caption{align-items:flex-start;margin-top:.65rem;font:400 .66rem var(--mono);color:var(--muted)}
.run-xp__caption b{font-weight:500;text-align:right;color:var(--ink)}
.run-badges{font-size:.78rem;color:var(--brand)}
.run-salvage{border-top:1px solid var(--line);padding:.75rem 0 .4rem;margin-top:.5rem;font-size:.78rem}
.run-salvage__head{margin-bottom:.3rem}
.salvage-icons{display:flex;gap:.35rem;align-items:center;flex-wrap:wrap;justify-content:flex-end}
.salvage-icon{display:inline-flex;width:26px;height:26px;flex-shrink:0;align-items:center;justify-content:center;border:1px solid currentColor;border-radius:5px;color:var(--brand);font:16px var(--mono)}
.salvage-icon.r-rare{color:var(--violet)}
.salvage-icon.r-elite{color:var(--xp)}
.salvage-icon.r-legendary{color:var(--hazard)}
.salvage-core{color:var(--violet);font: .7rem var(--mono)}
.run-salvage ul{list-style:none;padding:0;margin:.3rem 0}
.run-salvage li{display:flex;align-items:center;gap:.6rem;padding:.45rem 0;border-top:1px solid var(--line)}
.run-salvage li small{display:block;color:var(--muted);margin-top:.2rem}
.run-summary__footer{position:sticky;bottom:-1px;display:flex;justify-content:space-between;gap:.5rem;padding:.75rem 0 .1rem;background:var(--plate);border-top:1px solid var(--line);z-index:1}
.run-summary__footer .btn{font-size:.8rem}
.run-summary [hidden]{display:none}
.run-summary h2:focus{outline:none}
.sf-salvage-count{color:var(--violet);font:.6rem var(--mono);white-space:nowrap}
#sfHud .combat-salvage{display:flex;grid-column:1/-1;flex-direction:row;align-items:baseline;flex-wrap:wrap;gap:4px 6px;border-top:1px solid #ffffff14;padding-top:6px}
#sfHud .combat-salvage b{color:var(--xp-2)}
#sfHud .combat-salvage small{flex-basis:100%}
.sf-root:has(.sf-reporting) #sfHud,.sf-root:has(.sf-reporting) .combat-controls,.sf-root:has(.sf-reporting) #sfReserve{display:none!important}
.run-summary .analytics-report{margin:0;padding:0;border:0}
@media(max-width:480px){
  .run-metric{padding:.65rem .5rem}.run-metric dt{font-size:.68rem}.run-metric small{font-size:.6rem}
  .run-xp__caption{flex-direction:column;gap:.35rem}.run-summary__header .label{font-size:.62rem}
  .run-summary h2{font-size:1.4rem}.run-rewards__totals{gap:1.3rem}
  .run-summary-shell{padding:.35rem}.report.run-summary{padding:.85rem}
}
.analytics-report .board-table, #runAnalysis .board-table { font-size:.8rem; }
@media(max-width:640px){ .analytics-charts { grid-template-columns:1fr; } #runAnalysis .btn-row { align-items:stretch; } .analytics-chart text { font-size:21px; } }
:root{--mechanic-space:.65rem}
.sf-performance{padding:.45rem .8rem;color:var(--brand);font-size:.72rem;border-bottom:1px solid var(--line);min-height:1.6rem}
.sf-mechanic{padding:var(--mechanic-space) .85rem;border:1px solid var(--brand);border-radius:8px;margin:.5rem 0;background:var(--surface);font-size:.8rem}
.sf-mechanic strong{color:var(--brand)}
.sf-mechanic-status{float:right;color:var(--xp);font-size:.75rem}
.sf-mechanic p{margin:.35rem 0}
.sf-mechanic-notice{color:var(--xp);height:1.5em;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sf-mechanic-notice:empty{visibility:hidden}
.sf-mechanic details{font-size:.72rem;color:var(--muted)}
/* Physical-keyboard desktops keep targets and the reading surface in view.
   The battlefield stays on the left across every mechanic in the fight. */
@media(min-width:1000px){
  /* Leave room for both rows of the keyboard correction box. */
  #sfArm{bottom:calc(clamp(.5rem,3.5vh,1.3rem) + 5rem)}
}
@media(max-width:600px){.sf-mechanic-status{float:none;display:block;margin-top:.2rem}.sf-mechanic{font-size:.72rem}.sf-performance{font-size:.65rem}}

/* Boss instructions stay compact; the battlefield keeps its full width. */
.sf-mechanic{padding:.35rem .65rem;margin:.25rem 0}
.sf-mechanic.is-closing{border-color:var(--xp)}
.sf-mechanic.is-closing .sf-mechanic-status{font-weight:800;color:var(--xp)}
.sf-mechanic-notice{height:1em;line-height:1;font-size:.7rem;margin:.15rem 0!important}
/* The charge lesson is a paused, skippable playback with step controls. */
.charge-lesson__stage { padding:1rem; border:1px solid var(--line); border-radius:12px; background:var(--bg); text-align:center; }
.charge-lesson__ship { display:flex; justify-content:center; align-items:center; gap:1rem; min-height:70px; font-size:1.6rem; color:var(--brand); }
.charge-lesson__ship span { font-size:3rem; }
.charge-lesson__stage progress { display:block; width:100%; height:20px; margin:.5rem 0; accent-color:var(--brand); }
.charge-lesson__stage [data-typing] { min-height:3em; overflow-wrap:anywhere; }
.equipment-compare { width:min(820px,calc(100vw - 24px)); max-height:calc(100dvh - 32px); overflow:auto; padding:20px; background:#090e19; color:#eee8d5; border:1px solid #a78b53; border-radius:8px; box-shadow:0 20px 90px #000c; }
.equipment-compare::backdrop { background:#000a; }
.equipment-compare__head { display:flex; justify-content:space-between; align-items:center; gap:16px; margin-bottom:16px; }
.equipment-compare__head h2 { font-size:1.25rem; margin:0; }
.equipment-compare__pair { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
.equipment-tooltip { min-width:0; padding:18px; border:1px solid #6c6146; border-radius:5px; background:linear-gradient(#121c30,#080d18); box-shadow:inset 0 0 0 2px #040711; }
.equipment-tooltip h3 { color:var(--r,#d4c39c); font-size:1.15rem; margin:6px 0; }
.equipment-tooltip__status { color:#e8d29a; text-transform:uppercase; font-size:.7rem; letter-spacing:.09em; margin:0; }
.equipment-tooltip__meta { color:#b2b8c5; font-size:.75rem; margin:0 0 14px; }
.equipment-tooltip dl { margin:0; border-top:1px solid #383e4a; padding-top:8px; }
.equipment-tooltip dl>div { display:flex; justify-content:space-between; gap:12px; padding:7px 0; font-size:.81rem; border-bottom:1px solid #252c3a; }
.equipment-tooltip dt { color:#dedacb; }
.equipment-tooltip dd { margin:0; text-align:right; font-variant-numeric:tabular-nums; white-space:nowrap; }
.equipment-tooltip dd span { display:block; font-size:.75rem; }
.item-gain { color:#73e889; }
.item-loss { color:#ff8e91; }
.equipment-tooltip__effect { color:#8be09a; font-size:.83rem; line-height:1.65; margin-bottom:0; }
.equipment-compare__note { color:#aeb6c7; font-size:.75rem; line-height:1.6; }
.item-compare-button { width:100%; margin:4px 0 12px; }
.ability-guide section { border-bottom:1px solid var(--line); padding:10px 0 16px; }
@media(max-width:600px){ .equipment-compare__pair { grid-template-columns:1fr; } .equipment-compare{padding:14px;} }

/* Compact inventory: the bag remains visible beside one shared inspector. */
#pickerDlg.dlg { width:1120px; max-width:calc(100vw - 24px); max-height:calc(100dvh - 24px); }
#pickerBody.dlg-in { padding:18px; max-height:calc(100dvh - 24px); overflow:auto; }
.inventory-heading { position:sticky; top:-18px; z-index:5; background:var(--plate); display:grid; grid-template-columns:1fr auto auto; align-items:center; gap:12px; padding-block:6px; }
.inventory-heading .label { margin:0 0 4px; }
.inventory-heading h2 { font-size:1.15rem; margin:0; }
.inventory-hint { color:var(--muted); font-size:.78rem; margin:8px 0 14px; }
.inventory-layout { display:grid; grid-template-columns:260px minmax(0,1fr); gap:18px; align-items:start; }
.inventory-bag { position:sticky; top:60px; min-width:0; }
.inventory-wallet { padding:4px 0; }
.inventory-bag .item-filters { grid-template-columns:1fr 1fr; gap:8px; padding:0; border:0; background:none; margin:12px 0; }
.inventory-bag .item-search { grid-column:1/-1; }
.inventory-bag .item-filters label { font-size:.7rem; gap:4px; }
.inventory-bag .item-filters input,.inventory-bag .item-filters select { min-height:34px; padding:6px; border-radius:4px; }
.inventory-count { display:flex; flex-wrap:wrap; gap:4px; font-size:.73rem; color:var(--muted); margin:10px 0; }
.inventory-count button,.inventory-empty button { border:0; background:none; color:var(--brand); font:inherit; padding:0; text-decoration:underline; cursor:pointer; }
.inventory-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:6px; max-height:calc(100dvh - 345px); overflow:auto; padding:3px; }
.inventory-tile { display:flex; flex-direction:column; align-items:center; gap:4px; position:relative; min-width:0; min-height:102px; padding:7px 3px; background:linear-gradient(160deg,#162135,#0c111d); color:var(--ink); border:1px solid color-mix(in srgb,var(--r,#8294ad) 55%,#283242); border-radius:5px; cursor:pointer; font:inherit; }
.inventory-tile__art { position:relative; display:block; height:40px; }
.inventory-tile img { object-fit:contain; }
.inventory-tile__level { position:absolute; bottom:-2px; right:-8px; font:700 .65rem var(--mono); background:#0c111de8; color:var(--xp); }
.inventory-tile b { font-size:.68rem; line-height:1.2; font-weight:600; min-height:2.4em; overflow-wrap:anywhere; }
.inventory-tile small { font-size:.58rem; color:var(--muted); margin-top:auto; }
.inventory-tile.is-equipped small { color:var(--brand); }
.inventory-tile.is-preview { background:#1d3545; border-color:var(--brand); }
.inventory-tile[aria-pressed="true"] { box-shadow:inset 0 0 0 1px var(--brand); }
.inventory-tile:focus-visible,.inventory-count button:focus-visible { outline:2px solid var(--brand); outline-offset:2px; }
.inventory-inspector { min-width:0; }
.inventory-scope { font-size:.66rem; line-height:1.4; color:var(--muted); margin:0 0 8px; }
.inventory-pair { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); column-gap:10px; }
.inventory-sheet { display:grid; grid-template-rows:subgrid; min-width:0; padding:0 10px 6px; background:linear-gradient(145deg,#141e30,#0a101c); border:1px solid color-mix(in srgb,var(--r,#8294ad) 50%,#303c50); border-radius:5px; }
.inventory-sheet:first-child { grid-column:1; }
.inventory-sheet:last-child { grid-column:2; }
.inventory-sheet header { padding:9px 0; min-height:94px; box-sizing:border-box; }
.inventory-sheet header>p { font:600 .59rem var(--mono); letter-spacing:.07em; text-transform:uppercase; color:var(--brand); margin:0 0 7px; }
.inventory-sheet header>div { display:flex; align-items:center; gap:7px; min-height:34px; }
.inventory-sheet header img { flex:none; object-fit:contain; }
.inventory-sheet h3 { font-size:.9rem; line-height:1.2; color:var(--r,var(--ink)); margin:0; overflow-wrap:anywhere; }
.inventory-sheet header small { display:block; font-size:.62rem; color:var(--muted); margin-top:6px; }
.inventory-sheet dl { display:contents; }
.inventory-sheet dl>div { display:flex; align-items:center; justify-content:space-between; gap:8px; min-height:29px; padding:4px 0; border-top:1px solid #293448; font-size:.7rem; line-height:1.2; }
.inventory-sheet dt { color:#bfcadd; min-width:0; }
.inventory-sheet dd { margin:0; text-align:right; font-variant-numeric:tabular-nums; flex-shrink:0; }
.inventory-sheet dd span { font-size:.65rem; white-space:nowrap; }
.inventory-key { font-size:.65rem; color:var(--muted); margin:8px 0; line-height:1.5; }
.inventory-about { font-size:.72rem; color:var(--muted); }
.inventory-about summary { padding:7px 0; cursor:pointer; }
.inventory-about p { margin:6px 0; line-height:1.5; }
.inventory-action { position:sticky; bottom:-18px; display:flex; flex-wrap:wrap; align-items:center; gap:8px 12px; background:var(--plate); border-top:1px solid var(--line); padding:10px 0; margin-top:8px; }
.inventory-action .btn { font-size:.76rem; min-height:38px; }
.inventory-action p { font-size:.69rem; line-height:1.4; color:var(--muted); margin:0; flex:1 1 170px; }
.inventory-empty { font-size:.8rem; color:var(--muted); padding:12px 0; }
.item-decision .item-compare th,.item-decision .item-compare td { padding:4px 5px; }
.item-decision .item-compare td small { display:inline; margin-left:4px; font-size:.6rem; }
@media(max-width:940px){
  .inventory-layout { grid-template-columns:210px minmax(0,1fr); gap:12px; }
  .inventory-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
  .inventory-sheet dl>div { align-items:start; flex-direction:column; gap:3px; }
  .inventory-sheet dd { align-self:flex-end; }
}
@media(max-width:680px){
  #pickerBody.dlg-in { padding:12px; }
  .inventory-heading { top:-12px; grid-template-columns:1fr auto; gap:5px 10px; }
  .inventory-heading>.btn { grid-column:2; grid-row:1; }
  .inventory-wallet { grid-column:1/-1; }
  .inventory-layout { grid-template-columns:minmax(0,1fr); gap:14px; }
  .inventory-bag { position:static; }
  .inventory-grid { grid-template-columns:repeat(auto-fill,minmax(74px,1fr)); max-height:222px; }
  .inventory-tile { min-height:99px; }
  .inventory-bag .item-filters { grid-template-columns:1.3fr 1fr 1fr; }
  .inventory-bag .item-search { grid-column:auto; }
  .inventory-pair { column-gap:6px; }
  .inventory-sheet { padding-inline:7px; }
  .inventory-sheet header>div { align-items:start; gap:4px; }
  .inventory-sheet header img { width:26px; height:26px; }
  .inventory-sheet h3 { font-size:.78rem; }
  .inventory-sheet header small { font-size:.59rem; }
  .inventory-sheet dl>div { min-height:42px; font-size:.67rem; }
  .inventory-action { bottom:-12px; }
  .inventory-action .btn { width:100%; min-height:42px; }
  .equipment-compare { padding:12px; }
}

/* Combat chrome floats in the field; enemies own the available height. */
body:has(.game-host:not([hidden]) .sf-root){overflow:hidden}
.game-host:not([hidden]):has(.sf-root){position:fixed;inset:0;z-index:60;border:0;border-radius:0;height:100dvh;display:flex;flex-direction:column}
.game-host:has(.sf-root)>.btn-row{display:none}
.game-host .sf-root{height:100%!important;min-height:0!important}
.sf-root #sfHud{
  position:absolute;top:10px;left:10px;z-index:5;width:194px;box-sizing:border-box;
  display:grid;grid-template-columns:1.2fr .8fr 1fr;gap:8px;padding:11px!important;
  border:1px solid rgba(132,176,197,.2);border-radius:10px;
  background:rgba(8,14,23,.78);box-shadow:0 5px 24px #0003;
}
#sfHud> :not(.combat-stat):not(.combat-weapon):not(.combat-streak){display:none!important}
.combat-streak{grid-column:1/-1;min-width:0;border-top:1px solid #ffffff14;padding-top:7px;line-height:1.4}
.combat-streak>div:first-child{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:2px 6px;font-size:.75rem;color:var(--ink)}
.combat-streak .wbonus{font:600 .9rem var(--mono);white-space:nowrap}
.combat-streak small{display:block;min-height:2.8em;font-size:.75rem;color:var(--muted);letter-spacing:0}
.combat-streak.is-loss small{color:var(--xp)}
.combat-streak__meter{height:4px;margin:5px 0;background:#ffffff1a;border-radius:2px;overflow:hidden}
.combat-streak__meter[hidden]{display:none}
.combat-streak__meter i{display:block;width:calc(var(--streak,0)*100%);height:100%;background:var(--brand-2)}
#sfHud .combat-stat b{font-size:.94rem;font-variant-numeric:tabular-nums}
#sfHud .combat-stat span{font-size:.52rem;letter-spacing:.12em}
.combat-weapon{grid-column:1/-1;border:0;border-top:1px solid #ffffff14;padding:7px 0 0;background:none;color:var(--brand);font:600 .7rem var(--mono);text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
.combat-weapon:disabled{opacity:1;cursor:default}
.combat-controls{position:absolute;right:10px;top:10px;z-index:6}
.combat-controls [hidden]{display:none!important}
.sf-root .sf-performance,.sf-root #sfThreat{display:none!important}
.sf-boss-dock{position:absolute;left:50%;top:30%;transform:translateX(-50%);width:min(360px,calc(100% - 20px));z-index:3;pointer-events:none}
.sf-boss-dock.is-reading,.sf-boss-dock.is-repair{width:min(650px,calc(100% - 24px))}
.sf-boss-dock #sfBossTop{position:relative!important;inset:auto!important;padding:7px 12px!important;background:rgba(8,14,23,.88)!important;border:1px solid #eaba7833;border-radius:8px 8px 0 0}
.sf-boss-dock #sfBossTop>div:first-child{gap:8px!important;margin:0!important}
.sf-boss-dock .boss-face,.sf-boss-dock .boss-name small,.sf-boss-dock .profile,.sf-boss-dock #sfBossBrief{display:none}
.sf-boss-dock .boss-name{font-size:.82rem;color:var(--xp)}
.sf-boss-dock .bossbar{height:3px;margin:6px 0 0}
.sf-boss-dock .boss-intent{font-size:.64rem;margin:5px 0 0}
.sf-boss-dock .sf-mechanic{margin:0;padding:6px 12px!important;background:rgba(8,14,23,.88);border:1px solid #eaba7822;border-top:0;border-radius:0;font-size:.67rem}
.sf-boss-dock .sf-mechanic>strong{display:none}
.sf-boss-dock .sf-mechanic-status{float:none;font-size:.67rem}
.sf-boss-dock .sf-mechanic details{pointer-events:auto;font-size:.62rem;margin-top:3px;max-height:130px;overflow:auto}
.sf-boss-dock .sf-mechanic-notice:empty{display:block;visibility:hidden}
#sfBossDock #sfTx{position:relative;border:0;background:none}
#sfBossDock #sfTx>.tx{padding:9px 12px;background:linear-gradient(135deg,rgba(19,29,41,.96),rgba(8,14,23,.92));border:1px solid #eaba7844;border-top:0;border-radius:0 0 10px 10px}
#sfBossDock #sfTx .tx__text{box-sizing:border-box;font-size:clamp(17px,1.5vw,18px);line-height:1.65;--tx-lh:1.65;min-height:64px;height:auto;max-height:128px;max-width:none;width:100%;padding:22px 0 3px;margin:0;overflow-y:auto;scrollbar-width:thin;pointer-events:auto}
#sfBossDock #sfTx .tx__goal{margin:0 0 3px;font-size:.65rem}
#sfBossDock #sfTx .tx__accuracy{width:auto;display:grid;font-size:.7rem;height:46px;min-height:46px;margin:6px 0}
.tx__accuracy{box-sizing:border-box;display:grid;grid-template-columns:24px minmax(0,1fr) 80px;align-items:center;gap:8px;height:46px;padding:5px 8px;border:1px solid var(--line);border-radius:6px;background:rgba(11,30,30,.65);color:var(--good)}
.tx__accuracy-icon{font-size:20px;font-weight:700;text-align:center}
.tx__accuracy-copy{display:flex;flex-direction:column;min-width:0;line-height:1.35}
.tx__accuracy-copy strong{font-size:.76rem;letter-spacing:.02em}
.tx__accuracy-detail{font-size:.65rem;color:var(--muted);white-space:nowrap}
.tx__accuracy.is-recovering{color:var(--muted);background:rgba(25,31,39,.8)}
.tx__accuracy.is-reduced{color:var(--xp);border-color:var(--xp);background:rgba(75,39,10,.6)}
.tx__accuracy.is-reduced .tx__accuracy-detail{color:var(--ink)}
.tx__accuracy-meter{display:grid;grid-template-columns:repeat(10,1fr);gap:3px;height:15px}
.tx__accuracy-meter i{border:1px solid currentColor;border-radius:1px;opacity:.35}
.tx__accuracy-meter i.is-filled{background:currentColor;opacity:1}
.tx.has-damage-penalty{box-shadow:inset 0 0 0 1px var(--xp)}
.tx.has-damage-penalty .tx__ch.is-cur{box-shadow:inset 0 -2px var(--xp)}
#sfBossDock #sfTx .tx__accuracy--repair{display:block;height:auto;min-height:1.3em;padding:0;border:0;background:none;color:var(--muted)}
@media(max-width:440px){
  #sfBossDock #sfTx .tx__accuracy{grid-template-columns:18px minmax(0,1fr) 55px;gap:5px;padding:4px 6px}
  .tx__accuracy-meter{gap:2px;height:13px}
  .tx__accuracy-detail{font-size:.6rem}
}
#sfBossDock #sfTx .tx__offline{display:none}
#sfBossDock #sfTx .tx__bar,#sfBossDock #sfTx .tx__hull{display:none}
#sfBossDock #sfTx .typebox{height:auto;min-height:50px;padding:5px 9px 5px 54px;margin:7px 0 0;width:100%;max-width:100%;font-size:16px;grid-template-areas:'word fix' 'meter meter';grid-template-columns:minmax(0,1fr) auto}
.sf-root #sfType,#sfBossDock #sfTx .typebox{padding-left:82px;min-height:66px;box-sizing:border-box}
.focus-vitals{position:absolute;left:9px;top:18px;bottom:17px;display:flex;gap:13px;pointer-events:auto}
.focus-vital{position:relative;display:block;width:10px;height:100%;min-height:24px;border:1px solid #a1c5d633;border-radius:3px;background:#263442;overflow:visible}
.focus-vital i{position:absolute;bottom:0;left:0;right:0;height:calc(var(--energy,1)*100%);background:var(--brand);border-radius:2px;transition:height .12s linear}
.focus-vital--enemy i{background:var(--hazard)}
.focus-vital--shield i{background:var(--brand-2)}
.focus-vital[data-points]::before{content:attr(data-points);position:absolute;bottom:calc(100% + 3px);left:50%;transform:translateX(-50%);font:8px var(--mono);color:var(--ink)}
.focus-vital.is-critical i{background:var(--bad)}
.focus-vital small{position:absolute;top:calc(100% + 3px);left:50%;transform:translateX(-50%);font:6px var(--mono);letter-spacing:0;color:var(--muted)}
.sf-text-occluded{visibility:hidden!important}
.sf-pressure-warning{padding:10px 12px;border:1px solid var(--xp);border-radius:8px;background:rgba(100,65,10,.25);color:var(--xp);font-size:.85rem;line-height:1.5}
.combat-vitals{grid-column:1/-1;display:grid;gap:7px}
.combat-vital-row{display:grid;grid-template-columns:1fr auto;gap:3px;font:10px var(--mono);color:var(--brand)}
.combat-vital-row b{font-size:10px!important;color:var(--ink)}
.combat-vital-row .basebar{grid-column:1/-1;display:block;width:100%;height:5px;margin:0}
.combat-vital-row--shield{color:var(--brand-2)}
.combat-vital-row--shield .basebar__fill{background:var(--brand-2)}
.sf-protection{color:var(--brand-2);font:700 10px/1.4 var(--mono)}
#sfBossDock #sfTx .tx__hull{display:flex;gap:12px;flex-wrap:wrap}
.tx-vital{display:block;min-width:90px;color:var(--brand);font:10px var(--mono)}
.tx-vital b{font:inherit}.tx-vital--shield{color:var(--brand-2)}
.tx-vital i{display:block;height:4px;margin-top:3px;background:linear-gradient(to right,currentColor calc(var(--v)*100%),#ffffff22 0)}
@media(max-width:640px){
  .sf-root #sfHud{width:166px;top:6px;left:6px;gap:6px;padding:8px!important}
  .sf-boss-dock .boss-intent{font-size:.58rem}
  #sfBossDock #sfTx .tx__text{max-height:106px;font-size:15px}
  #sfBossDock #sfTx>.tx{padding:7px 9px}
}
@media(max-height:740px){
  #sfBossDock #sfTx .tx__text{max-height:88px}
  .sf-boss-dock .sf-mechanic details:not([open]){display:none}
}
@media(prefers-reduced-motion:reduce){.focus-vital i{transition:none}}
