/* ============================================================================
   memelord.game — styles.css (shell)
   ============================================================================
   Source split into public/styles/*.css on 2026-05-05 (X53). This file is now
   a thin @import chain — order matters for cascade. Edit individual sections
   in their own file, not here. See public/styles/README.md for the per-file
   topic map and contribution rules.

   Note on @import perf: each import is a sequential round trip. With HTTP/2
   multiplexing on Render this is acceptable for a single-page-load app, but
   if first-paint becomes a concern, the right next move is a build step that
   concatenates the imports into a single bundled file (we don't have one yet).
   ========================================================================== */


/* === inlined: styles/00-base.css === */
/* ==========================================================================
   memelord.game — shared styles (Memelord.game 95/98 theme)
   Matches memelord.com 1:1: #C0C0C0 desktop gray chrome, #02007F title-bar
   navy, #FFD700 gold accents, W95FA bitmap display font, sharp square
   corners and 2px hand-drawn bevels. Class names intentionally match older
   markup so existing HTML keeps working — only the paint changes.

   Everything below this :root block still works because every component
   references the CSS variables. Retoken = restyle.
   ========================================================================== */

/* memelord.com uses W95FA for chrome (buttons, headings, windows) and Geist
   for body text. We match that exact pair. Lato is kept as a hard fallback
   for systems that can't reach Google Fonts. */
@import url('https://fonts.googleapis.com/css2?family=Anton&family=Geist:wght@100..900&family=Lato:wght@300;400;700;900&display=swap');

/* W95FA — the brand pixel/display font. Self-hosted so the look never depends on the
   (flakier) cdnfonts CDN. font-display:swap means text shows in the fallback instantly and
   upgrades when W95FA arrives — it never blocks render or goes invisible. src order:
   local install → our self-hosted /fonts/w95fa.woff (delivered via the downloads manifest;
   the CSS bundler auto-prefixes /fonts under the base path) → cdnfonts as a last-ditch
   fallback during the deploy window. Remove the cdnfonts src once /fonts/w95fa.woff is live. */
@font-face {
  font-family: 'W95FA';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: local('W95FA'),
       url('/mwa/fonts/w95fa.woff') format('woff'),
       url('https://fonts.cdnfonts.com/s/45697/W95FA.woff') format('woff');
}

:root {
  /* ---------- Official memelord.com Win95 palette (raw-HTML confirmed) ----------
     #C0C0C0  window chrome / dominant surface
     #DFDFDF  bevel highlight (inner)
     #F0F0F0  bevel highlight (outer)
     #7F7F7F  bevel shadow (inner)
     #A0A0A0  bevel shadow (outer)
     #02007F  title-bar navy  (classic Win95 title bar)
     #FFD700  accent gold / crown
     #262626  body ink                                                       */
  --ml-gray:          #C0C0C0;
  --ml-gray-hi:       #DFDFDF;
  --ml-gray-hi-2:     #F0F0F0;
  --ml-gray-sh:       #7F7F7F;
  --ml-gray-sh-2:     #A0A0A0;
  --ml-navy:          #02007F;     /* was #0a2a62 — now matches memelord.com */
  --ml-navy-dark:     #01005a;
  --ml-navy-hi:       #3a38b3;
  --ml-gold:          #FFD700;
  --ml-ink:           #262626;

  /* ---------- Back-compat aliases so 1000+ lines below still paint ----------
     Every component here references these vars; we just rewire them. */
  --ml-blue:          var(--ml-navy);            /* was #627CBB XP Luna */
  --ml-blue-dark:     var(--ml-navy-dark);
  --ml-blue-hi:       var(--ml-navy-hi);
  --ml-blue-edge:     #01005a;
  --ml-yellow:        var(--ml-gold);            /* was #E9BB32 */
  --ml-yellow-dark:   #c9ab00;
  --ml-yellow-hi:     #fff27a;
  --ml-green:         #00a043;                   /* punchier, 95-appropriate */
  --ml-green-dark:    #007a33;
  --ml-green-hi:      #40d074;
  --ml-red:           #c60c30;
  --ml-red-dark:      #8d0022;

  /* ---------- Neutral / grey scale ----------
     All warm creams are OUT. Memelord.com is cold-gray only. */
  --face:          var(--ml-gray);           /* was #f2f0e8 */
  --face-2:        var(--ml-gray-hi);        /* was #e6e2d2 */
  --edge:          var(--ml-gray-sh);        /* was #7b7967 */
  --edge-dark:     #262626;
  --hi:            var(--ml-gray-hi-2);
  --text:          var(--ml-ink);
  --title-text:    #ffffff;
  --muted:         #3a3a3a;
  --danger:        var(--ml-red);

  /* ---------- Legacy accent aliases (existing markup keeps working) ---------- */
  --ml-pink:          #d148d1;
  --ml-cyan:          #6bc5d1;
  --accent:           var(--ml-green);
  --accent-2:         var(--ml-gold);
  --accent-3:         var(--ml-navy);
  --good:             var(--ml-green);
  --bg:               var(--face);
  --bg-2:             var(--face-2);
  --card:             var(--face);
  --card-2:           var(--face-2);
  --radius:           0;                         /* was 3px — memelord is square */

  /* ---------- Type ----------
     memelord.com literally ships W95FA as its display font. We follow. */
  --f-pixel:   'W95FA', 'Chicago', 'MS Sans Serif', 'Courier New', monospace;
  --f-display: 'W95FA', 'Lato', 'Arial Black', 'Helvetica Neue', sans-serif;
  /* memelord.com body font is Geist. We ship it first, with Lato/system as
     a fallback chain so copy always has a readable sans. */
  --f-body:    'Geist', 'Lato', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --f-caption: 'W95FA', 'Impact', 'Arial Black', sans-serif;

  /* ---------- Shadows ---------- */
  --shadow-win:   0 10px 24px rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.25);
  --shadow-card:  0 6px 16px rgba(0, 0, 0, 0.30);

  /* ---------- Back-compat Luna aliases still referenced in the file.
       Re-aliased to brand values so nothing else needs touching. ---------- */
  --luna-blue-1:     var(--ml-blue);
  --luna-blue-2:     var(--ml-blue);
  --luna-blue-3:     var(--ml-blue-dark);
  --luna-blue-dark:  var(--ml-blue-edge);
  --luna-blue-edge:  var(--ml-blue-edge);
  --luna-blue-hi:    var(--ml-blue-hi);
  --luna-green-1:    var(--ml-green);
  --luna-green-2:    var(--ml-green-dark);
  --luna-grey-face:  var(--face);
  --luna-grey-btn:   #ffffff;
  --luna-grey-btn-2: var(--face-2);
  --luna-grey-edge:  var(--edge);
  --luna-grey-hi:    var(--hi);
  --luna-text:       var(--text);
  --luna-title-text: var(--title-text);
}

* { box-sizing: border-box; }


/* === inlined: styles/01-style-guard.css === */
/* ============================================================================
   STYLE GUARD 窶・FULL-BLEED XP BLISS BODY BACKGROUND
   ============================================================================
   This block is the source of the "background keeps disappearing on every
   other deploy" regression. DO NOT rewrite body background to white or
   transparent. DO NOT claim "memelord.com body is pure white." That's false.

   Per MEMELORD_DESIGN_BIBLE.md ﾂｧ178 "The chrome / XP styling pattern":
     1. Full-bleed `body` background: XP Bliss (`public/img/prop-xp-bliss.jpg`),
        background-size: cover.

   Implementation: we layer the jpg FIRST, with a CSS gradient fallback
   underneath that paints the same blue-sky 竊・green-hills silhouette so the
   page is never bare white even when the jpg is missing or 404s. Keep both.

   There is a guard test (tests/test-style-guard.js) that will FAIL npm test
   if this block gets reverted to transparent/white. Don't silence the test,
   fix the styles.
   ============================================================================ */
html {
  margin: 0;
  padding: 0;
  min-height: 100vh;
  /* gradient lives on html so if body image fails entirely, sky still shows */
  background:
    linear-gradient(180deg,
      #3a87d9 0%,      /* deep sky */
      #6fb2ea 38%,     /* horizon haze */
      #92c650 52%,     /* hill crest */
      #4c9234 70%,     /* grass light */
      #2d6b1e 100%     /* grass deep */
    ) fixed;
  background-size: cover;
}
body {
  margin: 0;
  padding: 0;
  font-family: var(--f-body);
  font-size: 13px;                 /* XP default UI size */
  color: var(--luna-text);
  min-height: 100vh;
  overflow-x: hidden;
  /* XP Bliss image on body so if the asset ships later, it takes priority
     automatically. If it 404s, we fall through to the html gradient above. */
  background:
    url('/mwa/img/prop-xp-bliss.jpg') center center / cover fixed no-repeat,
    transparent;
}

/* 笏笏 ACCESSIBILITY: KEYBOARD FOCUS RINGS 笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏
   Visible focus indicators for keyboard navigation (WCAG 2.1 Level AA).
   Uses :focus-visible to avoid showing outlines for mouse clicks while
   ensuring keyboard users always see where they are. Focus ring is a 2px
   solid border with a slight offset, matching Win95 UI conventions. */
button:focus-visible,
a:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
[role="button"]:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid #000;
  outline-offset: 2px;
}

/* ============================================================================
   DESKTOP-LANE 窶・site-wide 660px breadcrumb
   ----------------------------------------------------------------------------
   Current state (X79-2026-05-08): unified desktop lane is 660px. Captain QA
   on the X78 720 lane (shipped earlier the same day): "let's try 660px on
   everything." Tighter than the X64-era 720 card cap; sits between the
   prior 560 (X64 .container narrow) and the 720 cap that's been the
   "feels right" historical reference. Mobile is unchanged 窶・every
   max-width below the @media (max-width: 640px) breakpoint, so phones still
   render 100% of their viewport width minus padding.

   If you're here to swap 660 竊・something else, here's the inventory:

     CSS files:
       public/styles/01-style-guard.css   (.container, .player-container,
                                            .container > .card cap,
                                            .container > [id^=view-] > .card cap)
       public/styles/10-landing.css       (.hero-art width)
       public/styles/11-site-header.css   (.page max-width)
                                          (.site-header itself spans the
                                           viewport 窶・chrome row, NOT in
                                           the 660 lane. X138 mistakenly
                                           added it; reverted in X148.)
       public/styles/19-auth-pages.css    (.subscribe-hero max-width)
       public/styles/21-phase-heroes.css  (.phase-hero max-width)
                                          (variants -tall/-crowned/-mini stay)

     JS-injected styles:
       public/situation-monitor.js        (.sit-monitor-frame max-width)

     Inline overrides:
       public/host.html                   (5 inline max-width attrs in #view-lobby)
       public/spectator.html              (4 inline max-width attrs across
                                            #view-join + #view-lobby)
       public/admin.html                  (1 inline max-width on #editModal card)

     Tests + docs:
       tests/test-style-guard.js          (regex accepts 500窶・99 窶・660, 720,
                                            and 880 all stay green; comment
                                            refresh optional)
       docs/design/03-technical-guardrails.md
       docs/design/04-style-changelog.md  (append the next change as a new
                                            entry, don't rewrite X65 / X78 / X79)

   Single grep: `grep -rn "660" public/ tests/ docs/design/` then filter to
   the files above. The phase-hero -crowned variant intentionally stays
   at 560 (T6-2026-04-30 spec), so don't sweep that one.

   History: 720 (X64-era card cap, .container 560) 竊・880 (X64+X65 same-day
   sweep) 竊・720 (X78-2026-05-08 revert) 竊・660 (X79-2026-05-08 narrow).
   ============================================================================ */

/* 2026-05-08 (X79): unified desktop lane narrowed 720 竊・660. Captain QA
   on the X78 720 lane: "let's try 660 px on everything." Mobile is
   unchanged. See the breadcrumb block above for the full inventory.

   ---- prior history ----

   2026-05-08 (X78): unified desktop lane reverted 880 竊・720. Captain QA
   on the 880 lane shipped by X65: "feels too wide on desktop." 720 is
   the value the .card cap held through X64; the X65 thread already
   pre-wrote a revert plan for this exact case (see the breadcrumb
   block above for the full inventory). Mobile is unchanged.

   ---- prior history ----

   2026-05-05 (X64): .container bumped 560px 竊・880px on desktop. The
   prior 560px was a recent narrowing that left desktop showing a tiny
   phone-width card stranded in a sea of XP Bliss wallpaper 窶・Captain
   QA after a friend reaction: "looks way cooler on mobile, didn't
   really understand desktop." 880px was roughly halfway between the
   phone-narrow 560px and the doc's historical 1100px claim.

   2026-05-05 (X65 follow-up): .player-container ALSO bumped 560 竊・880
   on desktop, and the .container > .card size-parity cap bumped
   720 竊・880 to match. Captain QA after X64 shipped: "the spectator
   page and the player page didn't get the change 窶・every single page
   without exception should be 880 pixels wide on desktop." Single
   canonical desktop lane was 880 from X65 竊・X78. See docs/design/
   03-technical-guardrails.md ﾂｧSize parity for the full rationale and
   docs/design/04-style-changelog.md (X65 + X78 entries) for framing. */
/* X86-2026-05-08: `.player-container` padding bumped 16px 竊・24px so
   its content area matches `.container` exactly. Captain QA: "the
   screens are still all different widths. Spectator is narrower than
   player, which is wider than host (which matches marketer mode)."
   With `box-sizing: border-box` on the global reset and `max-width:
   660px` on both selectors, the OUTER edge of these containers is
   the same 660px 窶・but inner cards (capped at 660 themselves) get
   clipped to the parent's content area. At 24px-padding the content
   area is 612px; at 16px-padding it was 628px. That 16px asymmetry
   was the visible "player wider than host" drift. Mobile is
   unchanged 窶・`17-mobile-tweaks.css` already unifies all three
   containers to 12px / 8px below the 640px / 375px breakpoints. */
.container        { max-width: 660px; margin: 0 auto; padding: 24px; }
.player-container { max-width: 660px; margin: 0 auto; padding: 24px; }

/* 笏笏 SIZE-PARITY GUARD 笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏
   Cards inside .container (host.html, spectator.html, /marketer) cap at
   660px so they fill the matching .container / .player-container lane
   on desktop without clipping back to a narrower widget. The cap exists
   so the same shared widget (submit stage, vote grid, situation monitor,
   scoreboard) renders at the same width on host TV vs. player phone.

   History: 720 (early host-TV-vs-phone parity through X64) 竊・880 (X65
   extended the unified desktop lane to .player-container, so the cap
   bumped to match) 竊・720 (X78-2026-05-08, Captain QA "feels too wide"
   on the X65 880 lane) 竊・660 (X79-2026-05-08, Captain QA "let's try 660
   px on everything").

   Cards that legitimately need an unbounded width (academy-scene
   victory screen) opt out with `class="card wide"`. With the cap at
   660 = container width, the wide opt-out is largely a no-op now, but
   keeping the selector documented for cards that might escape the
   container's padding via negative margins.

   Guarded by tests/test-style-guard.js 窶・removing this rule fails
   `npm test`. The test regex matches max-width values 500窶・99px, so
   the X65 竊・X78 竊・X79 round-trip stays green at every step. See
   docs/design/03-technical-guardrails.md ﾂｧ"Size parity" for the
   rationale and the avatar-picker / submit-stage precedent that
   established this pattern. */
.container > .card                 { max-width: 660px; margin-left: auto; margin-right: auto; }
.container > .card.wide { max-width: none; }
/* 2026-08-04: keep MISSION COMPLETE inside the same 660 lane as every other card */
.container > #academy-scene  { max-width: 660px; margin-left: auto; margin-right: auto; }

/* View-wrapped cards (host.html lobby / submit / vote / score / end).
   The size-parity rule above uses the child combinator and so doesn't
   reach into <div id="view-*"> wrappers, which left the lobby QR card and
   every other host card stuck to the left edge of the container.
   Same 660px cap + horizontal centering as direct .container > .card.
   Captain regression: 2026-04-27 host TV. The .wide opt-out mirrors the
   one above for the end-screen leaderboard. The academy hero / victory
   screen wraps are NOT .card so they aren't affected by the cap, but
   their own internal centering already takes care of layout. */
.container > [id^="view-"] > .card { max-width: 660px; margin-left: auto; margin-right: auto; }
.container > [id^="view-"] > .card.wide { max-width: none; }


/* === inlined: styles/02-typography.css === */
/* -----------------------------
   Typography — memelord.com brand rules:
   H1: Lato Heavy 24pt  (~1.7rem)
   H2: Lato Heavy 20pt  (~1.4rem, also used for card title bars)
   P : Lato Light  14pt (~0.95rem)
   ----------------------------- */
h1, h2, h3 {
  margin: 0 0 10px;
  line-height: 1.1;
  font-family: var(--f-display);
  font-weight: 900;                /* Lato Heavy */
  letter-spacing: 0;
  color: var(--text);
}
h1 { font-size: clamp(1.6rem, 4.5vw, 1.7rem); }
h2 { font-size: 1.4rem; }
h3 { font-size: 1.1rem; }
p  {
  margin: 0 0 8px;
  color: var(--muted);
  font-family: var(--f-body);
  font-weight: 300;                /* Lato Light */
  font-size: 14px;
  line-height: 1.5;
}

a { color: var(--ml-blue-edge); text-decoration: underline; }
a:hover { color: var(--ml-blue); }


/* === inlined: styles/03-chrome-primitives.css === */
/* -----------------------------
   XP icons (kept from prior pass)
   ----------------------------- */
.xp-icon {
  display: inline-block;
  width: 1.4em;
  height: 1.4em;
  vertical-align: -0.28em;
  margin-right: 0.45em;
  image-rendering: -webkit-optimize-contrast;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.30));
}
.xp-icon-tiny {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.18em;
  margin-right: 0.25em;
}
.xp-icon-lg {
  display: inline-block;
  width: 2em;
  height: 2em;
  vertical-align: -0.5em;
  margin-right: 0.5em;
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.35));
}

/* -----------------------------
   Logo — memelord signature over Bliss
   ----------------------------- */
.logo {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--f-pixel);
  font-size: 1rem;
  font-weight: 400;
  letter-spacing: 0.02em;
  color: #ffffff;
  text-shadow:
    0 0 10px rgba(0, 252, 157, 0.55),
    2px 2px 0 #000;
  text-transform: lowercase;
}
.logo::before {
  /* X65 follow-up (Captain, two passes): original was a 1.6s
     100%↔55% opacity + 1↔0.88 scale jitter that contributed to the
     "too much flickering" feel. Calmed first to a 6s breath, then
     (Captain second pass) lengthened to 15s. The dot now sits at 85%
     opacity for ~94% of each cycle and brightens briefly to full at
     the 96% mark, matching the chyron pulse cadence. No scale change. */
  content: "";
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--ml-green);
  box-shadow:
    0 0 0 2px #000,
    0 0 12px var(--ml-green);
  opacity: 0.85;
  animation: neonPulse 15s ease-in-out infinite;
}
.logo .dot { color: var(--ml-pink); }
@keyframes neonPulse {
  0%, 94%, 100% { opacity: 0.85; }
  96%           { opacity: 1.00; }
}
@media (prefers-reduced-motion: reduce) {
  .logo::before { animation: none; opacity: 0.85; }
}

/* -----------------------------
   Card — the Luna XP window
   Blue gradient title bar (via h2), beige body, chunky 3D border.
   The card is padded so body content sits inside the window; when the
   first child is an <h2> it negative-margins out to the chrome edge to
   become the title bar, and re-establishes a 12px gap below itself.
   ----------------------------- */
.card {
  position: relative;
  background: var(--luna-grey-face);
  border: 1px solid var(--luna-blue-edge);
  border-radius: 8px 8px 3px 3px;
  padding: 14px 16px;
  margin-bottom: 18px;
  box-shadow:
    inset 0 0 0 1px var(--luna-blue-hi),
    var(--shadow-win);
  color: var(--luna-text);
  overflow: hidden;
}
/* First <h2> in a card becomes a memelord.com-style title bar: flat
   brand blue (#627CBB) with Lato Heavy text, bleeds to window edges. */
.card > h2:first-child {
  margin: -14px -16px 12px;
  padding: 8px 14px 9px;
  font-family: var(--f-display);
  font-size: 1.05rem;              /* ≈ Lato Heavy 20pt scaled */
  font-weight: 900;                /* Lato Heavy */
  letter-spacing: 0.005em;
  text-transform: none;
  color: var(--title-text);
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.30);
  background: var(--ml-blue);
  border-bottom: 2px solid var(--ml-blue-edge);
  display: flex;
  align-items: center;
  gap: 8px;
  line-height: 1.15;
}

/* -----------------------------
   Buttons — Luna style
   Grey gradient with subtle 3D bevel; hover brightens, active insets.
   Primary (.btn) is the Luna "Start" green so memelord's neon intent
   survives without looking like Vista.
   ----------------------------- */
.btn {
  font-family: var(--f-display);
  font-weight: 900;                /* Lato Heavy */
  font-size: 14px;
  letter-spacing: 0.01em;
  text-transform: none;
  padding: 9px 18px;
  /* X164-2026-06-14: 44px minimum tap target (most players are on phones).
     Native <button> centers its label vertically within the min-height. */
  min-height: 44px;
  border: 1px solid var(--ml-green-dark);
  border-radius: 4px;
  background: var(--ml-green);     /* flat memelord brand green */
  color: #ffffff;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25);
  cursor: pointer;
  width: 100%;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.30),
    inset 0 -1px 0 rgba(0, 0, 0, 0.18),
    0 2px 4px rgba(0, 0, 0, 0.25);
  transition: filter 0.1s, transform 0.06s;
}
.btn:hover       { filter: brightness(1.08) saturate(1.05); }
.btn:active      { transform: translateY(1px); filter: brightness(0.95); box-shadow: inset 0 2px 2px rgba(0,0,0,0.20), 0 1px 2px rgba(0,0,0,0.2); }
.btn:disabled    { opacity: 0.55; cursor: not-allowed; filter: grayscale(0.6); transform: none; }

/* Secondary — memelord brand blue */
.btn-secondary {
  background: var(--ml-blue);
  color: #fff;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25);
  border: 1px solid var(--ml-blue-edge);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.30),
    inset 0 -1px 0 rgba(0, 0, 0, 0.18),
    0 2px 4px rgba(0, 0, 0, 0.25);
}

/* Accent — memelord brand yellow */
.btn-accent2 {
  background: var(--ml-yellow);
  color: #111;
  text-shadow: none;
  border: 1px solid var(--ml-yellow-dark);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    inset 0 -1px 0 rgba(0, 0, 0, 0.15),
    0 2px 4px rgba(0, 0, 0, 0.25);
}

.btn-danger {
  background:
    linear-gradient(180deg, #e56a6a 0%, #c42021 45%, #8d1718 100%);
  color: #fff;
  border: 1px solid #6a1011;
}

.btn-sm {
  padding: 4px 10px;
  font-size: 12px;
  width: auto;
}

/* -----------------------------
   Form controls — XP 3D inset bevel
   ----------------------------- */
input, textarea, select {
  font-family: var(--f-body);
  font-size: 13px;
  width: 100%;
  padding: 6px 8px;
  border-radius: 0;
  border: 1px solid #7b7967;
  border-top-color: #424242;
  border-left-color: #424242;
  background: #ffffff;
  color: #000;
  outline: none;
  box-shadow: inset 1px 1px 0 #dcdcdc;
  transition: border-color 0.1s, box-shadow 0.1s;
}
/* 2026-04-28: contrast fix — changed placeholder color from #888 (3.54:1)
   to #555555 (7.46:1) to meet WCAG AAA 7:1 standard. */
input::placeholder, textarea::placeholder { color: #555555; }
/* 2026-05-08 (X100): Captain feedback — the silly meme-phrase
   placeholders on the join + host-name fields ("BUTT" / "CHEEK" /
   "LITTLEFOOT'S MOTHER") were eating brain cycles for ~3 seconds
   before players realized they weren't real values. The high AAA
   contrast above is what made them legible AND what made them read
   as live content. These four fields specifically need the opposite
   signal: italic + lighter gray so the eye reads "non-content"
   immediately. WCAG explicitly exempts decorative placeholder text
   from contrast minimums (the input itself has visible structure),
   so the lower contrast here is intentional. The rest of the site
   keeps #555555 — only the join screen has the room-code-shape
   confusion this fix addresses. */
#joinCode::placeholder,
#joinName::placeholder,
#specName::placeholder,
#hostName::placeholder {
  color: #9a9a9a;
  font-style: italic;
}
input:focus, textarea:focus, select:focus {
  border-color: #0058ee;
  box-shadow: inset 1px 1px 0 #dcdcdc, 0 0 0 2px rgba(48, 129, 239, 0.25);
}

/* X handle input on marketermode.html — the @ is part of the input value
   itself (auto-pinned by marketermode.html's input/focus handlers), so the
   field is just a regular <input> with no separate prefix box. We
   keep an `.invalid` modifier here that the click-time validator
   toggles on "Open the Studio" — error state is strictly click-time,
   never live as the user types. */
.x-handle-input.invalid {
  border-color: #c4302b;
  box-shadow: inset 1px 1px 0 #dcdcdc, 0 0 0 2px rgba(196, 48, 43, 0.18);
}
.x-handle-error {
  margin: 4px 0 0;
  color: #c4302b;
}
select {
  appearance: none;
  -webkit-appearance: none;
  background-color: #fff;
  background-image:
    linear-gradient(45deg, transparent 50%, #333 50%),
    linear-gradient(135deg, #333 50%, transparent 50%);
  background-position: calc(100% - 14px) 55%, calc(100% - 10px) 55%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 28px;
}

.toggle {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 6px 12px;
  background: var(--luna-grey-btn);
  border: 1px solid #7b7967;
  border-radius: 3px;
  color: #000;
  font-family: var(--f-body);
  cursor: pointer;
}
.toggle input[type="checkbox"] {
  width: auto;
  margin: 0;
  accent-color: var(--luna-blue-1);
  box-shadow: none;
}


/* === inlined: styles/04-room-elements.css === */
/* -----------------------------
   Room code — keep pixel neon so it pops inside the Luna window
   ----------------------------- */
.room-code {
  font-family: var(--f-pixel);
  font-size: clamp(3.5rem, 10vw, 6rem);
  font-weight: 700;
  letter-spacing: 0.18em;
  text-align: center;
  color: var(--ml-green-dark);
  padding: 18px 0;
  text-shadow:
    0 0 10px rgba(0, 181, 112, 0.40),
    2px 2px 0 #fff,
    3px 3px 0 #222;
}

/* -----------------------------
   Player pills + grid
   ----------------------------- */
.players-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 10px;
}
.player-pill {
  background:
    linear-gradient(180deg, #ffffff 0%, #e9e6d4 100%);
  border: 1px solid #7b7967;
  padding: 8px 12px;
  border-radius: 3px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* 2026-07-07 (audit): breathing room so a long wrapped bot name
     ("EDGELORD-IN-CHIEF") never touches the ✕ remove button. */
  gap: 8px;
  font-family: var(--f-body);
  font-weight: 700;
  color: #000;
  box-shadow: inset 0 1px 0 #fff;
}
.player-pill.submitted {
  background: linear-gradient(180deg, #8cf5c3 0%, var(--ml-green) 100%);
  color: #000;
  border-color: #1e8554;
}
.player-pill.voted {
  background: linear-gradient(180deg, #b3fbff 0%, var(--ml-cyan) 100%);
  color: #000;
  border-color: #1687a0;
}

/* Avatar + name share a flex row so a wrapping long name keeps its
   second line aligned with the first line of text instead of flowing
   back under the avatar. .pill-avatar stays as the fixed-width left
   item (flex: 0 0 auto in styles/12-avatars.css); .player-pill-name
   is the flexible column that owns the wrap. min-width: 0 on both the
   row and the name column is required so flex children can shrink
   below their content size and wrap normally. overflow-wrap: anywhere
   lets very long unbroken handles break mid-word as a last resort
   (the standard case "EDGELORD-IN-CHIEF" already breaks at hyphens).
   Authored 2026-05-08 / X80 in response to Captain's wrap-under-avatar
   complaint on the host lobby roster. */
.player-pill > .player-pill-info {
  display: flex;
  align-items: center;
  min-width: 0;
  flex: 1 1 auto;
}
.player-pill-name {
  min-width: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* "+ Add Bot" tile inside the lobby players-grid — same outer
   dimensions and border treatment as .player-pill so the row never
   has mismatched-width edges, but visually flagged as an action via
   a brighter button-style background and an uppercase label. Lives
   as the last tile in the grid (see host.html lobby card); pairs
   with the Host pill in an empty room and slides to the end as
   bots/humans fill in. The :hover state warms toward the player-pill
   gradient so it reads as "this becomes a player when clicked." */
.player-pill-add-bot {
  background: linear-gradient(180deg, #f4f4f4 0%, #d4d4d4 100%);
  border: 1px solid #7b7967;
  border-radius: 3px;
  padding: 8px 12px;
  font-family: var(--f-body);
  font-weight: 700;
  font-size: inherit;
  color: #444;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  cursor: pointer;
  box-shadow: inset 0 1px 0 #fff;
  transition: background 0.1s, color 0.1s;
}
.player-pill-add-bot:hover {
  background: linear-gradient(180deg, #ffffff 0%, #e9e6d4 100%);
  color: #000;
}
.player-pill-add-bot:active {
  box-shadow: inset 1px 1px 0 #b0b0b0;
}
.player-pill-add-bot:focus-visible {
  outline: 2px solid #0058ee;
  outline-offset: 1px;
}

/* -----------------------------
   Hand picker (Cards-Against-Humanity-style template hand)
   -----------------------------
   Rendered by renderHandPicker() in public/player.js. The function used
   to ship inline cssText for these classes; lifting here keeps the rules
   on one stylesheet so the design bible owns the visual contract.
   `--accent` resolves to memelord brand green; the navy fallback
   matches the value the inline CSS used to hardcode. */
.hand-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 10px;
  margin-top: 8px;
}
.hand-card {
  padding: 6px;
  border: 2px solid var(--accent, #02007F);
  background: transparent;
  cursor: pointer;
  text-align: center;
  transition: all 0.15s ease-out;
  position: relative;
}
/* Hover effect: emphasize interactivity and strategic choice */
.hand-card:hover {
  transform: scale(1.05);
  box-shadow: 0 0 0 2px var(--accent, #02007F) outset;
}
/* Picked card: bold visual indication of selection */
.hand-card.picked {
  box-shadow: 0 0 0 3px var(--accent, #02007F) inset;
  background: rgba(2, 0, 127, 0.05);
  font-weight: 600;
}
/* Picked card hover: still interactive but with selected state */
.hand-card.picked:hover {
  box-shadow: 0 0 0 3px var(--accent, #02007F) inset, 0 0 8px rgba(2, 0, 127, 0.3);
}
.hand-card img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: 2px;
}
/* Note: the in-card `.small.muted` meme-name label rule was removed
   when submit-stage.js stopped rendering the visible label (the format
   name is now exposed only via img.alt + button.title for a11y/hover).
   Kept this comment so future edits don't reintroduce on-card text. */

/* -----------------------------
   Hand picker — fanned layout
   -----------------------------
   2026-05-01 (Captain feedback): the bare 5-tile auto-fill grid wasn't
   legible at a glance — players couldn't tell that the images were
   "their hand" of memes vs five unrelated images the game was showing.
   This block adds:
     1. `.hand-stage`         — wrapper that owns the card-sizing vars.
     2. `.hand-grid.hand-fan` — modifier that re-roles the grid as a
        fanned row of cards (rotation + overlap, rightmost on top).
   Per-card rotation is data-driven via the `--fan-angle` and
   `--fan-z` custom properties set by submit-stage.js renderHandPicker.
   The base `.hand-grid` rule above stays as a fallback so any future
   surface that wants the old grid look can just omit `.hand-fan`.
   (A `.hand-picker-header` banner shipped briefly alongside this block
    on 2026-05-01 and was removed the same day — Captain felt the fan
    visual alone was enough; don't reintroduce a separate text label.) */

.hand-stage {
  /* Card width clamps so the fan fits ~340px phone width (5 cards at
     ~74px each with ~36% overlap = ~268px span) and grows on tablet
     up to a 110px ceiling. Negative-margin overlap is a percentage of
     the card width so the math holds at any clamp value.
     2026-07-07: this clamp is now only the no-JS FALLBACK — it was tuned
     for the 5-card deal and overflowed the stage on 8–12 card hands
     (end cards clipped past the container edge = untappable; Captain
     screenshot). renderHandPicker (submit-stage.js) measures the mount
     and sets inline --card-w AND --card-overlap solved from the REAL
     card count (overlap deepens past -36% only when a big hand hits the
     56px width floor on a narrow screen). Keep the fractions in sync:
     -36% here ⇄ 0.64 visible-strip default in the JS solver. */
  --card-w: clamp(74px, 22%, 110px);
  --card-overlap: calc(var(--card-w) * -0.36);
  display: flex;
  justify-content: center;
  padding: 18px 4px 8px;
  margin-top: 4px;
  /* Reserve vertical room so a hovered/picked card can lift up out of
     the fan without bumping into the situation monitor above. */
  min-height: calc(var(--card-w) * 1.4 + 28px);
  overflow: visible;
}

.hand-grid.hand-fan {
  display: inline-flex;
  align-items: flex-end;
  justify-content: center;
  flex-wrap: nowrap;
  gap: 0;
  margin-top: 0;
}

.hand-grid.hand-fan .hand-card {
  width: var(--card-w);
  flex: 0 0 var(--card-w);
  margin: 0;
  padding: 4px;
  background: #fff;
  border: 2px solid var(--accent, #02007F);
  border-radius: 4px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.18);
  transform-origin: 50% 120%;
  transform: rotate(var(--fan-angle, 0deg));
  transition: transform 0.18s ease-out, box-shadow 0.18s ease-out;
  z-index: var(--fan-z, 1);
  position: relative;
}
.hand-grid.hand-fan .hand-card + .hand-card {
  margin-left: var(--card-overlap);
}

/* Hover / focus: card flips flat, lifts above the fan, and grows a
   touch so the player can read the full image before clicking. The
   z-index jump puts it on top of every neighbour regardless of fan
   stack order. */
.hand-grid.hand-fan .hand-card:hover,
.hand-grid.hand-fan .hand-card:focus-visible {
  transform: rotate(0deg) translateY(-14px) scale(1.06);
  z-index: 50;
  box-shadow: 0 10px 16px rgba(0, 0, 0, 0.28);
  outline: none;
}

/* Picked card: stays flat + lifted with the brand-accent halo. Defined
   AFTER :hover so the picked state wins on the brief frame between
   click and the caption-stage rebuild. */
.hand-grid.hand-fan .hand-card.picked {
  transform: rotate(0deg) translateY(-18px) scale(1.06);
  z-index: 60;
  box-shadow:
    0 0 0 3px var(--accent, #02007F) inset,
    0 12px 18px rgba(0, 0, 0, 0.32);
  background: rgba(2, 0, 127, 0.05);
}

/* ── Phone reaction-hand: bigger, upright, ~two rows ──────────────────────
   cowork-mlg-mobile-polish-b-2026-07-08. Captain's 390px phone audit: the
   10-card fan (renderHandPicker shrinks --card-w to ~63px so all 10 cram into
   one overlapping row) gives tiny tap targets. At phone width, drop the single
   fan for upright ≥84px cards that wrap into ~two rows. Preview→confirm already
   guards mis-taps; this just makes the FIRST tap comfortable. Desktop keeps the
   fan untouched. A light trailing overlap (margin-right, not a leading
   margin-left) means a wrapped row's first card carries no leading offset, so
   each row centers cleanly via justify-content (a partial last row — hands run
   8–12 cards — sits centered under the full rows). This lets ~5 cards still fit
   one 390px row, so the hand reads as ~two rows rather than four. Overrides the
   JS-set --card-w/--card-overlap by setting
   width/margin directly on the card (the inline vars live on .hand-stage). */
@media (max-width: 560px) {
  .hand-stage {
    min-height: 0;
    padding: 12px 6px 8px;
  }
  .hand-grid.hand-fan {
    /* inline-flex (desktop) shrinks to content, so flex-wrap never fires;
       switch to a full-width block flex so the row actually wraps on phones. */
    display: flex;
    width: 100%;
    box-sizing: border-box;
    flex-wrap: wrap;
    justify-content: center;
    row-gap: 12px;
    padding-right: 18px; /* absorbs the last card's trailing overlap */
  }
  .hand-grid.hand-fan .hand-card {
    width: 84px;
    flex: 0 0 84px;
    margin: 0 -18px 0 0;  /* light rightward overlap; wraps ~5/row on a phone */
    transform: none;       /* upright, not fanned, on phones */
    z-index: auto;
    box-shadow: 0 3px 7px rgba(0, 0, 0, 0.2);
  }
  .hand-grid.hand-fan .hand-card + .hand-card {
    margin-left: 0;        /* replace the desktop fan's left-overlap */
  }
  .hand-grid.hand-fan .hand-card:hover,
  .hand-grid.hand-fan .hand-card:focus-visible,
  .hand-grid.hand-fan .hand-card.picked {
    transform: translateY(-6px) scale(1.05);
    z-index: 50;
  }
}

/* -----------------------------
   Spectator "waiting" dancing wojak (X164-2026-06-14)
   Shown on the spectator screen during a reaction SUBMIT so the watch view
   has some life while players throw cards down. Pure-CSS "dance" (bob + sway);
   static under prefers-reduced-motion.
   ----------------------------- */
.spec-waiting-wojak {
  display: block;
  width: 96px;
  height: auto;
  margin: 16px auto 4px;
  transform-origin: 50% 100%;
  animation: spec-wojak-dance 1.1s ease-in-out infinite;
}
@keyframes spec-wojak-dance {
  0%, 100% { transform: translateY(0) rotate(-5deg); }
  25%      { transform: translateY(-8px) rotate(3deg); }
  50%      { transform: translateY(0) rotate(5deg); }
  75%      { transform: translateY(-8px) rotate(-3deg); }
}
@media (prefers-reduced-motion: reduce) {
  .spec-waiting-wojak { animation: none; }
}


/* === inlined: styles/05-meme-stage.css === */
/* -----------------------------
   Meme stage + canvas
   ----------------------------- */
.meme-display { text-align: center; margin: 18px 0; }
.meme-image {
  max-width: 100%;
  max-height: 340px;
  border-radius: 2px;
  background: transparent;
  display: block;
  margin: 0 auto;
  border: 1px solid #7b7967;
  box-shadow:
    inset 0 0 0 1px #fff,
    0 6px 12px rgba(0, 0, 0, 0.25);
}
.canvasWrap { display: flex; justify-content: center; position: relative; }
.canvasWrap canvas, .canvasWrap img { max-width: 100%; height: auto; }

[data-memelord-image] {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: default;
  max-width: 100%;
  height: auto;
  display: block;
  border-radius: 2px;
}

/* Save / Share / Post toolbar — sits BELOW the meme image, inside the
   submission card. Big, high-contrast, impossible to miss, because drunk
   party players were walking right past the old 12px corner icons.

   X09-2026-05-08: Captain's call — the dashed-border "toolbar" frame
   and the Save / Share chips were reading as too heavy when the only
   two buttons in it are a curt "💾 Save" and "↗ Share" (X09 also
   shortened the Save label from "Save This Meme" → "Save"). Drop the
   dashed frame + pale background, switch wrap → nowrap so the two
   chips always sit on one line, and shrink the chip padding/font so
   the row sits politely under the meme without competing with it. */
.meme-actions {
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  justify-content: center;
  align-items: center;
  margin: 6px 0 2px 0;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
}
/* Save / Share chips. Sized down on 2026-05-01 and again on
   2026-05-08 (X09) — the old 14px / 140px-min toolbar buttons read as
   the loudest thing on the round-results card and were drowning out
   the meme + winner chip. Now they're compact pill chips that sit
   politely under the meme. The `.meme-post-btn` rule below is
   preserved verbatim for the flag-gated Post-to-X button (see
   compositor.js `SHOW_POST_TO_X`). */
.meme-action-btn {
  /* X135-2026-05-14: was `flex: 0 1 auto` + `min-width: 0` which let the
     button shrink below its content size — combined with `white-space:
     nowrap` that meant the SAVE / SHARE labels overflowed the button
     box when the row was crowded (Captain's screenshot: "the save and
     share on the right-hand meme is not even in the button"). Flipped to
     `flex: 0 0 auto` so each button stays exactly the size of its
     padding + label. With the VOTE chip retired in the same thread
     there's no greedy sibling eating the row width anymore, so this
     just works. */
  flex: 0 0 auto;
  /* X145-2026-05-14: explicit `width: auto` to override `.btn { width:
     100% }` from 03-chrome-primitives.css (Captain on the post-cascade
     view-victory-celebration screenshot: "The final screen needs to
     have a save and a share button, and they need to be in the style
     of the rest of the save and share buttons of the game."). The
     gigachad / final-scoreboard / academy share-corner chips carry
     `class="btn meme-action-btn …"`; without this override the .btn
     full-width rule stretched each chip to its row, so the post-cascade
     SAVE chip rendered as a wide gray button matching Play Again /
     Back to Home below it instead of the compact green chip the
     `.meme-save-btn` rule designed it to be. Specificity is equal
     between `.btn` and `.meme-action-btn` (0,1,0 each), so source
     order decides — 05-meme-stage.css loads after 03-chrome-primitives,
     so this `width: auto` wins. */
  width: auto;
  font-family: var(--f-display);
  font-weight: 900;                        /* Lato Heavy */
  font-size: 10px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  padding: 4px 9px;
  border-radius: 4px;
  cursor: pointer;
  transition: filter 0.1s, transform 0.06s;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.35),
              0 1px 2px rgba(0, 0, 0, 0.18);
  white-space: nowrap;
}
.meme-action-btn.meme-save-btn {
  background: var(--ml-green);             /* brand green — primary action */
  color: #fff;
  border: 1px solid #4a8a40;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25);
}
.meme-action-btn.meme-share-btn {
  /* X82-2026-05-08: yellow → navy. Same motivation as the X81 score-card
     palette sweep — yellow didn't match the rest of the meme-actions
     toolbar (green Save + the navy votes-badge already on the same card).
     Navy reads as a "secondary action" sitting next to the green primary,
     in the same Win95 chrome register as the votes-badge on the card
     header. */
  background: var(--ml-navy);
  color: #fff;
  border: 1px solid var(--ml-navy-dark);
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25);
}
.meme-action-btn.meme-post-btn {
  background: var(--ml-blue);              /* brand blue — social */
  color: #fff;
  border: 1px solid #3e5890;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3);
}
/* X151-2026-05-15: VOTE chip reinstated. Captain reversed the X135
   "no vote button — click the meme" decision in two terse lines:
     "We need three buttons: 1. Vote 2. Save 3. Share.
      Remove the 'Tap your favorite'."
   Three buttons sit on every vote card now. The chip's color must:
     - NOT be the X134 purple Captain flagged as "obnoxiously purple"
       (the magenta `--ml-pink` register is now reserved for the .armed
       confirm strip and shouldn't double-up on the action row).
     - NOT collide with Save's green or Share's navy / Post's blue,
       which would muddy the three-button row visually.
   Brand red (`--ml-red` = #c60c30) is the remaining primary slot:
   distinct, on-brand, reads as the call-to-action voting demands.
   Sized identically to Save / Share so the three chips look like a
   single row. */
.meme-action-btn.meme-vote-btn {
  background: var(--ml-red);
  color: #fff;
  border: 1px solid #8e0822;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.30);
}
/* X135-2026-05-14: VOTE chip retired. Captain on the screenshot:
   "Why is there a big vote button on one and not a vote button on the
    other? Why is the vote button kind of obnoxiously purple? There
    shouldn't be a vote button at all. You should just click on the meme
    you like, and then it has a verify to see if that's the meme you
    want to vote for."

   The vote affordance is now click-the-meme-image → card enters .armed
   state → an inline .vote-confirm-bar appears with Yes / Cancel. Style
   for that pattern lives below (.submission-card.armed +
   .vote-confirm-bar). Previous .meme-action-btn.meme-vote-btn rules
   (pink-magenta, flex-grow, oversized type) deleted — the chip no longer
   renders. */

/* X135-2026-05-14: armed card — user has clicked the meme image during
   the VOTE phase and is being asked to verify before the vote is cast.
   Visual treatment: cyan-pink ring matching the brand's "primary action"
   register without competing with the green Save chip's color. The
   .meme-actions toolbar hides while armed so the only visible CTA is
   the confirm strip; Save / Share come back as soon as the user dismisses
   the confirm. */
.submission-card.armed {
  outline: 3px solid var(--ml-pink, #d83adb);
  outline-offset: -1px;
  box-shadow: 0 0 0 6px rgba(216, 58, 219, 0.18);
  position: relative;
}
.submission-card.armed .meme-actions {
  display: none;
}

/* The confirm strip. Sits below the meme image where the .meme-actions
   toolbar normally lives (and replaces it visually while .armed is on
   the card). Prompt + two buttons on a single row at desktop widths;
   wraps to two rows on narrow phones via flex-wrap. */
.vote-confirm-bar {
  display: none;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: center;
  align-items: center;
  margin: 6px 0 2px 0;
  padding: 6px 8px;
  background: rgba(216, 58, 219, 0.08);
  border: 1px dashed var(--ml-pink, #d83adb);
  border-radius: 4px;
}
.submission-card.armed .vote-confirm-bar {
  display: flex;
}
.vote-confirm-prompt {
  font-family: var(--f-display);
  font-weight: 900;
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ml-ink, #111);
  margin-right: 4px;
}
.vote-confirm-btn {
  flex: 0 0 auto;
  font-family: var(--f-display);
  font-weight: 900;
  font-size: 13px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  padding: 5px 11px;
  /* X164-2026-06-14: 44px tap-target floor for the per-card Yes/Cancel vote
     buttons (glyph buttons were ~24px — too small to thumb reliably). */
  min-height: 44px;
  min-width: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  cursor: pointer;
  white-space: nowrap;
  transition: filter 0.1s, transform 0.06s;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.35),
              0 1px 2px rgba(0, 0, 0, 0.18);
}
.vote-confirm-btn.vote-confirm-yes {
  background: var(--ml-green, #4caf50);
  color: #fff;
  border: 1px solid var(--ml-green-dark, #4a8a40);
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25);
}
.vote-confirm-btn.vote-confirm-no {
  background: #f0f0f0;
  color: #222;
  border: 1px solid #888;
}
.vote-confirm-btn:hover  { filter: brightness(1.07); }
.vote-confirm-btn:active { transform: translateY(1px); filter: brightness(0.95); }
.vote-confirm-btn:focus-visible {
  outline: 2px solid #000;
  outline-offset: 2px;
}

.meme-action-btn:hover   { filter: brightness(1.07); }
.meme-action-btn:active  { transform: translateY(1px); filter: brightness(0.95); }
.meme-action-btn:focus-visible {
  outline: 2px solid #000;
  outline-offset: 2px;
}

/* 2026-07-08 Memelord.game revert - Save/Share chip chrome override. Restyles the
   chips to the Win95 raised-bevel look of the big NEXT ROUND button (they were
   flat rounded pills reading as generic stock buttons). Placed AFTER the original
   .meme-action-btn* rules so it wins by source order; originals kept for history.
   SHARE is the prominent green primary; SAVE the gray secondary. Text uses
   var(--ml-ink) so it flips light on the Night skin. */
.meme-action-btn {
  font-family: 'Lato', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-weight: 800;
  font-size: 11px;
  padding: 7px 14px;
  min-height: 34px;
  color: var(--ml-ink);
  background: var(--ml-gray);
  border: 2px solid;
  border-radius: 0;
  border-color: var(--ml-gray-hi-2) var(--ml-gray-sh) var(--ml-gray-sh) var(--ml-gray-hi-2);
  box-shadow: inset 1px 1px 0 var(--ml-gray-hi), inset -1px -1px 0 var(--ml-gray-sh-2);
}
.meme-action-btn.meme-save-btn {
  background: var(--ml-gray);
  color: var(--ml-ink);
  border: 2px solid;
  border-color: var(--ml-gray-hi-2) var(--ml-gray-sh) var(--ml-gray-sh) var(--ml-gray-hi-2);
  text-shadow: none;
}
.meme-action-btn.meme-share-btn {
  background: var(--ml-green);
  color: #fff;
  border: 2px solid;
  border-color: var(--ml-green-hi) var(--ml-green-dark) var(--ml-green-dark) var(--ml-green-hi);
  box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.40), inset -1px -1px 0 rgba(0, 0, 0, 0.30);
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.28);
  font-size: 13px;
  padding: 8px 18px;
}
.meme-action-btn.meme-post-btn {
  background: var(--ml-blue);
  color: #fff;
  border: 2px solid;
  border-color: var(--ml-navy-hi) var(--ml-navy-dark) var(--ml-navy-dark) var(--ml-navy-hi);
  box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.30), inset -1px -1px 0 rgba(0, 0, 0, 0.30);
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.30);
}
.meme-action-btn.meme-vote-btn {
  background: var(--ml-red);
  color: #fff;
  border: 2px solid;
  border-color: #e2637a var(--ml-red-dark) var(--ml-red-dark) #e2637a;
  box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.30), inset -1px -1px 0 rgba(0, 0, 0, 0.30);
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.30);
}
.meme-action-btn:hover   { filter: brightness(1.06); }
.meme-action-btn:active  { transform: translate(1px, 1px); filter: brightness(0.93); }

/* X## (2026-05-10): meme lightbox — clicking a meme image in the VOTE /
   SCORE grid pops the full-size image up in a fixed overlay so the
   in-card captions (which are clipped at the card's image bounds when
   the slot has a long caption) stay readable. Click anywhere on the
   backdrop or hit Esc to dismiss; compositor.js handles the listener
   wiring. The image scales to fit the viewport with a small margin. */
.meme-lightbox {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: rgba(0, 0, 0, 0.82);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  cursor: zoom-out;
  animation: meme-lightbox-fade 140ms ease-out both;
}
.meme-lightbox-img {
  max-width: min(95vw, 1100px);
  max-height: 92vh;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 4px;
  border: 2px solid #fff;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
  background: #000;
}
@keyframes meme-lightbox-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .meme-lightbox { animation: none; }
}
/* Generic "Available on paid" upsell chip. Drop one of these into any
   settings row, dropdown caption, or action bar to seed conversion.
   Auto-hides for paid users via /js/paid-features.js's
   [data-hide-when-paid] handler — no JS-per-page required. Visually
   distinct from real action buttons (gold pill, small) so it reads
   as informational, not a primary control. */
.upsell-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
  padding: 4px 10px 4px 8px;
  border-radius: 999px;            /* pill — the only place we round corners */
  background: linear-gradient(180deg,
    rgba(255, 215, 0, 0.92) 0%,
    rgba(232, 187, 0, 0.92) 100%);
  border: 1px solid #c9ab00;
  color: #3a2a00;
  font-family: var(--f-body);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-decoration: none;
  text-transform: none;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55),
              0 1px 2px rgba(0, 0, 0, 0.18);
  transition: filter 0.1s, transform 0.06s;
}
.upsell-chip:hover  { filter: brightness(1.06); color: #2a1f00; text-decoration: none; }
.upsell-chip:active { transform: translateY(1px); filter: brightness(0.96); }
.upsell-chip-star  { font-size: 12px; }
.upsell-chip-text  { white-space: nowrap; }
.upsell-chip-arrow { font-weight: 900; opacity: 0.7; }
@media (max-width: 480px) {
  .upsell-chip-text { white-space: normal; }
}

/* "Remove watermark" upsell link inside the meme-actions bar. Free
   users only — public/js/paid-features.js removes it on subscription
   activation. Deliberately small and link-styled (not a button) so it
   sits quietly next to Save / Share / Post without competing for the
   primary action; clicking it sends the visitor to /subscribe.html. */
.meme-watermark-upsell {
  flex: 1 0 100%;
  text-align: center;
  margin-top: 2px;
  font-family: var(--f-body);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--muted);
  text-decoration: underline dotted rgba(10, 42, 98, 0.45);
  text-underline-offset: 2px;
  cursor: pointer;
  opacity: 0.85;
  transition: opacity 0.12s, color 0.12s;
}
.meme-watermark-upsell:hover {
  opacity: 1;
  color: var(--ml-navy);
  text-decoration-color: var(--ml-navy);
}

.canvasWrap, .submission-canvas-wrap, .meme-display, .submission-card { position: relative; }

/* -----------------------------
   Prompt / scaffold / chips
   ----------------------------- */
.scaffold {
  font-family: var(--f-display);
  font-size: clamp(1.1rem, 2.4vw, 1.5rem);
  color: #111;
  font-weight: 800;
  text-align: center;
  margin: 12px 0;
  padding: 12px 16px;
  background:
    linear-gradient(180deg, #fffdc7 0%, var(--ml-yellow) 100%);
  border: 1px solid #c4a83c;
  border-radius: 3px;
  box-shadow: inset 0 1px 0 #fff, 0 2px 3px rgba(0, 0, 0, 0.15);
  text-wrap: balance;
}
.theme-chip {
  display: inline-block;
  background:
    linear-gradient(180deg, #ff85ff 0%, var(--ml-pink) 100%);
  color: #111;
  padding: 4px 12px;
  border: 1px solid #8f1c8b;
  border-radius: 999px;
  font-family: var(--f-body);
  font-weight: 700;
  font-size: 0.82rem;
  margin: 6px 0;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.5);
}

.quality-badge {
  display: inline-block; padding: 3px 10px; border-radius: 999px;
  font-family: var(--f-body); font-weight: 700; font-size: 0.72rem;
  letter-spacing: 0.05em; margin-left: 8px;
  border: 1px solid #666;
}
.quality-badge.gold  { background: var(--ml-yellow); color: #000; border-color: #c4a83c; }
.quality-badge.good  { background: var(--ml-cyan); color: #000; border-color: #1687a0; }
.quality-badge.ok    { background: #dcd9c9; color: #444; }
.quality-badge.wild { background: var(--ml-pink); color: #000; border-color: #8f1c8b; }

.slot-label {
  font-family: var(--f-body);
  font-size: 11px;
  font-weight: 700;
  color: #0058ee;
  margin: 10px 0 4px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
/* 2026-04-28: contrast fix — changed color from #b36b00 (4.18:1)
   to #9d5700 (5.53:1) to meet WCAG AA 4.5:1 standard. */
.prefill-hint { color: #9d5700; font-style: normal; font-weight: 700; }

/* -----------------------------
   Timer HUD — becomes an XP status indicator
   ----------------------------- */
.timer {
  font-family: var(--f-pixel);
  font-size: 0.8rem;
  font-weight: 400;
  color: #0058ee;
  text-align: center;
  padding: 8px 12px;
  background:
    linear-gradient(180deg, #ffffff 0%, var(--luna-grey-btn) 100%);
  border: 1px solid #7b7967;
  border-radius: 3px;
  letter-spacing: 0.06em;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: inset 0 1px 0 #fff;
}
/* Hide the empty timer chip in LOBBY/END phases so it doesn't show up as
   a mystery white rectangle in the header. Padding + background were
   rendering even when textContent was empty. */
.timer:empty { display: none; }
/* Timer pressure tiers — kept deliberately subtle. The host.js /
   player.js / spectator.html timer renderers add `.warn` at ≤15s and
   `.urgent` at ≤5s. Captain wanted these chill, not annoying:
   - .warn: gentle gold breathe (opacity dip only, no movement)
   - .urgent: red with a sub-pixel nudge — barely perceptible, just
     enough peripheral signal that time is almost up.
   Both honor prefers-reduced-motion. The brand override later in this
   file still pins `.warn` background to var(--ml-gold) — keeping that
   override too for !important specificity inside the brand section. */
.timer.warn {
  color: var(--ml-ink);
  background: var(--ml-gold);
  border-color: #b89a3c;
  animation: timer-breathe 1.8s ease-in-out infinite;
}
.timer.urgent {
  color: #fff;
  background: linear-gradient(180deg, #e56a6a 0%, var(--danger) 100%);
  border-color: #6a1011;
  animation: timer-nudge 0.9s ease-in-out infinite;
}
/* 2026-04-28: contrast fix — changed color from #b36b00 (4.18:1)
   to #9d5700 (5.53:1) to match prefill-hint and meet WCAG AA 4.5:1. */
.timer.paused { color: #9d5700; border-color: #9d5700; }
@keyframes timer-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.88; }
}
@keyframes timer-nudge {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-0.5px); }
  75%      { transform: translateX(0.5px); }
}
@media (prefers-reduced-motion: reduce) {
  .timer.warn, .timer.urgent { animation: none; }
}


/* === inlined: styles/06-submissions-vote.css === */
/* -----------------------------
   Submission cards
   ----------------------------- */
.submission-card {
  background: #ffffff;
  border: 1px solid var(--edge);
  border-radius: 3px;
  padding: 12px;
  margin-bottom: 12px;
  cursor: pointer;
  transition: transform 0.1s, box-shadow 0.1s, border-color 0.1s;
  box-shadow: inset 0 1px 0 #fff, 0 3px 6px rgba(0, 0, 0, 0.20);
}
.submission-card:hover {
  transform: translate(-1px, -1px);
  border-color: var(--luna-blue-1);
  box-shadow: inset 0 1px 0 #fff, 0 5px 10px rgba(0, 88, 238, 0.30);
}

/* Reaction situation bar (Captain 2026-06-22) 窶・the round's situation pinned on
   top of each reaction meme during the VOTE reveal so each card reads as a real
   meme (setup on top, reaction image below). Pure DOM = instant, no compositing,
   so the vote wall is never blank while images load. */
.reaction-situation-bar {
  background: #0a2a62;
  color: #fff;
  font-weight: 700;
  text-align: center;
  line-height: 1.25;
  padding: 8px 10px;
  border-radius: 6px 6px 0 0;
  font-size: 0.9rem;
  /* 2026-08-04 (Captain): always show the ENTIRE situation text — no
     ellipsis / one-line clamp. Wrapping is fine; cutting the prompt is not. */
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
  word-break: break-word;}
.reaction-situation-bar + .reaction-card-img,
.reaction-situation-bar + .submission-canvas {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  margin-top: 0;
  display: block;
}
.submission-card.your-own { opacity: 0.6; cursor: not-allowed; }
.submission-card.your-own:hover {
  transform: none;
  border-color: var(--edge);
  box-shadow: inset 0 1px 0 #fff, 0 3px 6px rgba(0, 0, 0, 0.20);
}
/* X81-2026-05-08: VOTE-phase "you voted for this" indicator + SCORE-phase
   winner highlight + audience-favorite border all rebuilt on the site
   green/navy palette. The prior styling was a yellow gradient + yellow
   border + yellow shadow that didn't match anything else on the site 窶・   Memelord.game leans Win95 navy + memelord-green for accents (see .btn,
   .mw-dept-line, the SAVE THIS MEME button, the WINNING-BY-DEFAULT
   stamp). Captain 2026-05-08: "the yellow color scheme doesn't fit with
   anything else in the site." Green pulls the WINNER state into the
   same palette as the green save button + green dept seal sitting
   directly below/above it on the score card. */
.submission-card.voted {
  border-color: var(--ml-green-dark);
  box-shadow: inset 0 0 0 1px var(--ml-green), 0 4px 8px rgba(0, 160, 67, 0.30);
}
.submission-card.winner {
  border-color: var(--ml-green-dark);
  background: #ffffff;
  box-shadow:
    inset 0 0 0 1px var(--ml-green),
    0 6px 14px rgba(0, 160, 67, 0.35);
}
.submission-card.reveal {
  animation: memelord-reveal 480ms cubic-bezier(.18,.89,.32,1.28) both;
}
@keyframes memelord-reveal {
  0%   { transform: scale(0.88) translateY(6px); opacity: 0; }
  60%  { transform: scale(1.03) translateY(0); opacity: 1; }
  100% { transform: scale(1) translateY(0); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .submission-card.reveal { animation: none; }
}

/* X81-2026-05-08: WINNER badge 窶・green pill, mirrors the site's primary
   .btn (memelord-green with the W95FA / Lato display stack) so the
   winner chip and the SAVE button under it visually rhyme. The pulse
   halo now uses the green channel; reduced-motion path unchanged. */
.winner-badge {
  display: inline-block;
  background: var(--ml-green);
  color: #ffffff;
  font-family: var(--f-display);
  font-weight: 900;
  font-size: 0.7rem;
  padding: 2px 8px;
  border: 1px solid var(--ml-green-dark);
  border-radius: 999px;
  letter-spacing: 0.06em;
  margin-left: 6px;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
  animation: memelord-badge-pulse 1.2s ease-in-out infinite alternate;
}
@keyframes memelord-badge-pulse {
  from { box-shadow: inset 0 1px 0 rgba(255,255,255,0.25), 0 0 0 0 rgba(0, 160, 67, 0.55); }
  to   { box-shadow: inset 0 1px 0 rgba(255,255,255,0.25), 0 0 0 6px rgba(0, 160, 67, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .winner-badge { animation: none; }
}

/* X09-2026-05-08: score-card header grid. Captain redesigned the round-
   results card so the byline reads cleanly even on a phone:
     笏娯楳笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏ｬ笏笏笏笏笏笏笏笏笏笏笏笏・     笏・submitter name (full row 1) 笏・          笏・     笏懌楳笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏､           笏・     笏・BANGER / WIN-BY-DEFAULT     笏・1 vote    笏・     笏・                            笏・WINNER    笏・     笏披楳笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏ｴ笏笏笏笏笏笏笏笏笏笏笏笏・   Replaces the previous .row flex byline (left-aligned name + WINNER
   pill, right-aligned vote count). The vote count and the WINNER badge
   are now stacked on the right; the BANGER / WINNING-BY-DEFAULT
   classification sits in the middle slot below the name (the "newly
   opened blank space" Captain called out). Same conditions that drive
   the rubber-stamp swap on the meme below 窶・see host.js renderScore()
   classificationLabel computation, mirrored in player.js.

   Layout choice: CSS Grid, two columns. Left column flexes
   (minmax(0,1fr) so the name truncates with ellipsis instead of
   blowing out the card). Right column auto-sized. Name spans both
   columns on row 1 so it gets the full first line; classification
   anchors col 1 row 2; meta stack anchors col 2 row 2. Empty cards
   reuse the same grid with an empty .sub-classification cell so the
   AWOL rows stay vertically aligned with populated cards. */
.submission-header {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  column-gap: 12px;
  row-gap: 4px;
  margin-bottom: 8px;
}
.sub-name {
  grid-column: 1 / -1;
  grid-row: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--f-display);
  line-height: 1.2;
}
.sub-name strong {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}
.sub-classification {
  grid-column: 1;
  grid-row: 2;
  min-width: 0;
  font-family: var(--f-pixel);
  font-size: 0.85rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: transparent;          /* default empty cells render no ink */
  line-height: 1.15;
  /* Slightly tucked rotation so it reads as a stamped designation,
     not a UI label. Keeps the rubber-stamp register Captain set up
     on the meme below without piling on a second graphic. */
  transform: rotate(-2deg);
  transform-origin: left center;
}
.sub-classification:empty {
  display: none;
}
.sub-classification.sub-classification-banger {
  /* 2026-05-15 (Captain): cell now hosts the BANGER stamp PNG instead
     of the red Comic-Sans text. Text-only fallback color preserved so a
     missing image (PNG 404) still reads in brand. */
  color: #b82f4e;
}
/* 2026-05-15 (Captain): single BANGER, rendered as the stamp PNG
   (img/stamps/stamp-banger.png) inside .sub-classification. Replaces
   both the prior red Comic-Sans text AND the absolute-positioned
   overlay on the meme canvas 窶・Captain QA said the two readings
   duplicated each other. The extra tilt keeps the rubber-stamp
   "slammed on" feel that the text variant carried via transform. */
.sub-classification-stamp {
  display: block;
  height: clamp(36px, 8vw, 64px);
  width: auto;
  margin: 2px auto 0;
  transform: rotate(-6deg);
  transform-origin: center center;
  pointer-events: none;
}
/* cowork-mlg-bestgame-0714: one-shot slam-in for the BANGER stamp at the
   SCORE reveal 窶・drops in oversized and settles onto its resting -6deg
   tilt, so the flattest core beat reads as an actual rubber stamp
   landing. host.js/player.js add the --slam modifier only on the round's
   first SCORE render (round-gated), so broadcast re-renders don't replay
   it. Ends exactly at the static rule's transform 窶・no layout drift. */
@keyframes memelord-stamp-slam {
  0%   { transform: scale(2.3) rotate(4deg); opacity: 0; }
  55%  { transform: scale(0.94) rotate(-7.5deg); opacity: 1; }
  100% { transform: scale(1) rotate(-6deg); opacity: 1; }
}
.sub-classification-stamp--slam {
  animation: memelord-stamp-slam 340ms cubic-bezier(.18, .89, .32, 1.28) both;
}
@media (prefers-reduced-motion: reduce) {
  .sub-classification-stamp--slam { animation: none; }
}
/* cowork-mlg-bestgame-0714: one-shot gold pulse on the winning SCORE card
   窶・the same celebratory beat the END leaderboard already had
   (scoreRowWinnerHold, 08-round-recap.css) promoted to the per-round
   winner so rounds 1..N-1 get a payoff too. One-shot (not infinite);
   ends at scale(1) with the .winner rule's own box-shadow untouched. */
@keyframes memelord-card-winner-hold {
  0% { transform: scale(1); }
  35% {
    transform: scale(1.025);
    box-shadow:
      inset 0 0 0 1px var(--ml-green),
      0 0 0 3px rgba(255, 215, 0, 0.55),
      0 0 24px 6px rgba(255, 215, 0, 0.45);
  }
  70% {
    transform: scale(1.015);
    box-shadow:
      inset 0 0 0 1px var(--ml-green),
      0 0 0 2px rgba(255, 215, 0, 0.35),
      0 0 16px 4px rgba(255, 215, 0, 0.3);
  }
  100% { transform: scale(1); }
}
.submission-card.score-winner-hold {
  animation: memelord-card-winner-hold 1100ms cubic-bezier(0.2, 0.7, 0.2, 1) 1;
  position: relative;
  z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
  .submission-card.score-winner-hold { animation: none; }
}
.sub-classification.sub-classification-default {
  /* Same green as the WINNING-BY-DEFAULT stamp + .winner-badge so the
     "won by default" row reads as one cohesive design beat. */
  color: var(--ml-green-dark);
}
.sub-meta-stack {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-end;
  justify-content: center;
  white-space: nowrap;
}
.sub-meta-stack .winner-badge {
  /* The badge had a 6px left margin from the old inline-byline layout;
     clear it so it sits flush in the right column. */
  margin-left: 0;
}

/* X81-2026-05-08: empty/AWOL placeholder card for players who didn't
   submit a meme this round. Renders with the same outer shape as a
   real submission card so the score grid has a consistent rhythm
   (Captain feedback: "we should still have containers for the players
   that didn't submit memes, just blank where the meme should go").
   Muted opacity + zero hover lift so it reads as "this slot was
   empty," not "tap me." The .empty-meme-slot inside is a dashed
   placeholder with a single "NO TRANSMISSION" line. */
.submission-card.empty {
  cursor: default;
  opacity: 0.78;
  background: var(--face-2);
  border-color: var(--edge);
  box-shadow: inset 0 1px 0 #fff, 0 1px 3px rgba(0, 0, 0, 0.10);
}
/* X82-2026-05-08: reserve 2 lines of vertical space on the byline row
   so the meme slot below sits at the same y-coordinate whether the
   player's name fits on one line ("Host") or wraps to two ("Booty
   booty booty"). Captain QA on the SCORE/VOTE grid: the empty-slot
   "NO TRANSMISSION" placeholder appears at different vertical offsets
   across cards because the byline row is variable-height and the meme
   slot floats below it. Strategy: align-items:flex-start + min-height
   on the row matches the visual outcome of "add an invisible blank
   line on any one-line names" without needing to insert ghost markup.
   Mobile @560 below already forces the name to nowrap+ellipsis (1
   line), so the min-height only adds ~14px of extra header room there
   窶・small price for visual parity across the row.

   X09-2026-05-08: score-card byline migrated from .row to
   .submission-header (two-row grid: name + classification/meta-stack).
   The same parity argument applies 窶・empty AWOL cards have a blank
   .sub-classification cell, populated cards may have BANGER /
   WINNING-BY-DEFAULT text 窶・so reserve the same min-height on the
   grid wrapper. Vote-phase cards still use .row and keep the original
   rule. */
.submission-card .row {
  align-items: flex-start;
  min-height: 2.6em;
}
.submission-card .submission-header {
  min-height: 2.6em;
}
.submission-card.empty:hover {
  transform: none;
  border-color: var(--edge);
  box-shadow: inset 0 1px 0 #fff, 0 1px 3px rgba(0, 0, 0, 0.10);
}
.empty-meme-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 140px;
  margin-top: 8px;
  border: 1px dashed var(--edge);
  border-radius: 2px;
  background: repeating-linear-gradient(
    135deg,
    rgba(0, 0, 0, 0.025) 0px,
    rgba(0, 0, 0, 0.025) 6px,
    transparent 6px,
    transparent 12px
  );
  color: var(--muted);
  font-family: var(--f-pixel);
  font-size: 0.8rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-align: center;
  padding: 12px;
}
.report-btn {
  position: absolute;
  top: 6px;
  right: 6px;
  background: rgba(255, 255, 255, 0.7);
  color: #b82f4e;
  border: 1px solid #b82f4e;
  border-radius: 3px;
  padding: 2px 6px;
  font-size: 0.72rem;
  cursor: pointer;
  opacity: 0.7;
  transition: opacity 0.1s, background 0.1s;
  z-index: 5;
}
.report-btn:hover, .report-btn:focus-visible {
  opacity: 1;
  background: #b82f4e;
  color: #fff;
}
/* cowork-mlg-mobile-polish-b-2026-07-08: keep the 笞 Report chip clearly INSIDE
   the card at phone width. Reaction vote cards are padding:0 with a 10px corner
   radius + overflow:hidden, so the base 6px inset lands the chip right on the
   rounded corner. Nudge it in to sit fully clear of the curve, and make its
   backdrop more opaque so it stays legible over a busy meme. max-width guards a
   long label from ever reaching the opposite edge on a very narrow card. The
   base 6px inset stays for classic (padded) submission cards. */
.report-btn {
  max-width: calc(100% - 16px);
}
.submission-card.reaction-card .report-btn {
  top: 8px;
  right: 8px;
  background: rgba(255, 255, 255, 0.9);
}
/* X81-2026-05-08: audience-favorite outline rebuilt on the site pink
   accent (var(--ml-pink), already used by the host-crown .badge above
   .winner-badge). Pink keeps the audience-favorite distinct from the
   green WINNER state 窶・they can co-occur on the same card and still
   read clearly. Prior styling was the same yellow as winner; that
   collision plus the off-palette yellow was the X81 motivation. */
.submission-card.audience-favorite {
  border-color: #8f1c8b;
  box-shadow: inset 0 0 0 1px var(--ml-pink), 0 4px 8px rgba(209, 72, 209, 0.30);
}
.audience-badge {
  display: inline-block;
  background:
    linear-gradient(180deg, #ff85ff 0%, var(--ml-pink) 100%);
  color: #111;
  font-family: var(--f-body);
  font-weight: 700;
  font-size: 0.7rem;
  padding: 2px 8px;
  border: 1px solid #8f1c8b;
  border-radius: 999px;
  letter-spacing: 0.03em;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
}

.submission-meme img { max-width: 100%; max-height: 240px; border-radius: 2px; border: 1px solid #7b7967; }
.submission-canvas { max-width: 100%; max-height: 320px; object-fit: contain; border-radius: 2px; display: block; margin: 0 auto; border: 1px solid #7b7967; }

/* 笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏
   Score: render submissions in a 3-across grid so the host (and players)
   can compare 4+ memes at once on the recap surface. Drops to 2-across
   on tablets, 1-across on phones. Each card's meme is height-capped via
   .submission-canvas above so the grid stays uniform.

   Vote phase (X??-2026-05-15): one-per-row at every viewport width.
   Captain on a live screenshot of a 3-card vote row: "I can't read a
   single letter on these memes, so we need to either have one per
   line or two per line, but they need to be readable窶ｦ one taking up
   the full screen, you just scroll down for each different meme."
   The vote phase is where the reader is supposed to DIGEST each meme
   ("have fun with the meme"), not skim a comparison grid 窶・that's
   what the score phase is. So vote goes 1-up always, with the
   canvas height cap raised so the meme actually reads at its natural
   size inside the 660-px main column. Score stays 3 / 2 / 1
   responsive because it IS a comparison surface (the final
   scoreboard recap).
   笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏 */
#voteSubmissions {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}
#voteSubmissions .submission-card {
  margin-bottom: 0;
  padding: 12px;
}
/* Vote-phase canvas cap 窶・lift from the shared 320 px so a square
   meme can render close to the 660-px main-column width. Most memes
   in the catalog are 1:1 or slightly portrait, so 660 px max-height
   keeps the typical card on a single screen on desktop. Tall multi-
   panel comics (Q-bicycle, drake, expanding-brain) overrun this cap
   and the user scrolls within the card body 窶・exactly what Captain
   asked for ("you just scroll down for each different meme"). */
#voteSubmissions .submission-canvas {
  max-height: 660px;
}
/* Score-phase submission cards (X??-2026-05-15): Captain on a 3-up
   Round Results screenshot 窶・"the same exact thing. So whatever you do
   for the other page, do for this page as well." Same readability
   argument as the vote-phase fix above: at 3-across in the 660-px main
   column each card is ~210 px wide, the meme hits the 320 px canvas
   cap, captions become unreadable. Score-phase rows are also
   submission cards (submitter name + meme + vote count + WINNER /
   AUDIENCE-FAVORITE badges), and Captain wants every one of those
   readable. So match the vote-phase layout 窶・1-per-row at every
   viewport, 660 px canvas cap, 16 px gap, 12 px card padding. The
   X150-2026-05-14 bottom-justified .meme-actions trick (below) is
   preserved as a no-op safety net; with 1-per-row the cards stand
   alone and there's no row-wide alignment to enforce, but the flex-
   column setup also doesn't hurt and keeps the action row's vertical
   anchor stable if a future thread restores multi-up.

   The end-of-round SCOREBOARD (RANK / OPERATOR / BANGERS / MEME /
   TOTAL table painted on the situation-room hero) is a different
   surface in 08-round-recap.css and unaffected by this rule. */
#scoreSubmissions {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}
#scoreSubmissions .submission-canvas {
  max-height: 660px;
}
#scoreSubmissions .submission-card {
  margin-bottom: 0;     /* gap on the grid replaces per-card margin */
  padding: 12px;        /* match vote-phase interior */
  /* X150-2026-05-14: bottom-justify the meme-actions row. Captain on
     SCORE-phase screenshot: "these three memes should be bottom
     justified so the share and save buttons are at the same point on
     every meme instead of the share and save buttons being at
     different heights." The grid already stretches all cards in a
     row to the tallest card's height (align-items: stretch default).
     Pre-X150 the .submission-canvas-wrap had natural height (header +
     meme + actions), leaving empty space below the wrap inside taller
     cards 窶・which left each card's actions row floating at a
     different y-coordinate across the row depending on its own meme's
     aspect ratio. New behavior: the card is a column flex container,
     the .submission-canvas-wrap fills the remaining vertical space
     (`flex: 1`) as its own column flex container, and `.meme-actions`
     gets `margin-top: auto` so it lands at the bottom of that
     stretched wrap. Net effect: meme image stays at its natural
     position right under the header, empty space falls between the
     image and the action buttons, and the buttons align across the
     three-card row. Empty AWOL cards have no .submission-canvas-wrap
     (just header), so they're unaffected 窶・they keep their natural
     short height stretched by grid align-items. */
  display: flex;
  flex-direction: column;
}
#scoreSubmissions .submission-card .submission-canvas-wrap {
  display: flex;
  flex-direction: column;
  flex: 1;
}
#scoreSubmissions .submission-card .meme-actions {
  margin-top: auto;
}
/* X??-2026-05-15: dropped the @media (max-width: 900px) 竊・   repeat(2, minmax(0, 1fr)) and @media (max-width: 560px) 竊・   grid-template-columns: 1fr overrides since #scoreSubmissions is now
   1-per-row at every viewport. The 560-px block is preserved BELOW
   for its phone-tightening of the score-card byline (.row width
   shrink + WINNER badge typography) 窶・those rules still apply now
   that they're scoped to the inner .submission-card .row, not the
   grid container. */
@media (max-width: 560px) {
  /* X??-2026-05-15: Captain on mobile: "the save and share buttons are
     not on the actual meme." On a phone viewport the 1-per-row
     submission cards make the meme image fill the full card width,
     and the .meme-actions toolbar sitting BELOW the meme reads as a
     separate row of chrome rather than as a "share this meme"
     affordance attached to the meme. The .share-corner pattern on
     hero pages (gigachad, final scoreboard, academy) already does
     "overlay chips at the bottom-right of the image" 窶・port that
     positioning to .submission-card .meme-actions on phones so the
     chips ride on the meme itself. The .submission-canvas-wrap is
     already `position: relative` (set in 05-meme-stage.css line ~342),
     so we just absolute-position the action row over the meme's
     bottom-right corner. Semi-translucent black background gives the
     chips a readable strip regardless of the meme's bottom-edge
     contents (sky / dark frame / busy text). Desktop unaffected 窶・     this rule only fires <=560px. */
  .submission-card .meme-actions {
    position: absolute;
    right: 8px;
    bottom: 8px;
    margin: 0;
    padding: 4px 6px;
    background: rgba(0, 0, 0, 0.55);
    border-radius: 5px;
    gap: 4px;
    z-index: 2;
    /* Cancel the X150 bottom-justify margin-top: auto inside
       #scoreSubmissions 窶・at <=560px the toolbar is no longer a flex
       item of the canvas-wrap column, it's an absolutely-positioned
       overlay. */
    margin-top: 0;
  }
  /* Drop the meme-canvas-wrap's flex: 1 stretch on mobile so the wrap
     hugs the meme image and the overlay sits flush at the meme's
     bottom-right corner instead of at the bottom of a tall flex-grown
     wrap that includes empty space. */
  #scoreSubmissions .submission-card .submission-canvas-wrap,
  #voteSubmissions  .submission-card .submission-canvas-wrap {
    flex: 0 1 auto;
  }
  /* Score-card byline: the .row holding the author name on the left and
     the "N votes" pill on the right used to wrap awkwardly on phones 窶・     mostly one line, but a medium-long name + WINNER / 刹 Audience
     Favorite badges would push the votes pill (or a single badge) onto
     a second line by itself. Force the row to one line and let the
     author name truncate with an ellipsis; tighten the badge typography
     so it fits without the right column collapsing. The card body
     below is still full width, so the meme image is unaffected.

     X09-2026-05-08: vote-phase cards still use .row and keep these
     rules; score-phase cards migrated to .submission-header (the grid
     defined above) and get their phone-tightening below. */
  .submission-card .row {
    flex-wrap: nowrap;
    gap: 8px;
  }
  .submission-card .row > div:first-child {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: 0.92rem;
    line-height: 1.25;
  }
  .submission-card .row > div:first-child strong {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
  }
  .submission-card .row > div:last-child {
    flex: 0 0 auto;
    white-space: nowrap;
  }
  .submission-card .winner-badge,
  .submission-card .audience-badge {
    display: inline-block;
    white-space: nowrap;
    font-size: 0.62rem;
    padding: 1px 5px;
    margin-left: 4px;
    letter-spacing: 0.03em;
  }
  /* X09-2026-05-08: the meta-stack winner-badge sits flush in its
     column, not inline next to the name, so override the inline rule
     above's left-margin re-application. */
  .submission-card .sub-meta-stack .winner-badge,
  .submission-card .sub-meta-stack .audience-badge {
    margin-left: 0;
  }
  .submission-card .votes-badge {
    font-size: 0.78rem;
    padding: 2px 8px;
  }
  /* Score-card .submission-header phone tightening 窶・same intent as the
     .row rules above (name truncates, right column doesn't collapse,
     badges are sized for narrow viewports) but applied to the X09 grid
     layout. The classification line shrinks too so a long
     "WINNING BY DEFAULT" caption fits next to the meta-stack. */
  .submission-card .submission-header {
    column-gap: 8px;
  }
  .submission-card .sub-name {
    font-size: 0.92rem;
    line-height: 1.25;
  }
  .submission-card .sub-classification {
    font-size: 0.72rem;
    letter-spacing: 0.04em;
  }
}
.submission-slots { margin-top: 10px; font-size: 0.95rem; line-height: 1.45; font-family: var(--f-body); }
.submission-slot { margin: 3px 0; color: #111; }
.submission-slot .role {
  color: #0058ee;
  font-size: 0.7rem;
  font-family: var(--f-body);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* 笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏
   X150-2026-05-15: Default-win SCORE card 窶・meme-first layout.

   On single-submission rounds (subCount === 1) the X14 pass already
   strips vote count / WINNER badge / vote bar / classification line /
   the green WINNING-BY-DEFAULT stamp from the card. The wrapper chrome
   itself (3-col grid lane, white card bg, border, shadow, 8px padding,
   320px canvas height cap) is what was left, and on Captain's
   2026-05-15 QA it still dominated the surface 窶・the meme rendered at
   ~1/3 of the available width inside a card-in-a-card, with the
   actual artifact dwarfed by the framing.

   When host.js / player.js renderScore() flips
   #scoreSubmissions.is-default-win-solo, this stanza blows out the
   chrome: single full-width column, transparent card, no border /
   shadow / padding, centered name above a generously-sized meme. The
   parent .card.mw-crt's frame is the only chrome the lone meme needs.

   Multi-submission rounds (the contest path) keep the existing X09
   grid + header + BANGER stamp etc. 窶・this rule set is gated on the
   .is-default-win-solo class only.
   笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏笏 */
#scoreSubmissions.is-default-win-solo {
  grid-template-columns: 1fr;
}
#scoreSubmissions.is-default-win-solo .submission-card {
  background: transparent;
  border: none;
  box-shadow: none;
  padding: 0;
  margin: 0;
  cursor: default;
}
#scoreSubmissions.is-default-win-solo .submission-card:hover {
  transform: none;
  border: none;
  box-shadow: none;
}
#scoreSubmissions.is-default-win-solo .submission-header {
  display: block;
  text-align: center;
  margin-bottom: 10px;
}
#scoreSubmissions.is-default-win-solo .sub-name {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  font-size: 1.15rem;
  line-height: 1.25;
}
#scoreSubmissions.is-default-win-solo .submission-canvas-wrap {
  margin-top: 0;
}
#scoreSubmissions.is-default-win-solo .submission-canvas {
  /* Lift the 320px cap so the lone meme fills the parent .card lane
     (~720px max). Object-fit:contain inherited from the base rule
     preserves the meme's aspect ratio. The 70vh ceiling keeps tall
     stacked memes (two-button + reaction face etc.) inside the
     viewport on short desktops / landscape phones. */
  max-height: 70vh;
  width: 100%;
  height: auto;
}

/* -----------------------------
   Reaction reveal cards (X163-2026-06-14)
   In reaction mode the play IS the image, so we drop the meme-card chrome and
   let the image run big / edge-to-edge. The .submission-card.reveal keyframe
   above is reused for the staggered deal-in (game-stage.js sets a per-card
   animation-delay) and already no-ops under prefers-reduced-motion.
   ----------------------------- */
.submission-card.reaction-card {
  padding: 0;
  background: transparent;
  border: none;
  box-shadow: none;
  overflow: hidden;
  border-radius: 10px;
}
.submission-card.reaction-card .submission-canvas-wrap {
  margin: 0;
  border-radius: 10px;
  overflow: hidden;
}
.reaction-card-img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 64vh;
  object-fit: contain;
  background: #000;
  border-radius: 10px;
  cursor: pointer;
}
/* Locked-in / selected vote still reads clearly without the card chrome. */
.submission-card.reaction-card.voted {
  outline: 4px solid var(--ml-green, #00a043);
  outline-offset: 2px;
}
/* Your own played card can't be voted 窶・dim it slightly so the eye skips it. */
.submission-card.reaction-card.your-own { opacity: 0.85; }



/* === inlined: styles/07-submit-stage.css === */
/* -----------------------------
   Submit stage — spatial WYSIWYG submit UI
   ----------------------------- */
/* The meme image fills the stage; textareas float over the slot regions
   defined by meme.text_positions.  The visual metaphor: "type right where
   the text goes." */

/* -----------------------------
   Reaction mode — the "played" confirmation card.
   In Reaction mode (the image-reaction game) tapping a hand image plays it
   immediately; this shows the image the player threw down while everyone
   else is still choosing. The hand itself reuses the .hand-fan / .hand-card
   fan-picker styling, so reaction images fan out exactly like meme cards. */
.reaction-played {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 10px 0 4px;
}
.reaction-played-img {
  max-width: min(320px, 80%);
  max-height: 42vh;
  width: auto;
  height: auto;
  border-radius: 8px;
  border: 2px solid var(--ml-navy, #1b2a4a);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
  background: #000;
}
/* Card "slam" (X163-2026-06-14) — the played image lands with a quick
   scale/settle the first time it appears after a player throws it down.
   player.js / host.js add the --slam modifier only on the play tick (a
   one-shot flag) so broadcast re-renders of the waiting state don't replay
   it. Reuses the reveal easing from 06-submissions-vote.css. */
@keyframes memelord-reaction-slam {
  0%   { transform: scale(1.18) rotate(-3deg); opacity: 0.35; }
  55%  { transform: scale(0.97) rotate(0.5deg); opacity: 1; }
  100% { transform: scale(1) rotate(0deg); opacity: 1; }
}
.reaction-played-img--slam {
  animation: memelord-reaction-slam 360ms cubic-bezier(.18, .89, .32, 1.28) both;
}
@media (prefers-reduced-motion: reduce) {
  .reaction-played-img--slam { animation: none; }
}
/* cowork-mlg-bestgame-0714: one-shot confirm pop when a caption locks in.
   The most-repeated action in the game (submit) had zero payoff — the
   button just swapped text. Class-only, added in the player:submit ack
   (player.js) with a remove→reflow→add so it replays each round; the
   e2e-pinned "Submitted ✓" text is untouched. */
@keyframes memelord-submit-lock {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}
.btn--submit-locked {
  animation: memelord-submit-lock 260ms cubic-bezier(.18, .89, .32, 1.28) 1;
}
@media (prefers-reduced-motion: reduce) {
  .btn--submit-locked { animation: none; }
}

/* Reaction preview + confirm (Captain 2026-06-22) — tapping a hand card opens
   this composed-meme preview (situation on top) with a Play/Back confirm before
   anything is sent. */
.reaction-preview-card {
  display: flex;
  justify-content: center;
  margin: 6px auto 0;
}
.reaction-preview-card .submission-canvas,
.reaction-preview-card .reaction-card-img {
  max-width: min(360px, 92%);
  max-height: 46vh;
  width: auto;
  height: auto;
  border-radius: 8px;
  border: 2px solid var(--ml-navy, #1b2a4a);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
  background: #000;
}
.reaction-preview-actions {
  display: flex;
  justify-content: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 14px;
}

.submit-stage {
  position: relative;
  width: 100%;
  max-width: 540px;
  margin: 8px auto 6px auto;
  background: #000;
  border: 2px solid var(--ml-navy);
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
.submit-stage-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  user-select: none;
  pointer-events: none;
}
.submit-slot-box {
  position: absolute;
  box-sizing: border-box;
  padding: 0;
  /* 2026-05-02 — Captain: "remove the white boxes, the text should just be
     on the image." Was a dashed white border + dark/navy translucent
     overlay + 1px black box-shadow, which on pale image regions read as a
     bright "white box" framing the caption.
     2026-05-03 followup — Captain: "get rid of the white background on
     all of the text boxes ... no white box. That white box should be a
     png and see through." The 5-02 pass left state-based outlines on
     :placeholder-shown / :hover / :focus-within (dashed white border
     plus a yellow focus glow), and those still read as a white box
     frame around the slot. Removed all of them. The slots are now
     fully transparent in every state — players locate empty slots via
     the placeholder text itself (Impact + outline so it reads as meme
     copy, see .submit-slot-input::placeholder below). No border, no
     box-shadow, no background. */
  border: none;
  border-radius: 3px;
  box-shadow: none;
  background: transparent;
  cursor: text;
}
.submit-slot-input {
  /* 2026-05-03 — Captain: "make all the text boxes capable of more size.
     When you type they should expand to fit all the text. The boxes are
     safe areas. Going outside is almost always fine as long as it's not
     more than 50%."
     Implementation: the .submit-slot-box wrapper still pins to the
     authored safe-area coords (defines the slot's anchor + center on
     the meme image), but the textarea itself is sized 150% on each axis
     and shifted -25% top/left so its center matches the slot center.
     That gives ~25% of spill on every side (50% total per axis) where
     typed text can render outside the safe area before submit-stage.js's
     autoFitFont() decides to shrink the font. The textarea's transparent
     background means the spillover area looks like part of the meme,
     not a UI chrome rectangle. */
  position: absolute;
  top: -25%;
  left: -25%;
  width: 150%;
  height: 150%;
  box-sizing: border-box;
  padding: 2px 4px;
  margin: 0;
  background: transparent;
  border: none;
  outline: none;
  resize: none;
  overflow: hidden;
  line-height: 1.08;
  font-weight: 900;
  letter-spacing: 0.01em;
  /* X85-2026-05-08: force word-wrap on long strings so the autoFitFont
     loop in submit-stage.js can find a font size that fits. Without
     these two declarations, a long unbreakable token (rare but real —
     e.g. a player typing a URL or a long single word) would force
     scrollWidth past clientWidth at every font size, so the loop
     would bottom out at the floor without wrapping. `word-break:
     break-word` allows mid-word breaks for languages without natural
     break points; `overflow-wrap: anywhere` is the modern equivalent
     and lets the browser break at any character if needed. Together
     they guarantee that long sample/placeholder text wraps to
     multiple lines instead of clipping mid-word at the right edge.
     Captain feedback: "the sample isn't fully viewable. It needs to
     adjust the text size and also wrap." */
  word-break: break-word;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  /* 2026-05-03 — the global `input, textarea, select` rule (now in
     `public/styles/15-mlcom-overrides.css` after the X53 split)
     paints an XP-style multi-layer inset bevel via four `box-shadow
     inset ... !important` rules on every textarea, and a four-layer
     `!important` block also forces `background: #fff`, `border: 0`,
     `font-family: var(--f-body)`, and `color: var(--ml-ink)`. That
     bevel + the white background combine into Captain's recurring
     "white box framing the caption" report; the `color: --ml-ink`
     override is the "white text turns gray when typing" report.
     2026-05-05 (X61) — the real fix landed in
     `public/styles/15-mlcom-overrides.css` as a `.submit-slot-input,
     .submit-slot-input:focus { ... !important }` carve-out IMMEDIATELY
     AFTER the radio/checkbox carve-out (lines ~167-177). It uses
     `!important` so it actually beats the global `!important` block
     above it — every prior fix in THIS file (07-submit-stage.css)
     was non-important and silently lost the cascade. The lines below
     (background: transparent, border: none, box-shadow: none) are
     kept as belt-and-suspenders + documentation; they would still
     apply if the X61 carve-out is ever removed.
     font-family / color / text-align / text-shadow / text-transform
     are set inline by submit-stage.js to match the compositor style
     for the slot. As of X61, submit-stage.js uses
     `ta.style.setProperty('color', ..., 'important')` so a future
     STYLES preset with non-white text wins over the X61 carve-out's
     `color: #ffffff !important`. */
  box-shadow: none;
}
.submit-slot-input:focus {
  box-shadow: none;
  border: none;
  outline: none;
}
.submit-slot-input::placeholder {
  /* 2026-05-01: was rgba(255,255,255,0.55) which read as muddy gray on the
     meme image — Captain feedback was that the preloaded guidance text
     looked "washed out and ugly." Bumped to solid white so the placeholder
     reads as real meme text (the player can still tell it's placeholder
     because it disappears on first keystroke / textarea has no value).
     Black 4-corner halo at 95% opacity gives the imgflip-style outline. */
  color: #ffffff;
  text-shadow:
    -2px -2px 0 rgba(0, 0, 0, 0.95),  2px -2px 0 rgba(0, 0, 0, 0.95),
    -2px  2px 0 rgba(0, 0, 0, 0.95),  2px  2px 0 rgba(0, 0, 0, 0.95),
     0   -2px 0 rgba(0, 0, 0, 0.95),  0    2px 0 rgba(0, 0, 0, 0.95),
    -2px  0   0 rgba(0, 0, 0, 0.95),  2px  0   0 rgba(0, 0, 0, 0.95);
  /* Inherit Impact + uppercase from the inline style on the textarea so
     placeholders read as proper meme captions, not ghosted helper text.
     Captain feedback 2026-04-25: was too small + wrong font (default UI).
     2026-05-01: removed hardcoded `font-size: 22px` so the placeholder
     now inherits the auto-fit size that submit-stage.js's autoFitFont()
     stamps onto the parent textarea. Empty textareas get sized at
     ~0.42 × box-height on initial render, so a tall slot now shows a
     tall placeholder instead of always reading at 22px regardless of
     slot size (the old behavior made placeholders look tiny in big
     slots and clipped in small ones). */
  font-family: inherit;
  font-weight: 900;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.submit-slot-input:disabled { opacity: 0.7; cursor: not-allowed; }
.submit-stage-hint {
  margin: 6px 0 2px 0;
  opacity: 0.85;
}

/* Setup helper — sits ABOVE the meme image and stays visible while the
   player is typing. The slot.label was previously used as the textarea
   placeholder, so it disappeared on first keystroke. Captain explicitly
   asked for it to persist so the player always sees the prompt. */
.submit-stage-setup {
  max-width: 540px;
  margin: 4px auto 8px auto;
  padding: 8px 12px;
  background: var(--ml-navy, #0a2a62);
  border: 2px solid var(--ml-navy-dark, #061a3d);
  border-radius: 6px;
  text-align: center;
}
.submit-stage-setup-line {
  font-family: var(--f-display, 'Impact', sans-serif);
  font-size: 16px;
  letter-spacing: 0.04em;
  color: #ffffff;
  line-height: 1.3;
  text-transform: none;
}
.submit-stage-setup-line + .submit-stage-setup-line {
  margin-top: 4px;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  padding-top: 4px;
}

/* Below-the-meme fallback block — used for visible slots that lack a
   text_position on the meme image (4-30). Reuses .submit-stage-setup
   for the navy backdrop + padding; adds top margin so it doesn't
   crowd the meme above. */
.submit-stage-fallback-below {
  margin-top: 12px;
}

/* Static prefilled slot display (e.g. "Always has been" on the
   always-has-been template) — matches the look of the editable slot
   but without any editable indicator. 2026-05-02 followup to
   .submit-slot-box's chrome removal: keep this fully transparent too,
   so the prefill text reads as part of the meme image. */
.submit-slot-box--static {
  border: none;
  background: transparent;
  cursor: default;
}
.submit-slot-box--static:hover {
  background: transparent;
  border-color: transparent;
}
.submit-slot-static {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 900;
  letter-spacing: 0.01em;
  line-height: 1.08;
  font-size: clamp(14px, 4vw, 28px);
  user-select: none;
}

/* Fallback path (memes without text_positions): image preview, then a
   prominent setup line, an action label "Your punchline", then the
   input. The setup persists visually because it's a separate element,
   not the textarea's placeholder. */
.submit-fallback-preview {
  max-width: 360px;
  margin: 0 auto 10px auto;
  border: 2px solid var(--ml-navy, #0a2a62);
  border-radius: 6px;
  overflow: hidden;
  background: #000;
}
.submit-fallback-preview img {
  display: block;
  width: 100%;
  height: auto;
}
.submit-fallback-action-label {
  margin-top: 10px;
  margin-bottom: 4px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ml-navy, #0a2a62);
}
.submit-fallback-input {
  width: 100%;
  /* Use the canonical meme stack (matches submit-stage.js IMPACT_FONT for the
     on-meme slot inputs) instead of var(--f-display), which leads with the
     W95FA pixel font and renders as illegible chunky blocks at this size.
     Captain feedback 2026-05-01: font was unreadable. */
  font-family: 'Impact', 'Anton', 'Arial Black', 'Helvetica Neue', sans-serif;
  font-size: 20px;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: 0.02em;
  /* Drop forced uppercase — the on-meme slots transform-uppercase because
     they're WYSIWYG with the composed meme. The fallback input is just a
     normal text box BELOW the image, so honor whatever case the player
     types. The compositor still uppercases when it bakes the final meme. */
  text-transform: none;
  padding: 10px 12px;
  border: 2px solid var(--ml-navy, #0a2a62);
  border-radius: 4px;
  background: #fff;
  color: #111;
  resize: vertical;
}
.submit-fallback-input:focus {
  outline: 2px solid var(--ml-yellow, #e9bb32);
  outline-offset: 1px;
}

/* Host submit stage — Captain feedback 2026-04-25: host's meme was much
   larger than the player's because the host page is wider and the stage
   hit the 540px max. Cap the host stage at the player's typical mobile
   width so they look consistent. */
#hostSubmitStage .submit-stage,
#hostSubmitStage .submit-stage-setup,
#hostSubmitStage .submit-fallback-preview {
  max-width: 380px;
}

/* -----------------------------
   Mad Libs sentence scaffold (X91-2026-05-08)
   ----------------------------- */
/* A single line of clean prose above the meme stage that reads as
   the canonical KYM-style sentence with the slot blanks live-filled
   from the player's typing. Captain X91: "it should be visually
   pleasing and simple." Non-interactive — just typography that
   updates as the player types in the floating textareas. The blank
   spans get a subtle yellow underline-highlight so the eye lands on
   them, but the surrounding stem reads as ordinary prose. No edit
   chrome (contenteditable, dotted underlines, etc.) — editable-stem
   is a deferred follow-up. When the meme has no `mad_libs_template`
   field this whole element doesn't render.

   Layout: max-width matches the stage width on each surface (540px
   default, 380px host) so the sentence reads as a caption ABOVE the
   meme rather than a separate banner. */
.submit-mad-libs {
  max-width: 540px;
  margin: 6px auto 8px auto;
  padding: 8px 14px;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid rgba(0, 0, 0, 0.18);
  border-radius: 6px;
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.10);
  font-family: var(--f-body);
  font-size: clamp(0.95rem, 2.4vw, 1.1rem);
  line-height: 1.4;
  color: #1a1a1a;
  text-align: center;
}
.submit-mad-libs-stem {
  /* Plain stem text — no styling beyond the inherited body font.
     Reads as normal prose. */
  white-space: pre-wrap;
}
.submit-mad-libs-blank {
  /* The blank — visually distinct so the eye knows "this is where
     your slot fills." Filled state = yellow background highlight.
     Empty state = dotted underline + grey "____" placeholder. */
  display: inline-block;
  padding: 0 6px;
  background: var(--ml-yellow, #ffe47a);
  border-radius: 3px;
  font-weight: 700;
  color: #1a1a1a;
  /* Cap how wide a single blank can stretch to keep the sentence
     readable when a player types a long phrase. The textarea on the
     meme is the source of truth for the whole value; this is just a
     preview, so eliding gracefully is fine. */
  max-width: 32ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: baseline;
  transition: background-color 0.15s ease;
}
.submit-mad-libs-blank.is-empty {
  background: transparent;
  border-bottom: 2px dotted rgba(0, 0, 0, 0.45);
  color: rgba(0, 0, 0, 0.45);
  font-weight: 400;
  letter-spacing: 0.15em;
  padding: 0 4px 1px 4px;
}

/* X95-2026-05-08 — when the scaffold is the primary input, the blanks
   are contenteditable. Add a subtle outline + cursor: text so they
   read as tap-to-edit fields, and a strong focus ring so the active
   blank is unambiguous. The READ-ONLY (X91) preview-only style stays
   intact — these rules only apply when the IIFE adds the
   `--editable` modifier. */
.submit-mad-libs--editable .submit-mad-libs-blank {
  cursor: text;
  outline: 1px solid rgba(0, 0, 0, 0.18);
  outline-offset: 1px;
  /* Allow the editable blank to wrap so a longer phrase doesn't get
     truncated mid-typing. Ellipsis / max-width carries on for the
     read-only preview where the textarea is the source of truth. */
  max-width: none;
  overflow: visible;
  text-overflow: clip;
  white-space: normal;
}
.submit-mad-libs--editable .submit-mad-libs-blank:hover {
  outline-color: rgba(0, 0, 0, 0.38);
}
.submit-mad-libs--editable .submit-mad-libs-blank:focus {
  outline: 2px solid var(--ml-accent, #1f6feb);
  outline-offset: 2px;
  background: var(--ml-yellow, #ffe47a);
}
.submit-mad-libs--editable .submit-mad-libs-blank.is-empty {
  /* Keep the dotted-underline placeholder cue but show the same
     "this is a tap target" outline so empty blanks are obviously
     interactive. */
  outline: 1px dashed rgba(0, 0, 0, 0.32);
  outline-offset: 2px;
}

/* Host page renders the submit stage at 380px; mirror the scaffold
   width so it sits in the same lane. */
#hostSubmitStage .submit-mad-libs {
  max-width: 380px;
}

/* -----------------------------
   Situation pin + per-slot suggest button (X90-2026-05-08)
   ----------------------------- */
/* The "situation pin" — a sticky readable copy of the round's
   situation, rendered immediately above the meme stage so it stays in
   the player's eyeline while they type. Pre-X90 the situation lived
   only in #situationMonitorHost / #submitPrompt at the top of the
   SUBMIT card and scrolled off as soon as the player engaged with
   the slots. Captain X90 framing: "right now it's just too hard to
   play the game." Visible chrome here is intentionally cheap — the
   serious situation chrome (Situation Monitor breaking-news panel)
   stays where it was; this pin is the "you're answering THIS" anchor
   that the player can glance at without scrolling. */
.submit-situation-pin {
  max-width: 540px;
  margin: 8px auto 10px auto;
  padding: 10px 14px;
  background: var(--ml-navy);
  color: #fff;
  border: 2px solid var(--ml-yellow, #ffe47a);
  border-radius: 6px;
  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.55);
  font-family: var(--f-pixel);
  text-align: center;
}
.submit-situation-pin-headline {
  font-size: clamp(0.85rem, 2.6vw, 1.05rem);
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1.2;
}
.submit-situation-pin-text {
  font-size: clamp(0.7rem, 2.0vw, 0.85rem);
  font-family: var(--f-body);
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1.3;
  margin-top: 4px;
  opacity: 0.92;
}
/* Host page renders the submit stage at 380px; mirror the situation
   pin width so the chrome reads as a stack with the same lane. */
#hostSubmitStage .submit-situation-pin {
  max-width: 380px;
}

/* "Help me out" row (X107-2026-05-10) — replaces the per-slot 💡 buttons
   (X90, retired). One secondary-style button below the meme stage that
   fills any EMPTY slot textareas with /api/slot-suggest results. Centered,
   modest size so it reads as an opt-in helper rather than a primary action.
   Slots that already have player text are left alone — see
   mountHelpMeOutButton() in public/submit-stage.js. */
.submit-help-row {
  display: flex;
  justify-content: center;
  margin-top: 10px;
}
.submit-help-btn {
  width: auto;
  min-width: 140px;
  font-size: 13px;
  padding: 6px 14px;
  opacity: 0.85;
}
.submit-help-btn:hover,
.submit-help-btn:focus-visible { opacity: 1; }
.submit-help-btn:disabled { cursor: progress; opacity: 0.55; }

/* Inline status message shown when "Help me out" is clicked while the
   Meme AGI suggestion path is offline (X135-2026-05-14). Renders in
   the same .submit-help-row slot the button occupied so layout
   doesn't shift. */
.submit-help-msg {
  font-size: 13px;
  opacity: 0.75;
  text-align: center;
  padding: 6px 14px;
}

/* Fallback path wrap — kept as a positioning context for any future
   per-slot affordances. Currently no children rely on its
   `position: relative;`, but the wrap node is still emitted by
   submit-stage.js buildFallback so the rule stays. */
.submit-fallback-input-wrap {
  position: relative;
  display: block;
}

/* -----------------------------
   Layout helpers
   ----------------------------- */
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.row > * { flex: 1; }
.row-nogrow > * { flex: 0 1 auto; }

.big-meme-stage {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
  align-items: start;
}
@media (max-width: 800px) {
  .big-meme-stage { grid-template-columns: 1fr; }
}

/* -----------------------------
   Scoreboard
   ----------------------------- */
.scoreboard { display: flex; flex-direction: column; gap: 6px; }


/* === inlined: styles/08-round-recap.css === */
/* -----------------------------
   Round-recap hero overlay
   -----------------------------
 * S6-2026-04-30 窶・task [A] + [D].
 *
 * The round-complete and final-round scoreboards are NOT a separate
 * panel below a phase hero; they're an OVERLAY drawn AS text on the
 * hero image's blank fields (OPERATOR / BANGERS / MEME / TOTAL).
 * `hero-round-complete-situation-room.png` is designed with those slots
 * pre-cut into the art; we just position absolute spans over them.
 *
 * Percentage-based positioning so the layout scales with the hero
 * image (host card width is variable, hero retains its aspect ratio).
 * Row inset and column widths are measured directly off the source PNG
 * (1672ﾃ・41) 窶・see the comments on .round-recap-hero-rows below for the
 * row-center percentages and column boundary x-coordinates.
 */
.round-recap-hero {
  position: relative;
  width: 100%;
  margin: 0 auto;
  line-height: 0;
  /* Same chrome edge as a phase-hero so the seam between rest-of-card
     and the hero panel reads as a window, not an island image. */
  border: 1px solid #7b7967;
  border-radius: 3px;
  overflow: hidden;
  /* Make this an inline-size container so children can use cqw units
     to size against the IMAGE's rendered width instead of the viewport.
     The overlay font scales with the hero image so the white-sticker
     text matches the printed names underneath at every breakpoint. */
  container-type: inline-size;
}
.round-recap-hero-img {
  display: block;
  width: 100%;
  height: auto;
  /* The art is hand-painted; do NOT smooth it. Crisp edges keep the
     situation-room signage readable at the overlay corners. */
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
  image-rendering: crisp-edges;
}
.round-recap-hero-rows {
  position: absolute;
  /* Position grid measured directly off hero-round-complete-situation-room.png
     (1672ﾃ・41). The printed table has 8 rows under a header strip, with
     OPERATOR / BANGERS / MEME / TOTAL columns and a separate TOP SCORE
     box on the right that the overlay must NOT cover.

     ROWS 窶・measured row centers in the printed art (% of image height):
       row 1=24.3%  row 2=30.4%  row 3=36.5%  row 4=41.9%
       row 5=47.6%  row 6=53.0%  row 7=58.3%  row 8=64.9%
     With an inset of 21.5% top / 32.2% bottom + eight equal-height grid
     rows, the row centers land at 24.4 / 30.2 / 36.0 / 41.9 / 47.7 /
     53.5 / 59.3 / 65.1% 窶・every row within ~1% of the printed name
     underneath, so the white sticker covers the printed line cleanly.

     COLUMNS 窶・left edge of OPERATOR column is at x竕・90 (17.3%), right
     edge of TOTAL column is at x竕・220 (73.0%). Right of x=1220 is the
     yellow TOP SCORE box, which the overlay must clear.

     X134-2026-05-12 (Captain: "scoreboard seems to have reverted to a
     version where it doesn't cover up all of the text underneath ...
     it keeps happening"): switched from `display: flex; flex-direction:
     column; justify-content: space-evenly` to `display: grid;
     grid-template-rows: repeat(8, 1fr)` because the flex variant was
     content-height-sensitive. The X10X blank padding rows render an
     em-dash (~15px tall), while real player rows with avatars + names
     render ~30-40px tall; flex+space-evenly distributed the extra
     height as gap space, sliding each row's vertical center AWAY from
     the painted PNG's slot center and exposing painted "DOOMER DARKLY"
     / "GLOOMJAK" / etc. above and below the short blank stickers.
     Captain has seen this exact regression more than once (X81/X82/X86/
     X87/X96/X105/X10X all touched these positions). With grid
     repeat(8, 1fr), each row is exactly 1/8 of the inset region
     regardless of content 窶・blank rows + real rows are always the
     same height, the white sticker always covers the painted slot.
     The companion `tests/test-style-guard.js` "round-recap-hero
     overlay" section guards against a future regression back to flex. */
  inset: 21.5% 27% 32.2% 17.3%;
  display: grid;
  grid-template-rows: repeat(8, 1fr);
  gap: 0;
  font-family: var(--f-body);
  font-weight: 800;
  /* Font-size sits a touch UNDER the printed names in the hero art so
     the JS sticker covers the printed line cleanly without overflowing
     into the next row's slot. Printed text is ~37px tall in the
     1672px-wide source (~2.2% of image width); the overlay used to
     match at 2.2cqw, but the sticker padding + 1.4em avatar pushed the
     row taller than the printed slot and clipped long names. X82
     dropped the upper bound to 1.7cqw and tightened the clamp range so
     a long name like "EDGELORD-IN-CHIEF 仄" fits inside the OPERATOR
     column. We size against the hero CONTAINER (cqw, set up via
     container-type on .round-recap-hero) so the overlay scales with
     the rendered image instead of the viewport. */
  font-size: clamp(0.45rem, 1.7cqw, 0.95rem);
  letter-spacing: 0.01em;
  /* Each cell is its own bad-Photoshop white sticker (see .rrh-cell
     rule below) so the row-level color/text-shadow is no longer the
     primary readability mechanism 窶・text is black on white. We keep a
     muted shadow on the row in case the leader-row override pushes a
     cell back to a non-white background. */
  color: #000;
  line-height: 1.2;
}
.round-recap-hero-row {
  display: grid;
  /* Column proportions measured from the printed header labels 窶・the
     OPERATOR / BANGERS / MEME / TOTAL header centers sit at 27.2 / 46.9
     / 57.6 / 67.7% of the image. Inside the 17.3%窶・3.0% inset slice,
     those proportions are 44 / 17 / 17 / 22 (sum: 100). */
  grid-template-columns: 44% 17% 17% 22%;
  /* X134-2026-05-12: stretch (was: center). The parent
     .round-recap-hero-rows is now `grid repeat(8, 1fr)`, so each row is
     1/8 of the inset region; stretching cells to fill the row makes
     each white sticker cover its painted slot's full height. The
     .rrh-cell rule below switches to flex + align-items: center so
     text stays vertically centered inside the now-stretched cell. */
  align-items: stretch;
  gap: 0;
  padding: 0;
}
/* X10X-2026-05-08 窶・blank padding rows ALWAYS-on so the painted
   PNG demo data ("DOOMER DARKLY", "GLOOMJAK", etc.) is always
   covered. Captain feedback #7: "Please cover all of these also up
   with blank scores, like we do for the players, but just blank."
   The existing .rrh-cell rule below already paints a white-sticker
   background that obscures whatever's painted on the hero PNG
   underneath; blank rows just dim the em-dash placeholder text so
   they read as "intentionally empty" rather than competing
   visually with real player rows. */
.round-recap-hero-row--blank .rrh-cell {
  color: rgba(0, 0, 0, 0.42);
  font-weight: 600;
}
/* Each cell is a slightly-badly-pasted white sticker that covers up the
   pre-printed line on the Situation Room hero art. Same bad-Photoshop
   vocabulary as .academy-face-photoshop / .academy-name-photoshop on the
   Mission Complete screen 窶・white background, black text, faint dark
   drop shadow, slight per-cell rotation so no two stickers sit at the
   same angle. The :nth-child() rotations alternate ﾂｱ1窶・deg so the row
   reads as four hand-cropped paste-ins rather than a clean table. */
.rrh-cell {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  background: #fff;
  color: #000;
  /* X134-2026-05-12: switched to flex so text stays vertically centered
     inside the now-stretched cell (parent row uses align-items: stretch
     so each cell fills its 1/8 slot height). Pre-X134 the cell was
     content-sized via `align-items: center` on the row, which made the
     white sticker only as tall as its text 窶・for blank rows with an
     em-dash that was ~12px tall, leaving the painted PNG name peeking
     through above and below. */
  display: flex;
  align-items: center;
  justify-content: center;
  /* Tightened vs. the X66 original (4px 7px). The sticker only needs
     to cover the printed line of text plus a hairline of margin 窶・     extra vertical padding pushed the sticker into the adjacent
     printed row's slot, and the chunky drop shadow made it overlap
     even more. X82 trims to 1px 5px so the sticker matches the
     printed row height closely. */
  padding: 1px 5px;
  line-height: 1.1;
  /* Crisp 1px black outline + a chunky offset shadow = "I cut this out
     of an image and pasted it on top." Shadow lengths reduced from the
     5px / 3px / 1px ladder to 3px / 2px / 1px so the visible bleed
     past the row slot doesn't overlap the row above. */
  border: 1px solid #000;
  box-shadow:
    1px 1px 0 #000,
    2px 2px 0 rgba(0, 0, 0, 0.40),
    3px 3px 5px rgba(0, 0, 0, 0.30);
  text-shadow: none;
}
/* Operator cell needs to wrap long handles (e.g. "EDGELORD-IN-CHIEF
   仄") rather than ellipsize them 窶・Captain wants the whole name
   readable. Override the row-level nowrap + ellipsis. The rest of the
   numeric cells (BANGERS / MEME / TOTAL) keep nowrap because the
   values are 1窶・ chars and shouldn't ever wrap.

   X86-2026-05-08: dropped the operator-cell cqw + max so a 2-line
   wrapped handle ("Booty booty booty 荘") fits inside the printed
   row slot without bleeding into the row above/below. The row-level
   font-size is `clamp(0.45rem, 1.7cqw, 0.95rem)` for the numeric
   columns (where the 1窶・ char values can stay punchy). The operator
   cell drops to `clamp(0.4rem, 1.45cqw, 0.82rem)` so a wrapped name
   renders ~15% smaller than the BANGERS/MEME/TOTAL digits 窶・still
   legible (~10窶・3px depending on viewport), but tight enough that
   two lines stack inside the printed row's vertical slot. Captain
   feedback: "adjust the text size on the names so they fit
   correctly." */
.rrh-cell.rrh-operator {
  white-space: normal;
  text-overflow: clip;
  overflow-wrap: anywhere;
  word-break: break-word;
  /* Slightly tighter line-height when the name does break to two
     lines 窶・1.0 keeps the wrapped name from punching into the row
     above/below. */
  line-height: 1.0;
  /* 2026-05-14 (Captain): "The font on the player names on the scoreboard
     is smaller. It got reverted somehow to a different font than it used
     to be... it should look presentable, a little bit bigger, and fit in
     the text box uniformly and well." Reverted the X86 + X105 size
     shrinks that compounded into a name column noticeably smaller than
     the BANGERS / MEME / TOTAL digits. The operator cell now inherits
     the row's default `clamp(0.45rem, 1.7cqw, 0.95rem)` so the name
     reads at the same scale as the numeric columns. Long handles with
     trailing emoji still wrap (overflow-wrap: anywhere + word-break +
     line-height: 1.0 above) so the cell stays inside its printed row
     slot vertically even at the larger size.
       Earlier: X105-2026-05-10 dropped 1.45cqw / 0.82rem 竊・1.25cqw /
       0.72rem; X86-2026-05-08 dropped 1.7cqw / 0.95rem 竊・1.45cqw /
       0.82rem. Both were responses to specific long-name screenshots
       and over-corrected for the typical case.
       2026-07-05 (Captain): long names ("MEWS DADDY MILLION") overflowed the
       results row and the TOTAL column got cut. Back to a smaller clamp so long
       names wrap to two tidy lines and fit inside the printed slot. */
  font-size: clamp(0.4rem, 1.45cqw, 0.8rem);
}
/* Per-cell rotation 窶・uneven on purpose. Keep all transforms small
   (竕､2deg) so the row still scans as a row. */
.rrh-cell.rrh-operator { transform: rotate(-1.4deg); }
.rrh-cell.rrh-bangers  { transform: rotate(0.9deg); }
.rrh-cell.rrh-meme     { transform: rotate(-1.8deg); }
.rrh-cell.rrh-total    { transform: rotate(1.5deg); }
/* Leader row: swap white sticker for yellow sticker so the rank-1 line
   still pops the way the old gold-text rule did. Black text holds. */
.round-recap-hero-row.leader .rrh-cell {
  background: var(--ml-yellow, #ffe47a);
  border-color: #6e5b00;
}
.rrh-operator {
  /* X134-2026-05-12: was `display: inline-flex` 窶・bumped to `flex` so the
     operator sticker fills the cell width (the parent .rrh-cell rule now
     uses display:flex + align-items:center for the stretch-to-fill row
     layout). The avatar + name still flow horizontally via the same
     flex container, with gap:6px between them as before. */
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 6px;
}
.rrh-operator .pill-avatar {
  /* Trimmed from 1.4em to 1.0em (X82). At 1.4em the avatar pushed
     the operator cell taller than the printed row, contributing to
     the sticker overflowing the row slot. 1.0em sits flush with the
     name's cap height and leaves more horizontal room for a long
     handle to fit on one line. */
  width: 1.0em;
  height: 1.0em;
  flex: 0 0 auto;
  border-radius: 50%;
  background: #fff;
  object-fit: cover;
}
/* cowork-mlg-mobile-polish-b-2026-07-08: cap the wrapped operator NAME at two
   lines (ellipsize beyond) so a long handle stops stacking into three tiny
   ~6px lines at 竕､390px and blowing out the printed row. The name is the only
   flexible part of the operator cell; the crown / avatar / host+dc tags stay
   inline via the .rrh-operator flex row. No sticker geometry, inset, or
   guard-pinned value is touched 窶・this only bounds the name text. */
.rrh-op-name {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
  word-break: break-word;
  min-width: 0;
}
.rrh-bangers, .rrh-meme, .rrh-total { text-align: center; }

/* TOP SCORE side panel overlay (X82-2026-05-08, retuned 2026-05-08).
   The situation-room hero PNG has a yellow "TOP SCORE / 188 / WOJAK
   GENERAL" placard hand-painted into the upper-right of the image.
   Placeholder values were baked into the art, so without an overlay
   the hero displays the same "188 / WOJAK GENERAL" every game.
   The pre-tune version stacked two flex stickers starting at
   top:42% with tiny fonts 窶・they painted *below* the printed text
   instead of *over* it, so the live score (e.g. "100 / CRYPTO
   WIDOW") read as a small second placard tacked on under the
   placeholder. Captain asked for the live values to cover the
   printed values at the same size.
   Each sticker now absolute-positions over its target region:
     - .rrh-top-score-value sits over the "188" trophy digits
       (centered ~30% image height; 18% wide ﾃ・~25% tall 窶・sized
       to cover the digit pixels without painting over the laurel
       wreaths on either side).
     - .rrh-top-score-name sits over the "WOJAK GENERAL" white
       sticker (centered ~49% image height; 21% wide ﾃ・8% tall 窶・       sized to match the printed sticker's footprint).
   Container `.rrh-top-score` itself fills the hero (inset:0) and is
   pointer-events:none; only its absolute children paint.
   Pixel-percentages measured directly off
   `hero-round-complete-situation-room.png` (1672ﾃ・41). */
.rrh-top-score {
  /* 2026-07-08 QA (Captain's direct ruling, same day, PM): the two floating
     stickers 窶・yellow live-score box + white name plate 窶・ARE the design.
     Captain: "they're designed to be sitting on top of the grid ... they're
     just mis-sized on the grid." An earlier same-day thread replaced this
     container with an opaque gold winner panel (name only, no score); QA
     restored the sticker design per Captain's order and kept that thread's
     other wins (rank-1-only crown, Save/Share chrome). Container = the
     transparent inset:0 overlay again; only its absolute children
     (.rrh-top-score-value + .rrh-top-score-name, pixel-measured below)
     paint 窶・the painted TOP SCORE label + laurels + placard frame stay
     visible around them. */
  position: absolute;
  inset: 0;
  font-family: var(--f-body);
  font-weight: 800;
  letter-spacing: 0.01em;
  color: #000;
  line-height: 1.0;
  pointer-events: none;
}
/* 2026-07-08 QA 窶・the painted title reads "ROUND 7 RESULTS" on EVERY round
   (the art bakes a demo round number 窶・same class of problem as the painted
   "188 / WOJAK GENERAL" placard). Cover just the digit with a live
   round-number sticker in the same bad-photoshop vocabulary. Painted "7"
   pixel-measured at x45.76竊・7.42 / y10.1竊・5.5 on the 1320ﾃ・43 art; the
   sticker is CENTER-anchored on the digit so a 2-digit round grows both
   ways instead of sliding into "RESULTS". Font ~3cqw = the painted title's
   glyph scale. Emitted by scoreboard-renderer.js only when the live round
   number is known (no round 竊・painted 7 shows, the pre-2026-07-08 status
   quo, never a blank patch). */
.rrh-round-num {
  position: absolute;
  top: 9.7%;
  left: 46.6%;
  transform: translateX(-50%) rotate(1.1deg);
  height: 6.4%;
  min-width: 2.7%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #fff;
  color: #000;
  border: 1px solid #000;
  box-shadow:
    1px 1px 0 #000,
    2px 2px 0 rgba(0, 0, 0, 0.40);
  font-size: clamp(0.7rem, 3cqw, 1.8rem);
  line-height: 1;
  white-space: nowrap;
  text-align: center;
  box-sizing: border-box;
  padding: 0 0.35%;
}
/* X105-2026-05-10 窶・re-measured both sticker positions against the
   painted PNG (1672ﾃ・41). Pre-X105 the value sticker sat at
   top:20% / height:25% and the name sticker at top:46% / height:8%,
   which placed the live yellow rectangle 11% LOWER than the painted
   yellow trophy box (which spans y竕・5竊・95 竊・9%-31%) and the live
   white name sticker BELOW the painted "WOJAK GENERAL" rectangle
   (y竕・05竊・55 竊・32%-38%). The result on Captain's ROUND 7 screenshot
   was a yellow "100" sitting where the painted laurel base sat and a
   small white "Host" floating below the painted name area instead of
   covering it. Re-aligned to top:9%/h:23% (yellow) and top:32%/h:6.5%
   (white) so each overlay paints over its painted counterpart at the
   same vertical position, with widths trimmed slightly so the laurel
   wreaths on either side of the trophy box remain visible. */
.rrh-top-score-value {
  position: absolute;
  /* X150-2026-05-15 (Captain: "the name and the score of the leading
     scorer are not covering properly anymore"). Bumped top 9% 竊・7%,
     right 7% 竊・5%, width 17% 竊・21%, height 23% 竊・27%. The X105
     measurements traced the painted yellow trophy rectangle's nominal
     bounds, but the live yellow sticker was clipping inside those
     bounds 窶・Captain's screenshot showed the painted "188" and the
     painted yellow rectangle edges still visible past the live yellow
     sticker on the top + right sides. The new dimensions add ~2%
     margin on every axis so the live yellow always fully obscures the
     painted yellow at every hero render-size. The laurel-wreath art
     sits OUTSIDE the painted yellow rectangle, so the bumped width
     doesn't collide with it. */
  /* 2026-07-08 (Captain: "they're designed to be sitting on top of the
     grid ... they're just mis-sized on the grid"): the CURRENT hero PNG
     is a different render than the art the X105/X150 numbers were traced
     from 窶・it's 1320ﾃ・43 (same 16:9 aspect as the old 1672ﾃ・41, so
     percentages still work), and its painted placard sits LOWER and
     smaller. PIXEL-MEASURED (script-scanned color bands, not eyeballed):
       placard border strokes x75.5 / x93.3, top y15.3, bottom y46.8
       "TOP SCORE" label y17.6竊・1.0 (must stay visible)
       "188" digits x79.3竊・9.8 / y25.0竊・8.9 (laurels hug both sides)
       "WOJAK GENERAL" line x76.7竊・2.0 / y41.5竊・4.5
     The old "top 7% / h 27%" yellow box rode the ROUND N RESULTS title band
     and spilled past the placard border onto the board frame. New box
     covers the digit zone with ~1% margin, inside the placard, clear of
     the TOP SCORE label above and the name sticker below. */
  /* 2026-07-08 fit pass: nudged up 24->22 + taller 16->18 (and a touch wider)
     so the live yellow fully covers the painted yellow rectangle 窶・the old box
     left a thin painted-yellow sliver at its top edge. Left edge ~78% still
     clears the 73% TOTAL column. */
  top: 22%;
  right: 8.7%;
  width: 13.6%;
  height: 18%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ml-yellow, #ffe47a);
  color: #000;
  border: 1px solid #6e5b00;
  box-shadow:
    1px 1px 0 #000,
    2px 2px 0 rgba(0, 0, 0, 0.40);
  /* Dropped from 8.5cqw to 6.5cqw so the digits sit COMFORTABLY
     inside the yellow rectangle the way the painted "188" does
     (digit cap-height 竕・60% of box height, with breathing room
     above and below). Pre-X105 8.5cqw made the digits punch the
     top and bottom edges of the box on the round-complete card. */
  font-size: clamp(1rem, 5.5cqw, 3.2rem); /* 2026-07-05: smaller number (Captain: score box too big) */
  line-height: 1;
  transform: rotate(-1.2deg);
  white-space: nowrap;
  text-align: center;
  box-sizing: border-box;
  padding: 0 4px;
}
.rrh-top-score-name {
  position: absolute;
  /* 2026-08-04 (Captain: "MATH POPE JR" rendered as "ATH POPE JR"):
     the 17%/right:7% sticker was clipping the left edge of long names.
     Widen + shift left a touch, pad both sides, and wrap instead of
     ellipsizing so the full operator name always shows. Still stays
     inside the painted placard and clear of the TOTAL column (~71%). */
  top: 39.5%;
  right: 6%;
  width: 20%;
  height: 9%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #fff;
  color: #000;
  border: 1px solid #000;
  box-shadow:
    1px 1px 0 #000,
    2px 2px 0 rgba(0, 0, 0, 0.40);
  font-size: clamp(0.34rem, 1.7cqw, 0.85rem);
  line-height: 0.95;
  transform: rotate(0.8deg);
  white-space: normal;
  overflow: hidden;
  text-overflow: clip;
  overflow-wrap: anywhere;
  word-break: break-word;
  hyphens: auto;
  text-align: center;
  box-sizing: border-box;
  padding: 2px 5px;
}
/* X10X-2026-05-08 窶・blank TOP SCORE placard dims the em-dash so it
   reads as "intentionally empty" while the yellow + white sticker
   rectangles still paint over the demo placard on the PNG. (Restored
   2026-07-08 QA together with the sticker design; a same-day thread had
   recolored it for the retired winner-panel variant.) */
.rrh-top-score--blank .rrh-top-score-value,
.rrh-top-score--blank .rrh-top-score-name {
  color: rgba(0, 0, 0, 0.42);
  font-weight: 600;
}
/* Final-round variant uses the gigachad crowned hero. The art has the
   same column structure; this hook is here so future tunes (e.g. extra
   TOP-SCORE column, different inset percentages) can land without
   touching the round-complete rule. */
.round-recap-hero--final .round-recap-hero-rows {
  inset: 28% 4% 6% 4%;
}

/* Crown winner name 窶・keep the entire winner.name visible above the
   final scoreboard. Ellipsize only as last resort (max-width caps it
   at the card width). S6-2026-04-30 窶・task [D]. */
.winner-crown-name {
  color: var(--accent);
  max-width: 100%;
  overflow-wrap: anywhere;
  word-break: break-word;
  hyphens: auto;
  text-align: center;
  margin: 0 auto;
  padding: 0 12px;
}
.score-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 12px;
  background:
    linear-gradient(180deg, #ffffff 0%, #ece9d8 100%);
  border: 1px solid #7b7967;
  border-radius: 3px;
  font-family: var(--f-body);
  font-weight: 700;
  color: #000;
  box-shadow: inset 0 1px 0 #fff;
}
.score-row.leader {
  background:
    linear-gradient(180deg, #fffdc7 0%, var(--ml-yellow) 100%);
  color: #000;
  border-color: #c4a83c;
  font-weight: 800;
}

/* -----------------------------
   Scoreboard staggered reveal
   ----------------------------- *
 * The END card builds up #last 竊・#1 with 600ms beats (host.js +
 * player.js drive the timing). Rows render with .score-row--hidden;
 * the JS removes it and adds .score-row--revealed in reverse order.
 * The winner row gets .score-row--winner-hold for the celebratory
 * gold pulse beat. prefers-reduced-motion users skip the animation
 * entirely (the JS short-circuits and applies the end state on the
 * first frame).
 */
.score-row--hidden {
  opacity: 0;
  transform: translateY(10px) scale(0.985);
  /* No transition here 窶・the reveal class drives the animation. */
}
.score-row--revealed {
  animation: scoreRowReveal 360ms cubic-bezier(0.2, 0.7, 0.2, 1.05) both;
}
@keyframes scoreRowReveal {
  0%   { opacity: 0; transform: translateY(10px) scale(0.985); }
  60%  { opacity: 1; transform: translateY(-2px) scale(1.01); }
  100% { opacity: 1; transform: translateY(0)    scale(1); }
}
.score-row--winner-hold {
  animation: scoreRowWinnerHold 1200ms cubic-bezier(0.2, 0.7, 0.2, 1) 1;
  z-index: 1;
  position: relative;
}
@keyframes scoreRowWinnerHold {
  0% {
    transform: scale(1);
    box-shadow: inset 0 1px 0 #fff;
  }
  35% {
    transform: scale(1.04);
    box-shadow:
      inset 0 1px 0 #fff,
      0 0 0 3px rgba(255, 215, 0, 0.55),
      0 0 24px 6px rgba(255, 215, 0, 0.45);
  }
  70% {
    transform: scale(1.025);
    box-shadow:
      inset 0 1px 0 #fff,
      0 0 0 2px rgba(255, 215, 0, 0.35),
      0 0 16px 4px rgba(255, 215, 0, 0.3);
  }
  100% {
    transform: scale(1);
    box-shadow: inset 0 1px 0 #fff;
  }
}
@media (prefers-reduced-motion: reduce) {
  /* Belt-and-suspenders with the JS short-circuit: if any path here
     leaves a row mid-animation, force the end state. */
  .score-row--hidden,
  .score-row--revealed,
  .score-row--winner-hold {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}


/* === inlined: styles/09-utilities-admin.css === */
/* -----------------------------
   Utilities
   ----------------------------- */
.muted   { color: var(--muted); }
.center  { text-align: center; }
.hidden  { display: none !important; }
.small   { font-size: 12px; }

.badge {
  display: inline-block;
  background:
    linear-gradient(180deg, #ff85ff 0%, var(--ml-pink) 100%);
  color: #111;
  padding: 2px 10px;
  border: 1px solid #8f1c8b;
  border-radius: 999px;
  font-family: var(--f-body);
  font-weight: 700;
  font-size: 0.72rem;
  margin-left: 6px;
  letter-spacing: 0.02em;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.5);
}
/* X81-2026-05-08: votes-badge rebuilt on the navy chrome. Prior styling
   was a yellow gradient that didn't match anything else on the site;
   the new shell uses the same Win95 navy + W95FA-ish display stack as
   the host-crown badge area + the .mw-dept-line below the score card,
   so the "N votes" pill reads as cohesive chrome rather than a
   leftover from a different theme. */
.votes-badge {
  display: inline-block;
  background:
    linear-gradient(180deg, var(--ml-navy-hi) 0%, var(--ml-navy) 100%);
  color: #ffffff;
  padding: 2px 10px;
  border: 1px solid var(--ml-navy-dark);
  border-radius: 999px;
  font-family: var(--f-display);
  font-weight: 900;
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.30);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
}

/* Per-submission vote-share bar — surfaced on every SCORE-phase card so
   non-winners can see "I got 3, the winner got 7" at a glance. Fill width
   is voteCount / topVotes (set inline as --vote-bar-fill). Cards with 0
   votes keep the empty shell so the row stays vertically aligned.
   X81-2026-05-08: fill rebuilt on the site green channel. The yellow
   gradient that shipped with the prior styling was the same off-palette
   yellow the score-card chrome used; both swept to green in the same
   pass so the bar reads as continuous with the WINNER badge above it. */
.vote-bar {
  position: relative;
  height: 8px;
  margin-top: 8px;
  background: rgba(0, 0, 0, 0.08);
  border: 1px solid rgba(0, 0, 0, 0.18);
  border-radius: 999px;
  overflow: hidden;
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.12);
}
.vote-bar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--vote-bar-fill, 0%);
  background:
    linear-gradient(180deg, var(--ml-green-hi) 0%, var(--ml-green) 100%);
  border-right: 1px solid var(--ml-green-dark);
  transition: width 380ms ease-out;
}
.submission-card.winner .vote-bar-fill {
  background:
    linear-gradient(180deg, var(--ml-green-hi) 0%, var(--ml-green-dark) 100%);
}
.vote-bar[data-empty="true"] .vote-bar-fill { display: none; }
@media (prefers-reduced-motion: reduce) {
  .vote-bar-fill { transition: none; }
}

/* -----------------------------
   Admin dashboard
   ----------------------------- */
.admin-table { width: 100%; border-collapse: collapse; margin-top: 12px; background: #fff; }
.admin-table th, .admin-table td {
  padding: 8px 10px; text-align: left;
  border-bottom: 1px solid #dcd9c9;
  font-size: 13px;
}
.admin-table th {
  color: #0058ee;
  font-family: var(--f-body);
  font-weight: 700;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  background: #ece9d8;
  border-bottom: 1px solid #7b7967;
}
.admin-table td img { max-height: 40px; border-radius: 2px; border: 1px solid #7b7967; }
.admin-mood {
  display: inline-block;
  background: #ece9d8;
  color: #0058ee;
  padding: 2px 8px;
  border: 1px solid #7b7967;
  border-radius: 3px;
  font-size: 0.72rem;
  margin: 2px;
}

.slot-editor {
  background: #ffffff;
  border: 1px solid #7b7967;
  padding: 10px;
  border-radius: 3px;
  margin-bottom: 8px;
  box-shadow: inset 0 1px 0 #fff;
}

/* -----------------------------
   Moderation term cloud (admin)
   ----------------------------- */
.term-cloud {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 8px;
  background: #fff;
  border: 1px solid #7b7967;
  border-radius: 3px;
  min-height: 40px;
  max-height: 160px;
  overflow-y: auto;
  box-shadow: inset 1px 1px 0 #dcdcdc;
}
.term-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: #ece9d8;
  color: #1687a0;
  padding: 2px 6px;
  border-radius: 3px;
  font-size: 0.78rem;
  font-family: var(--f-body);
  border: 1px solid #7b7967;
}
.term-chip[data-severity="hard"]   { color: #b82f4e; border-color: #b82f4e; }
.term-chip[data-severity="soft"]   { color: #0058ee; border-color: #0058ee; }
.term-chip[data-severity="family"] { color: #b36b00; border-color: #b36b00; }
.term-chip .term-remove {
  background: transparent;
  color: inherit;
  border: 0;
  cursor: pointer;
  padding: 0 2px;
  font-size: 1rem;
  line-height: 1;
  opacity: 0.7;
}
.term-chip .term-remove:hover { opacity: 1; }

/* `.install-bar` (the bottom XP-taskbar PWA install prompt) was removed in
   X99 2026-05-08 — it nagged first-time players during the join flow.
   Browser-native install (URL-bar menu / share sheet) is sufficient.
   If you reintroduce a custom install affordance, gate it on
   `roundsPlayed >= 1` and don't put it bottom-fixed during join. */

/* 10-landing.css — index.html only. Linked directly there.
   Was imported here through 2026-05-05 (X67). Kept off the shared
   @import chain so game pages (host/player/spectator/marketer/etc.)
   don't ship the ~9 KB of landing-page hero / hero-grid / recruiter
   chrome that only matters on /. The .hero selectors are not used
   anywhere else (verified by Grep, X67 2026-05-05). */

/* === inlined: styles/11-site-header.css === */
/* -----------------------------
   Memelord.com / merch.memelord.com style site header. Solid brand teal
   band, WHITE blocky pixel-font wordmark, right-side button. Mirrors the
   header on merch.memelord.com and memelord.com/dashboard directly 窶・   Captain supplied a reference screenshot; this matches it.
   Brand teal: sampled from the live merch site (~#00a9ac).
   ----------------------------- */
.site-header {
  position: relative;
  /* X148-2026-05-14: reverted X138's `max-width: 660px` cap. X138 read
     Captain's "the main screen header seems to be higher than the 660
     pixels" as a request to cap `.site-header` (the chrome that goes
     on every page) at 660, but Captain actually meant the LANDING
     HERO IMAGE on `/`. Capping the site-header broke the layout 窶・the
     navy band shrank to 660 centered while `.site-header-banner`
     (absolute-positioned at max-width: 92vw inside) overflowed it,
     leaving a giant pixel-art banner where the navy nav bar used to
     be. Site-header is back to spanning the viewport like every other
     production site's chrome row. The hero-image-only fix lives in
     X148's other edit (target: landing splash on `/`, not chrome). */
  width: 100%;
  /* Banner artwork ships its own sky/landscape so the bar is just a navy
     strip behind/around it. Bar min-height = banner height (104px, the
     restored 3:1 original) + 16px vertical breathing 竕・120px; absolute-
     positioned banner sits centered in that box, no overhang. (Was a flat
     teal bar with the banner overhanging via a navy `::before` shelf 窶・     the image's integrated scenery makes that framing redundant and the
     shelf is `display: none` below.) 2026-07-14: 96 竊・120 to seat the
     taller 3:1 banner (was 84px/8:1). */
  min-height: 120px;
  background: var(--ml-navy);
  border-bottom: 1px solid rgba(0, 0, 0, 0.25);
  padding: 8px 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
  z-index: 10;
}
/* Banner replaces the text wordmark 窶・hidden globally. */
.site-header .wordmark {
  display: none;
}
/* Legacy navy "shelf" (formerly framed the narrow MEMETICWARFARE.GAME
   wordmark banner). The new wide pixel-art banner ships its own
   sky/grass scenery so the shelf just clutters the bar 窶・kept the
   pseudo-element disabled here in case any inherited styling tries
   to bring it back. */
.site-header::before {
  display: none;
}

/* Hero banner 窶・the wide pixel-art MEMETICWARFARE.GAME / MEMETIC BOOT CAMP
   landscape (3546ﾃ・43, ~8:1 ratio). Sized to live entirely INSIDE the
   96px bar instead of overhanging like the old narrow wordmark used
   to. Absolute + double-translate centers it both axes; max-width
   92vw protects narrow desktops; pointer-events:none keeps it from
   eating clicks meant for the buttons that sit on top of it.
   z-index:0 keeps the layering intent explicit 窶・the action buttons
   in `.site-header-actions` are flex-flowed and stack above the
   absolute banner without needing their own z-index. Mobile (@640px,
   below) reverts to position:static so the banner stacks above the
   buttons in a column layout. */
.site-header-banner {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* 2026-07-14 (Captain): RESTORED the original clean 3:1 pixel-art banner
     (source: Apr 29 09_59_10 PM) over the May-1 "phone-mockup" remake that
     had been stretched into an 8:1 strip 窶・Captain: "a remake of a remake窶ｦ
     it's stretched weird." The forced `aspect-ratio: 8/1` + `object-fit:
     cover` were tuned for that phone-strip art and were exactly what made
     the banner read squashed/stretched; both are GONE now. The PNG is
     natively 3:1, so `height` drives the size and `width: auto` follows the
     intrinsic ratio 窶・no crop, no distortion. See docs/design/04-style-
     changelog.md 2026-07-14 + hand-off/GRAPHICS_ASSET_INDEX.md. */
  height: clamp(64px, 9vw, 88px); /* 2026-08-04: was fixed 104 — too big/small by page */
  width: auto;
  max-width: 92vw;        /* never overshoot the viewport on narrow desktops */
  max-height: 100%;
  object-fit: contain;    /* safety only 窶・native 3:1 art never needs a crop */
  display: block;
  pointer-events: none;
  z-index: 0;
}
/* The banner is now wrapped in a <picture> so the browser can swap to
   a mobile-tuned source at 竕､640px (see js/site-header.js). The wrapper
   has no styling intent of its own 窶・make it transparent in the flex
   layout so the inner <img class="site-header-banner"> participates
   as if it were still a direct flex child of .site-header. Without
   `display: contents` the picture wrapper would be a 0-height inline
   flex item on desktop (harmless) but on mobile (where the banner
   reverts to `position: static`) the picture would become a
   non-stretching inline flex item that prevents the img from sizing
   to 96vw. `display: contents` removes the box entirely. */
.site-header > picture {
  display: contents;
}

/* No more banner overhang to clear 窶・the banner is fully contained
   inside the bar now. Keep a small breathing gap so page content
   isn't flush against the bar's bottom edge. */
.site-header + main {
  margin-top: 16px;
}
.site-header .menu-btn {
  position: absolute;
  right: 18px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: transparent;             /* merch.memelord.com nav items are
                                          text-only white links on teal */
  color: #ffffff;
  border: none;
  font-family: var(--f-body);
  font-weight: 600;
  font-size: 14px;
  text-decoration: none;
  opacity: 0.95;
  transition: opacity 0.12s;
}
.site-header .menu-btn:hover { opacity: 1; text-decoration: underline; }
.site-header .menu-btn .xp-icon-tiny { filter: brightness(0) invert(1); opacity: 0.9; }

/* Action cluster 窶・takes remaining flex space in .site-header.
   Banner is a separate flex item to its left; see .site-header-banner.
   Left sub-group: [memelord.com][Boot Camp Manual].
   Right sub-group (margin-left:auto): [Log in / Logged in][Settings]. */
.site-header .site-header-actions {
  flex: 1;
  display: flex;
  align-items: center;
  min-width: 0;
}
.site-header .site-header-actions .menu-btn {
  position: static;
  right: auto;
  top: auto;
  transform: none;
}
.site-header .site-header-actions .settings-gear {
  display: inline-flex;
  align-items: center;
}
/* Left and right button groups inside .site-header-actions. */
.site-header .site-header-left {
  display: flex;
  align-items: center;
  gap: 8px;
}
.site-header .site-header-right {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;            /* pushes right group to the far right */
}
/* Header action buttons (MANUAL + Settings).
   2026-07-14 (Captain): "revert the manual and settings button to what we
   had back when it was Memelord.com." Reverted from the navy #02007F /
   cyan #2cf3d0 wordmark-and-cog chips back to the memelord.com gray Win95
   button look. The `.btn` base already inherits the #C0C0C0 raised-bevel
   chrome from 15-mlcom-overrides.css (memelord.com's exact button paint),
   so this rule only SIZES the button as an icon + uppercase label 窶・   matching memelord.com's "Missions" button (full-colour XP icon on a
   gray bevel). The `.ml-brand-wordmark-btn` / `.ml-com-wordmark-btn` /
   `.ml-brand-icon-btn` navy+cyan rules below are now DEAD (site-header.js
   no longer emits those classes) 窶・kept inert for one release, safe to
   delete in a later cleanup. */
.site-header .site-header-actions .btn.site-header-btn {
  width: auto;
  height: 36px;
  min-height: 0;            /* beat the base .btn 44px min-height */
  padding: 0 12px;
  font-size: 13px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  white-space: nowrap;
  text-decoration: none;
}
.site-header .site-header-actions .btn.site-header-btn img {
  width: 18px;
  height: 18px;
  display: block;
  flex-shrink: 0;
}

/* MEMELORD.COM wordmark button 窶・pixel-perfect match to the cyan-on-navy
   wordmark used in the memelord.com site header (see Captain's reference
   screenshot 2026-05-01 PM). Replaces the gray Win95 chip + my-computer
   icon that this slot used to render as. Key recipe:
     窶｢ Background: var(--ml-navy) #02007F 窶・same flat navy memelord.com uses
     窶｢ Text: #2cf3d0 cyan/teal 窶・the signature memelord.com wordmark color
     窶｢ Font: smooth bold sans-serif (Arial Black / Helvetica Neue) 窶・NOT
       the pixel W95FA from --f-display, because the memelord.com header
       wordmark is rendered in a clean rounded sans (visible in the ref
       screenshot 窶・the letterforms are smooth, not pixelated)
     窶｢ text-shadow: 0 2px 0 rgba(0,0,0,0.55) 窶・the dark drop under each
       letter that gives the wordmark its 3D pop in the reference
     窶｢ letter-spacing: 0.02em 窶・tight, matching the reference
   Selector specificity (extra .ml-com-wordmark-btn class) beats the
   28ﾃ・8 icon-chip default at `.site-header .site-header-actions .btn`
   without needing !important on layout rules; !important is used on
   background/color/font properties only as belt-and-braces against any
   late page-specific overrides (some pages re-declare .btn). Sized 36px
   tall with 16px horizontal padding so the wordmark has the same visual
   weight as the memelord.com header reference at the small chrome
   scale. */
.site-header .site-header-actions .btn.ml-com-wordmark-btn,
a.ml-com-wordmark-btn,
.site-header .site-header-actions .btn.ml-brand-wordmark-btn,
a.ml-brand-wordmark-btn {
  /* X17 (Captain feedback 2026-05-01c): X16's 44px / 18px target
     was too chunky next to the centered pixel-art banner; cut to
     24px tall / 11px font for MEMELORD.COM, 9px for MANUAL.
     X28 (Captain feedback 2026-05-03): the Settings gear chip
     is 32ﾃ・2 (rule at L竕・410) and the [MEMELORD.COM][MANUAL]
     pair was sitting 8px shorter than the gear, so the three
     header buttons didn't share a baseline. Bumped to 32px tall
     to match Settings; font + horizontal padding scaled by 32/24
     so the text doesn't drown in whitespace at the new height
     (11竊・4px on MEMELORD.COM, 7竊・px padding; MANUAL override
     below: 9竊・1px / 5竊・px). The bevel insets and outer drop-shadow
     stay at 1px / 2px because they're the brand-chrome signature;
     text-shadow stays at 1px because that's already the smallest
     readable drop. The mobile breakpoint at L竕・500 keeps its own
     36px / 16px values since the header collapses to flex-column
     on phones (banner stacks above the buttons, no horizontal
     alignment to honor) and that breakpoint also pins Settings to
     36ﾃ・6, so all three already line up there. */
  width: auto !important;
  height: 32px !important;
  padding: 0 9px !important;
  background: #02007F !important;
  border: 1px solid #01005a !important;
  border-radius: 4px;
  box-shadow:
    inset 1px 1px 0 rgba(255, 255, 255, 0.2),
    inset -1px -1px 0 rgba(0, 0, 0, 0.45),
    0 1px 2px rgba(0, 0, 0, 0.25);
  color: #2cf3d0 !important;
  font-family: 'Arial Black', 'Helvetica Neue', 'Lato', sans-serif !important;
  font-size: 14px !important;
  font-weight: 900 !important;
  letter-spacing: 0.02em;
  line-height: 1;
  text-decoration: none !important;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.55);
  transition: background 0.1s, color 0.1s, transform 0.05s;
}
.site-header .site-header-actions .btn.ml-com-wordmark-btn:hover,
a.ml-com-wordmark-btn:hover,
.site-header .site-header-actions .btn.ml-brand-wordmark-btn:hover,
a.ml-brand-wordmark-btn:hover {
  background: #0a08a5 !important;
  color: #6bf7df !important;
  text-decoration: none !important;
}
.site-header .site-header-actions .btn.ml-com-wordmark-btn:active,
a.ml-com-wordmark-btn:active,
.site-header .site-header-actions .btn.ml-brand-wordmark-btn:active,
a.ml-brand-wordmark-btn:active {
  transform: translateY(1px);
  box-shadow:
    inset 1px 1px 0 rgba(0, 0, 0, 0.4),
    inset -1px -1px 0 rgba(255, 255, 255, 0.1),
    0 0 0 rgba(0, 0, 0, 0);
}
.site-header .site-header-actions .btn.ml-com-wordmark-btn:focus-visible,
a.ml-com-wordmark-btn:focus-visible,
.site-header .site-header-actions .btn.ml-brand-wordmark-btn:focus-visible,
a.ml-brand-wordmark-btn:focus-visible {
  outline: 2px solid #6bf7df;
  outline-offset: 2px;
}
/* MANUAL chip 窶・visually subordinate to the MEMELORD.COM wordmark next to
   it. Captain feedback 2026-05-01a: button felt too big horizontally
   next to MEMELORD.COM. X17 (2026-05-01c) shrunk the whole cluster
   to 24px / 11px / 9px. X28 (2026-05-03) bumped the cluster to 32px
   to match the Settings gear chip; this rule keeps MANUAL one step
   smaller than its MEMELORD.COM sibling so it still reads as the
   secondary item in the pair 窶・font 11px (vs 14px) and tighter
   horizontal padding (7px vs 9px), proportional to the X17 ratios.
   !important wins against the shared rule's !important. Mobile
   breakpoint restores 36px / 12px values (see L竕・500). */
.site-header .site-header-actions .btn.ml-brand-wordmark-btn {
  padding: 0 7px !important;
  font-size: 11px !important;
  letter-spacing: 0.04em;
}

.ml-com-wordmark-text {
  display: inline-block;
  /* Inherits color / font / shadow from the parent button 窶・kept as a
     hook in case a future variant needs to style the text separately
     from the button frame. */
}

/* 笏笏 Cyan-on-navy brand icon buttons (Boot Camp Manual + Settings) 笏笏
   Visual sibling of `.ml-com-wordmark-btn` above. Same memelord.com
   palette 窶・flat #02007F navy face, dark #01005a hairline border, soft
   white-on-top / dark-on-bottom inset bevel 窶・but icon-only and square
   so the gear/help glyphs read as part of the same chrome family as
   the MEMELORD.COM wordmark chip. The XP icon `<img>` is recolored to
   the brand cyan #2cf3d0 with a CSS `filter` chain (brightness(0)
   flattens to solid black, then invert+sepia+saturate+hue-rotate paints
   it cyan, then a drop-shadow gives the same 3D pop the wordmark text
   gets from `text-shadow`). Higher specificity (`.btn.ml-brand-icon-btn`)
   beats the generic 28ﾃ・8 chip default at L2031 without `!important`
   on layout rules; visual properties carry `!important` for the same
   late-page-override defense the wordmark btn uses. */
.site-header .site-header-actions .btn.ml-brand-icon-btn {
  /* X16 (Captain 2026-05-01b): shrunk 36竊・2px square so the
     Settings chip joins the rest of the header cluster in the
     compact-chrome shrink. Inner icon shrinks 20竊・6px below so
     the icon-to-chip ratio stays the same as the previous
     20:36 (竕・5%) 竊・16:32 (=50%, close enough to read as the
     same family). */
  width: 32px !important;
  height: 32px !important;
  padding: 0 !important;
  background: #02007F !important;
  border: 1px solid #01005a !important;
  border-radius: 4px;
  box-shadow:
    inset 1px 1px 0 rgba(255, 255, 255, 0.2),
    inset -1px -1px 0 rgba(0, 0, 0, 0.45),
    0 1px 2px rgba(0, 0, 0, 0.25);
  color: #2cf3d0 !important;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.1s, color 0.1s, transform 0.05s;
}
.site-header .site-header-actions .btn.ml-brand-icon-btn:hover {
  background: #0a08a5 !important;
}
.site-header .site-header-actions .btn.ml-brand-icon-btn:active {
  transform: translateY(1px);
  box-shadow:
    inset 1px 1px 0 rgba(0, 0, 0, 0.4),
    inset -1px -1px 0 rgba(255, 255, 255, 0.1);
}
.site-header .site-header-actions .btn.ml-brand-icon-btn:focus-visible {
  outline: 2px solid #6bf7df;
  outline-offset: 2px;
}
.site-header .site-header-actions .btn.ml-brand-icon-btn img {
  width: 16px;
  height: 16px;
  /* Recolor the multi-color XP PNG to brand cyan #2cf3d0:
     1) brightness(0) saturate(100%) 竊・flatten to solid black silhouette
     2) invert + sepia + saturate + hue-rotate 竊・paint it cyan
     3) drop-shadow 竊・match the wordmark's 3D pop  */
  filter:
    brightness(0) saturate(100%)
    invert(86%) sepia(45%) saturate(2390%)
    hue-rotate(118deg) brightness(102%) contrast(89%)
    drop-shadow(0 1px 0 rgba(0, 0, 0, 0.55));
}

/* The memeticwarfare.game page: a site header, then content on the Bliss
   background. Unlike the old design, there is NO single big container
   covering the wallpaper 窶・cards float as their own XP windows and
   the Bliss hills are visible around them.

   2026-05-05 (X64): .page bumped 560px 竊・880px on desktop, matching
   the .container revert in 01-style-guard.css. Same friend-reaction
   fix: the prior 560px narrowing left landing.html showing a tight
   single-column stack adrift in XP wallpaper, and Captain wanted
   desktop to use a noticeable chunk of the screen without going all
   the way back to the historical 1100px. The hero-art (10-landing.css)
   stays at its native 560px width so the pixel-art panel doesn't
   scale up blurry; the .hero-grid's existing -20px escape margin
   then pulls the four landing cards out to the full 880px lane on
   desktop, which is exactly the visible-content growth Captain asked
   for. Mobile (<=640px viewport) never hits this max-width because
   17-mobile-tweaks.css drops .page padding to 12px and the viewport
   itself is the limiting factor. See docs/design/04-style-changelog.md
   2026-05-05 entry for the framing. */
/* History note: 560 (X64-era) 竊・880 (X64+X65 lane unification) 竊・   720 (X78-2026-05-08 revert, "feels too wide") 竊・660 (X79-2026-05-08,
   "let's try 660 px on everything"). The 880 narrative in the comment
   block above documents how we got here; lane is now 660. */
.page {
  /* X86-2026-05-08: horizontal padding 20px 竊・24px so the landing
     page's content area lines up with `.container` (24px) and
     `.player-container` (24px post-X86). Pre-X86 the landing felt
     "much wider" (Captain) because the content area at 660 - 40 =
     620px was 8px wider than host/player/spectator at 660 - 48 =
     612px AND `.hero-grid` had a -20px escape margin pulling the
     four landing cards out to the full 660 lane (see `.hero-grid`
     in 10-landing.css for the matching escape-margin update). Top /
     bottom padding stays asymmetric (24px / 40px) 窶・the bottom
     breathing room is intentional for the landing's vertical scroll
     past the four cards into the footer. */
  padding: 24px 24px 40px;
  max-width: 660px;
  margin: 0 auto;
}

/* Dedicated Start-bar窶都tyled header up top (added in index.html) */
.xp-titlebar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 14px 7px;
  background:
    linear-gradient(180deg,
      var(--luna-blue-1) 0%,
      #2a79ef 48%,
      var(--luna-blue-3) 100%);
  color: #fff;
  font-family: var(--f-body);
  font-weight: 700;
  font-size: 0.95rem;
  border: 1px solid var(--luna-blue-edge);
  border-radius: 8px 8px 0 0;
  box-shadow: inset 0 1px 0 var(--luna-blue-hi);
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.4);
}
.xp-titlebar .controls {
  margin-left: auto;
  display: flex;
  gap: 3px;
}
.xp-titlebar .controls span {
  width: 22px; height: 20px;
  border: 1px solid var(--luna-blue-dark);
  background:
    linear-gradient(180deg, #6caeed 0%, #2a79ef 100%);
  border-radius: 3px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  color: #fff;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.35);
}
.xp-titlebar .controls .close {
  background:
    linear-gradient(180deg, #ee7a5d 0%, #c12b1d 100%);
  border-color: #7c1410;
}

/* X31-2026-05-03: Riah Studio badge sits in normal document flow at
   the natural end of the page 窶・NOT pinned to the viewport. Captain:
   "I don't want the RiahStudio button to remain static on the page.
   It should ONLY be at the very bottom of the page." Previously this
   rule used `position: fixed !important` with bottom/right anchors so
   the badge floated in the lower-right corner over scrolling content;
   that meant the signature was always overlaying whatever section the
   user was reading on a long page (lobby pills, leaderboard rows,
   submit grid, etc.) and was particularly intrusive on the index
   landing where the four landing cards extend below the fold.
   Removing `position: fixed` puts the badge inline at the end of
   `<main>` (or after `<main>`) on every page that includes
   `<p class="footer-tag">窶ｦ</p>` 窶・host, player, spectator, index,
   marketermode, marketer, submit 窶・and `text-align: center` puts it
   in the center of the page width. Margin-bottom uses
   `env(safe-area-inset-bottom)` to keep clearance over the iOS home
   indicator on phones; margin-top adds breathing space from whatever
   page content sits above. submit.html still carries an inline
   `style="margin-top: 20px"` and that wins for the top margin there
   (acceptable 窶・its layout intent predates this rule). */
.footer-tag {
  font-family: var(--f-pixel);
  font-size: 0.58rem;
  color: #fff;
  text-align: center;
  letter-spacing: 0.12em;
  text-shadow: 1px 1px 0 #000;
  margin: 24px 0 max(16px, env(safe-area-inset-bottom, 16px));
}
.footer-tag a { color: #fff; text-decoration: underline; }

/* "Made by RIAHSTUDIO" pixel-art badge. Sits centered at the natural
   end of the page (in normal flow inside its `.footer-tag` wrapper 窶・   see X31-2026-05-03 above). Reduced to 28px tall so it reads as a
   subtle signature, not a banner. Pixelated rendering preserves the
   chunky retro letterforms; drop-shadow gives it a tiny bit of pop.
   Slight opacity (0.85) for a softer visual presence without
   sacrificing readability. Height stays well under the 40px design
   bible spec. */
.footer-riah {
  display: inline-block;
  height: 22px;
  width: auto;
  vertical-align: middle;
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
  filter: drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.45));
  opacity: 0.85;
}


/* === inlined: styles/12-avatars.css === */
/* ==========================================================================
   Wojak avatars — shared picker + pill rendering
   Every player's chosen Wojak rides along in room:state as player.avatar.
   Renders come from MemelordAvatars.imgHtml(id, 'pill-avatar').
   ========================================================================== */

.wojak-avatar {
  display: inline-block;
  image-rendering: -webkit-optimize-contrast;
  user-select: none;
  -webkit-user-drag: none;
  vertical-align: middle;
}
.pill-avatar {
  width: 22px;
  height: 22px;
  margin-right: 8px;
  border: 1px solid #111;
  background: #fff;
  box-shadow: 1px 1px 0 rgba(0,0,0,0.35);
  flex: 0 0 auto;
}
.score-row .pill-avatar,
.player-pill .pill-avatar {
  width: 24px;
  height: 24px;
}

/* Avatar picker — XP-style grid of 6-wide thumbs. Responsive: scales to
   4-wide on narrow phones. */
.avatar-picker-mount {
  margin-top: 4px;
  padding: 6px;
  background: #ffffff;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-sh),
    inset -1px -1px 0 var(--ml-gray-hi-2),
    inset  2px  2px 0 var(--ml-gray-sh-2),
    inset -2px -2px 0 var(--ml-gray-hi);
}
.avatar-picker {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 4px;
  /* Cap and center so the host (.container, 1100px) and the player
     (.player-container, 560px) end up with matching tile sizes. Without
     this cap the host card stretches to ~1100px and the 6-column grid
     blows the wojak tiles up to ~165px each, while the player on the
     same screen sees ~85px tiles. 540px ≈ what the player gets on
     desktop (560 card width minus internal padding), so this preserves
     the player tile size and only shrinks the host. */
  max-width: 540px;
  margin: 0 auto;
}
@media (max-width: 420px) {
  .avatar-picker { grid-template-columns: repeat(5, 1fr); }
}
.avatar-chip {
  position: relative;
  background: var(--ml-gray);
  border: 0;
  padding: 2px;
  cursor: pointer;
  aspect-ratio: 1 / 1;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-hi-2),
    inset -1px -1px 0 var(--ml-gray-sh-2),
    inset  2px  2px 0 var(--ml-gray-hi),
    inset -2px -2px 0 var(--ml-gray-sh);
  transition: none;
}
.avatar-chip:hover {
  background: #e8e4d0;
}
.avatar-chip.selected {
  background: var(--ml-gold);
  box-shadow:
    inset  1px  1px 0 #a17d00,
    inset -1px -1px 0 #fff7b8,
    inset  2px  2px 0 #a17d00,
    inset -2px -2px 0 #fff7b8;
}
.avatar-chip.locked {
  opacity: 0.55;
  cursor: not-allowed;
}
.avatar-chip.locked:hover { background: var(--ml-gray); }
.avatar-chip-img {
  width: 100%;
  height: 100%;
  display: block;
  /* Some of the Wojak sources are framed tight to the edges, so `cover`
     lopped off the tops of heads and chins. `contain` keeps the whole
     face visible. The chip backdrop is a flat gray (see .avatar-chip
     above) — the prior sky→grass bliss gradient was removed 2026-05-05
     per Captain ("remove the vertical Bliss strips around all of the
     Wojak characters"). */
  object-fit: contain;
  object-position: center;
  padding: 2px;
  background: transparent;
  position: relative;
  z-index: 1;
}
/* Wojak chip backdrop history: a sky→grass bliss gradient lived here
   2026-05-01 → 2026-05-05 (.avatar-chip::before painted under every
   chip; X49 unmasked it on the 23 square wojaks via PNG alpha-channel
   side strips). Captain reversed the look 2026-05-05 ("remove the
   vertical Bliss strips around all of the Wojak characters"), so the
   ::before pseudo-elements were dropped. Selected state still reads
   via the chip's own gold background (.avatar-chip.selected below) and
   the gold bevel box-shadow; locked state still reads via the outer
   opacity: 0.55 on .avatar-chip.locked. The 23 PNGs from X49 retain
   their transparent side strips — those now reveal the chip's flat
   gray (var(--ml-gray)) instead of the gradient, which is the desired
   "no bliss strip" look. */
.avatar-lock {
  position: absolute;
  top: 0; right: 2px;
  font-size: 0.85rem;
  filter: drop-shadow(1px 1px 0 #fff);
  pointer-events: none;
}
.avatar-hint {
  font-size: 0.72rem;
  margin: 6px 0 0;
  color: var(--muted);
  font-style: italic;
}


/* === inlined: styles/13-mission-complete.css === */
/* ==========================================================================
   MISSION COMPLETE 窶・end-of-game screen
   public/img/hero-mission-complete.png carries the whole arcade-ending vibe.
   On top of it we paste 窶・intentionally badly 窶・the winner's Wojak over
   the honor-recipient's face, and their name over the word "SOLDIER" in
   "CONGRATULATIONS, SOLDIER!". The bad-Photoshop aesthetic IS the joke:
   white crop-edges, slight tilt, Comic Sans red, off-saturation.
   ========================================================================== */

.academy-scene {
  position: relative;
  margin: 18px 0 24px;
  padding: 0;
  background: #000;
  /* 2026-08-04: fit the end card in the viewport — was overflowing past the fold */
  max-width: 660px;
  max-height: min(78vh, 900px);
  margin-left: auto;
  margin-right: auto;
  border: 3px solid #000;
  box-shadow:
    inset 0 0 0 2px #FFD700,
    0 6px 0 #000,
    0 12px 30px rgba(0,0,0,0.55);
  overflow: hidden;
  animation: missionFlashIn 0.7s cubic-bezier(.18,.89,.32,1.28) both;
}
@keyframes missionFlashIn {
  0%   { transform: scale(0.85); opacity: 0; filter: brightness(2.4); }
  60%  { transform: scale(1.02); opacity: 1; filter: brightness(1.2); }
  100% { transform: scale(1);    opacity: 1; filter: brightness(1);   }
}
@media (prefers-reduced-motion: reduce) {
  .academy-scene { animation: none; }
}

/* Hero frame is the positioning anchor for the overlays. It has zero
   line-height so the <img> fills it with no phantom descender gap. */
.academy-hero-frame {
  position: relative;
  display: block;
  line-height: 0;
}
.academy-hero {
  display: block;
  width: 100%;
  height: auto;
  max-height: min(72vh, 820px);
  object-fit: contain;
  /* Pixel art. Do NOT let the browser smooth it into mush. */
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
  image-rendering: crisp-edges;
  background: #000;
}

/* Badly-Photoshopped face 窶・pasted over the honor-recipient wojak on the
   right. Coordinates are percentages of the image so they scale with it.
   Image is 1402ﾃ・122; the actual head on the hero sits at roughly
   (x=71%, y=26%), spans ~17% ﾃ・20% of the base.

   X??-2026-05-14 (Captain QA): "The face has way too much white space
   around it. It's covering up other things than just the face." The
   underlying issue was the face PNGs in img/wojaks/faces/ had opaque
   off-white (~245 RGB) backgrounds that PIL/the prior cropping pipeline
   never made transparent, so the overlay's rectangular box painted a
   white halo over the helmet / sky / trophy area around the recipient's
   head. Two coordinated fixes:
     1. Asset side 窶・every face PNG re-cropped to its content bbox and
        flood-filled from corners with transparent alpha (preserves the
        wojak's interior white skin, only the surrounding background is
        clipped). See /tmp pipeline notes in the handoff snippet.
     2. CSS side (this rule) 窶・aspect-ratio 3/2 landscape 竊・5/6 portrait
        so the box matches the now-portrait crops; top 34% 竊・26% so it
        lands on the actual face on the hero instead of below it; width
        22% 竊・17% so the overlay covers just the head rather than
        bleeding into the helmet/sky/trophy. The drop-shadow + tilt +
        hue shift stay 窶・the joke is still a sloppy edit, just one that
        respects the silhouette of the head it's covering.

   Earlier history: X108-2026-05-10 removed the white crop-edge border
   and bumped width 14% 竊・22% to compensate for the same "white box"
   look 窶・but the source of the white was the PNG background, not the
   CSS, so the symptom kept resurfacing. This pass cuts the white at
   the source. X85-2026-05-08 aspect-ratio 4/5 竊・3/2. */
.academy-face-photoshop {
  position: absolute;
  left: 71%;
  /* X??-2026-05-15 (Captain QA): "I prefer if it was a tiny bit closer
     to the character's face." The 2026-05-14 pass landed at top: 26%
     based on a measurement of the wojak's literal pixel center, but
     visually the bearded character's perceived face-center sits a bit
     lower because of his hair volume above the brow. Nudge 26% 竊・28%
     so the overlay reads as ON the face, not above it. */
  top: 28%;
  width: 17%;
  /* Portrait 窶・matches the tight head-only crops in img/wojaks/faces/
     after the 2026-05-14 re-crop pass (average AR ~0.85 portrait). */
  aspect-ratio: 5 / 6;
  transform: translate(-50%, -50%) rotate(-5deg);
  /* X154-2026-05-15 (Captain follow-up: "the white background was
     supposed to be smaller than it was the last time, because the last
     time it was about the width of the total face in excess white
     space. We wanted the white space, just not anything much going
     beyond the face size itself.").
     History: the 2026-05-14 re-crop pass made each face PNG's corners
     transparent to kill the X108 white halo. That over-corrected 窶・the
     painted hero face bled through, every winner looked like the same
     painted Memelord. The first X154 attempt put `background: #fff`
     back on the whole box, which covered the painted face but exposed
     the FULL transparent corner area of the PNG (~21-25% of the box)
     as visible white, recreating the X108 halo.
     This pass keeps the white background (so the painted face stays
     covered) but pushes the transparent corners off-canvas with a
     1.2ﾃ・scale on the inner img + `overflow: hidden` here. The
     visible white now sits as a thin sticker crop-edge (a couple
     percent of the box width) instead of a halo as wide as the face.
     The inner img scale is in `.academy-face-photoshop img` below. */
  background: #fff;
  overflow: hidden;
  box-shadow:
    1px 1px 0 rgba(0,0,0,0.55),
    3px 4px 0 rgba(0,0,0,0.45),
    5px 6px 6px rgba(0,0,0,0.45);
  /* Slight mismatched color so it doesn't blend 窶・scuffed JPEG vibe. */
  filter: saturate(1.25) contrast(1.08) hue-rotate(4deg);
  z-index: 2;
  pointer-events: none;
  line-height: 0;
}
.academy-face-photoshop img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  /* Anchor to the top of the crop so the face sits high in the
     sticker (eyes near vertical center) instead of the chin / neck
     dominating the frame. The face crops are head-down; "center top"
     keeps the eye line in view even when cover-fitting trims the
     bottom. */
  object-position: center top;
  /* X154-2026-05-15 窶・scale up so the face PNG's transparent corners
     (the flood-filled background from the 2026-05-14 re-crop pass)
     overflow the parent and clip via the parent's `overflow: hidden`.
     The wojak's opaque head silhouette now fills the visible box
     edge-to-edge; only a thin white sticker crop-edge remains (the
     intentional bad-photoshop aesthetic), not the X108-era "halo as
     wide as the face." 1.2ﾃ・is the sweet spot 窶・bigger than ~1.15
     and the chin clips on tall wojaks; smaller and ~5-10% of visible
     transparent corner survives at the top corners. transform-origin
     defaults to center but the parent uses object-position: center top
     so the visible region biases toward the top of the head 窶・chin
     clipping is the safer side to accept. */
  transform: scale(1.2);
  /* Extra JPEG-ish fuzz */
  filter: contrast(1.05) saturate(1.1);
}

/* Badly-Photoshopped name 窶・yellow pill with red Comic-Sans text and a
   chunky black outline.

   X108-2026-05-10 (Captain QA, after ~8 prior threads): "the name tag
   is still messed up窶ｦ please go over it again and again until you can
   actually read who won. And it should go over the word SOLDIER."

   The CONGRATULATIONS, SOLDIER! line in img/hero-mission-complete.png
   sits inside the dark teal panel at roughly (left=15%, right=72%) ﾃ・   (top=63%, bottom=78%). The "SOLDIER!" word is in the right half of
   the centered text 窶・its center lands at roughly (x=58%, y=68%) of
   the 1402ﾃ・122 base.

   Earlier values (left=63%, top=69%) had the pill's center past the
   right edge of "SOLDIER!" 窶・Captain's screenshot showed the tag
   floating over the soldier's chest rather than over the word. Moved
   to (left=58%, top=72%) and added `min-width: 22%` so the pill is
   physically big enough to cover the word even for a short handle
   like "HOST" (4 chars).

   Font-size lineage:
   - X66 original: clamp(.95rem, 2.6vw, 1.7rem) 窶・Captain: "tiny."
   - X108-2026-05-10 竊・clamp(1.5rem, 4.6vw, 2.6rem). Captain 2026-05-15
     on a "LITTLEFOOT'S MEMERY" handle: "you finally, for the first
     time ever in months, got the player name to be bigger. The problem
     is now it's gigantic." Visible truncation on the screenshot
     ("LITTLEFOOT'S M窶ｦ") confirmed the pill was hitting max-width: 80%
     and ellipsizing.
   - X??-2026-05-15 (this rule): clamp(1.2rem, 3.4vw, 2rem) 窶・split the
     difference between the two prior extremes (~23% smaller than X108).
     Padding also trimmed 10/18/12 竊・7/14/8 so the pill's overall
     footprint comes down proportionally, not just the glyph height.
     A 12-char handle like "LITTLEFOOT'S" should now fit inside the
     22%窶・0% width band without ellipsis at desktop widths.

   Earlier history: X85-2026-05-08 bumped font ~1.4ﾃ・and widened
   max-width 56% 竊・80%. */
.academy-name-photoshop {
  position: absolute;
  left: 58%;
  top: 72%;
  transform: translate(-50%, -50%) rotate(-4deg);
  /* X162-2026-05-15: Captain still wanted the pill smaller after the
     X154 trim. "Not as small as it used to be when it was never
     working, but smaller than it is now." Lineage:
       - X66 (Captain: "tiny"):     clamp(.95rem, 2.6vw, 1.7rem) / padding 5px 10px 6px
       - X108 (Captain: "gigantic"): clamp(1.5rem, 4.6vw, 2.6rem) / padding 10px 18px 12px
       - X154 (still too big):       clamp(1.2rem, 3.4vw, 2rem)   / padding 7px 14px 8px
       - X162 (this pass):           clamp(1.05rem, 3vw, 1.85rem)  / padding 5px 11px 6px
     New clamp + padding are the arithmetic midpoint of X66 and X154 窶・     ~12% smaller than X154 across the clamp band, padding scaled in
     lockstep so the pill footprint comes down with the glyph height
     (Captain: "smaller font and smaller box"). Mobile clamp at the
     bottom of this file mirrors the same proportional step down.
     If X162 still reads "too big" the next step toward X66 would be
     clamp(1rem, 2.8vw, 1.75rem) / padding 4px 10px 5px. */
  padding: 5px 11px 6px;
  background: #FFD94A;
  color: #cc0000;
  font-family: "Comic Sans MS", "Comic Sans", "Chalkboard SE", cursive;
  font-weight: 800;
  font-style: italic;
  font-size: clamp(1.05rem, 3vw, 1.85rem);
  /* X??-2026-05-14: parent .academy-hero-frame sets line-height: 0 to kill
     the image's phantom descender gap, and that 0 inherits into this pill,
     collapsing its line box to zero. Combined with overflow: hidden the
     letters were clipped top and bottom 窶・"HOST" rendered as a sliver.
     Force an explicit line-height so the pill grows to fit the glyphs. */
  line-height: 1.15;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
  /* Pill must physically cover "SOLDIER!" 窶・short handles like HOST
     get padded out to the SOLDIER footprint by min-width. Long handles
     can still extend past, capped by max-width so they don't ramp
     across the medal. */
  min-width: 22%;
  max-width: 80%;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  border: 2px solid #fff;
  box-shadow:
    1px 1px 0 #000,
    3px 4px 0 rgba(0,0,0,0.45),
    6px 7px 7px rgba(0,0,0,0.4);
  text-shadow: 1px 1px 0 #000;
  z-index: 3;
  pointer-events: none;
  box-sizing: border-box;
}

/* Name "lands" with weight 窶・the overlay starts collapsed + invisible,
   slams down past full size, settles at rest. A brief golden glow halo
   fades behind it on the same beat so it reads as earned, not stamped.
   Triggered once per game by host.js adding the --pop class when
   renderAcademy() runs. CSS keyframes preserve the base translate/rotate
   so the rest position matches the static rule above. */
.academy-name-photoshop--pop {
  animation: academyNamePop 720ms cubic-bezier(.18,.89,.32,1.45) both;
}
.academy-name-photoshop--pop::before {
  content: '';
  position: absolute;
  inset: -28% -16%;
  background: radial-gradient(closest-side, rgba(255,217,74,0.85), rgba(255,170,0,0.35) 55%, rgba(255,170,0,0) 78%);
  border-radius: 50%;
  z-index: -1;
  pointer-events: none;
  animation: academyNameGlow 900ms ease-out both;
}
@keyframes academyNamePop {
  0%   { transform: translate(-50%, -50%) rotate(-4deg) scale(0.05); opacity: 0; filter: brightness(2.2); }
  55%  { transform: translate(-50%, -50%) rotate(-4deg) scale(1.32);  opacity: 1; filter: brightness(1.35); }
  78%  { transform: translate(-50%, -50%) rotate(-4deg) scale(0.94);  opacity: 1; filter: brightness(1.05); }
  100% { transform: translate(-50%, -50%) rotate(-4deg) scale(1);     opacity: 1; filter: brightness(1); }
}
@keyframes academyNameGlow {
  0%   { opacity: 0;    transform: scale(0.5); }
  35%  { opacity: 0.95; transform: scale(1.15); }
  100% { opacity: 0;    transform: scale(1.6); }
}
@media (prefers-reduced-motion: reduce) {
  .academy-name-photoshop--pop,
  .academy-name-photoshop--pop::before { animation: none; }
  .academy-name-photoshop--pop::before { display: none; }
}

/* Mobile: the target regions on a narrow viewport can crowd each other.
   2026-05-14: desktop width dropped to 17% to match the actual face
   on the hero (see main rule); mobile follows the same downsize from
   26% 竊・20% 窶・still a touch bigger than desktop so the head reads on a
   phone, but no longer overshooting the helmet/sky region. */
@media (max-width: 560px) {
  .academy-face-photoshop { width: 20%; }
  /* Mobile dialed-back in lockstep with the desktop X??-2026-05-15
     trim (was clamp(1.2rem, 6.4vw, 2rem)). Still reads on a phone, no
     longer gigantic. X162-2026-05-15: another step down to track the
     desktop X162 trim. Pre-X162 was clamp(1rem, 4.8vw, 1.55rem); new
     value is clamp(0.9rem, 4.2vw, 1.4rem) 窶・same ~12% trim as the
     desktop clamp midpoint move. */
  .academy-name-photoshop { font-size: clamp(0.9rem, 4.2vw, 1.4rem); }
}


/* === inlined: styles/14-a11y-settings.css === */
/* ==========================================================================
   Accessibility primitives
   ========================================================================== */
.skip-link,
.sr-only { position: absolute; }
.sr-only {
  width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0);
  white-space: nowrap; border: 0;
}
.skip-link {
  top: 10px; left: 10px; z-index: 200;
  transform: translateY(-160%);
  background: var(--ml-green);
  color: #000;
  padding: 8px 12px;
  border-radius: 6px;
  font-weight: 700;
  text-decoration: none;
  transition: transform 160ms ease-out;
}
.skip-link:focus {
  transform: translateY(0);
  outline: 2px solid #000;
  outline-offset: 2px;
}

/* ---------- Toast notifications ---------- */
.toast-container {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1000;
  display: flex;
  flex-direction: column-reverse;
  gap: 8px;
  pointer-events: none;
  max-width: 90vw;
  width: min(480px, 90vw);
}
.toast {
  background:
    linear-gradient(180deg, #ffffff 0%, var(--luna-grey-face) 100%);
  color: #000;
  border: 1px solid #7b7967;
  border-radius: 6px;
  padding: 10px 14px;
  font-family: var(--f-body);
  font-weight: 700;
  font-size: 13px;
  box-shadow:
    inset 0 1px 0 #fff,
    0 6px 14px rgba(0, 0, 0, 0.35);
  pointer-events: auto;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 180ms ease-out, transform 180ms ease-out;
  animation-fill-mode: forwards;
}
.toast.toast-visible {
  opacity: 1;
  transform: translateY(0);
}
.toast.toast-error {
  border-color: var(--danger);
  color: #6a1011;
}
.toast.toast-success {
  border-color: #1e8554;
  color: #124c2f;
}
.toast.toast-info {
  border-color: #2e7db5;
  color: #1d4a73;
}
@media (prefers-reduced-motion: reduce) {
  .toast {
    transition: opacity 60ms linear;
    transform: none;
  }
}

/* ==========================================================================
   Collapsible Game Settings card (host lobby)
   ========================================================================== */
.settings-card { padding: 0; }
.settings-card > summary {
  list-style: none;
  cursor: pointer;
  padding: 8px 14px;
  font-family: var(--f-body);
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: 0;
  color: var(--luna-title-text);
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.40);
  background:
    linear-gradient(180deg,
      var(--luna-blue-1) 0%,
      #2a79ef 48%,
      var(--luna-blue-3) 100%);
  border-bottom: 1px solid var(--luna-blue-dark);
  display: flex;
  align-items: center;
  justify-content: space-between;
  user-select: none;
}
.settings-card > summary::-webkit-details-marker { display: none; }
.settings-card > summary::after {
  content: "▸";
  font-size: 1rem;
  transition: transform 120ms ease;
  color: #fff;
}
.settings-card[open] > summary::after { transform: rotate(90deg); }
.settings-card > summary:focus-visible {
  outline: 2px solid #fff;
  outline-offset: -2px;
}
.settings-body {
  padding: 10px 16px 14px;
}
@media (prefers-reduced-motion: reduce) {
  .settings-card > summary::after { transition: none; }
}


/* === inlined: styles/15-mlcom-overrides.css === */
/* ==========================================================================
   MEMELORD.COM 95/98 OVERRIDE LAYER — loaded last so it wins by cascade.
   Rewires all button chrome, window chrome, card chrome, and headings to
   the exact Win95 look memelord.com ships. Do not reorder below this.
   ========================================================================== */

/* Win95 raised-bevel button — flat #C0C0C0 with a 2px hand-drawn bevel
   (white top-left, dark gray bottom-right). Uppercase, Lato Heavy.
   On :active the bevel inverts so the button looks pressed.
   X123-2026-05-12 (Captain): font-family swapped from `var(--f-display)`
   to a Lato-first stack so button labels read as Lato Heavy. Pre-X123
   `--f-display` listed W95FA first, which meant every button label
   rendered in the chunky pixel font once the W95FA CDN was added
   (Google Fonts–style @import in 00-base.css). Captain QA on the four
   landing-card buttons: "what happened to the font style on the
   buttons here? It looks like it got changed for some reason. It
   should go back to what it was before. It was looking nice." Title
   bars + headings keep `--f-display` (intentionally W95FA-first per
   the memelord.com brand) — this is a button-specific revert. */
.btn, .btn-secondary, .btn-accent2, button.btn, a.btn {
  background: var(--ml-gray) !important;
  background-image: none !important;
  color: #262626 !important;
  font-family: 'Lato', 'Helvetica Neue', Helvetica, Arial, sans-serif !important;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  border: 0 !important;
  border-radius: 0 !important;
  padding: 10px 18px;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-hi-2),
    inset -1px -1px 0 var(--ml-gray-sh-2),
    inset  2px  2px 0 var(--ml-gray-hi),
    inset -2px -2px 0 var(--ml-gray-sh) !important;
  text-shadow: none;
  cursor: pointer;
  transition: none;
}
.btn:hover, .btn-secondary:hover, .btn-accent2:hover { filter: brightness(1.04); }
.btn:active, .btn-secondary:active, .btn-accent2:active {
  box-shadow:
    inset -1px -1px 0 var(--ml-gray-hi-2),
    inset  1px  1px 0 var(--ml-gray-sh-2),
    inset -2px -2px 0 var(--ml-gray-hi),
    inset  2px  2px 0 var(--ml-gray-sh) !important;
  transform: translate(1px, 1px);
}
/* Secondary = same chrome, navy text */
.btn-secondary { color: var(--ml-navy) !important; }
/* Accent = gold bar under the label */
.btn-accent2 {
  color: var(--ml-ink) !important;
  border-bottom: 3px solid var(--ml-gold) !important;
}

/* Card / window chrome — #C0C0C0 face, navy title bar, square corners,
   dropshadow like a window cast on a desktop. */
.card, .panel, .window, .settings-card {
  background: var(--ml-gray) !important;
  background-image: none !important;
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-hi-2),
    inset -1px -1px 0 var(--ml-gray-sh-2),
    inset  2px  2px 0 var(--ml-gray-hi),
    inset -2px -2px 0 var(--ml-gray-sh),
    4px 4px 0 rgba(0, 0, 0, 0.35) !important;
  color: var(--ml-ink);
}

/* Card titles become navy title bars, matching memelord.com's window chrome. */
.card > h2, .panel > h2, .window > h2, .settings-card > summary {
  background: var(--ml-navy) !important;
  background-image: none !important;
  color: #ffffff !important;
  font-family: var(--f-display) !important;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  border: 0 !important;
  border-radius: 0 !important;
  padding: 6px 10px !important;
  margin: -10px -10px 10px !important;
  text-shadow: none;
}

/* Site header → classic memelord.com navy title bar. */
.site-header {
  background: var(--ml-navy) !important;
  background-image: none !important;
  border-radius: 0 !important;
  border-bottom: 2px solid var(--ml-navy-dark) !important;
  color: #ffffff !important;
}
.site-header .wordmark {
  font-family: var(--f-display) !important;
  color: #ffffff !important;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.site-header .menu-btn {
  background: var(--ml-gray) !important;
  color: var(--ml-ink) !important;
  border: 0 !important;
  border-radius: 0 !important;
  font-family: var(--f-display) !important;
  text-transform: uppercase;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-hi-2),
    inset -1px -1px 0 var(--ml-gray-sh-2),
    inset  2px  2px 0 var(--ml-gray-hi),
    inset -2px -2px 0 var(--ml-gray-sh) !important;
}

/* Headings — W95FA, square, ink-black, gold underline for h1 per memelord.com */
h1, h2, h3 { font-family: var(--f-display) !important; color: var(--ml-ink) !important; }
h1 { text-transform: uppercase; letter-spacing: 0.02em; }
.hero h1 { border-bottom: 4px solid var(--ml-gold); padding-bottom: 6px; display: inline-block; }
.hero .hot  { color: var(--ml-red); }
.hero .pink { color: var(--ml-gold); }

/* Kicker + tagline in bitmap type, memelord.com-style. */
.kicker, .tagline, .footer-tag {
  font-family: var(--f-display) !important;
  color: var(--ml-ink);
}

/* Feature icon shake on hover — ported from memelord.com's featureIconShake */
@keyframes ml95shake {
  0%,100% { transform: translate(0,0) rotate(0deg); }
  25%     { transform: translate(-1px,-1px) rotate(-3deg); }
  50%     { transform: translate( 1px,-1px) rotate( 3deg); }
  75%     { transform: translate(-1px, 1px) rotate(-2deg); }
}
.card:hover .xp-icon,
.xp-icon:hover,
.xp-icon-lg:hover {
  animation: ml95shake 0.35s linear infinite;
}

/* Links — classic Win95 navy/underline */
a { color: var(--ml-navy) !important; text-decoration: underline; }
a:hover { color: var(--ml-navy-hi) !important; }

/* X127-2026-05-12: anchors that wear button chrome must NOT inherit the
   universal Win95 link styling above. Background story: X126 converted
   six landing-page <button> elements to <a href> so right-click →
   "Open in new tab" works on the PLAY / SITUATION ROOM hero CTAs and
   the four landing-card actions. The .btn / .hero-cta classes still
   paint the 3D button chrome correctly, but the universal `a` rule
   above was overlaying navy text + an underline beneath the label —
   Captain QA on the hero CTAs: "what happened to the really cool
   looking buttons we had? Why are they gone?" They weren't gone, just
   wearing a link costume. This rule strips the costume.
   - text-decoration: none — kills the underline beneath the label on
     every button-class anchor (.btn / .btn-secondary / .btn-accent2 /
     .hero-cta / .hero-cta-situation).
   - color overrides on the two hero CTAs restore the intended palette
     (#fff for PLAY, --ml-green-hi for SITUATION ROOM); without these,
     the universal `a { color: navy !important }` was painting both
     labels navy regardless of what 10-landing.css said. The .btn
     family already carries `color !important` from the override block
     ~110 lines above so it didn't need a color restore, only the
     text-decoration reset. */
a.btn, a.btn-secondary, a.btn-accent2,
a.hero-cta, a.hero-cta-situation {
  text-decoration: none !important;
}
/* Hero CTA color restores. .hero-cta default = white (PLAY); the more-
   specific .hero-cta-situation overrides to neon green (SITUATION
   ROOM). Both states have a matching :hover override below so the
   universal `a:hover { color: navy-hi !important }` rule doesn't
   flash the label navy on mouseover — the hover visual is supposed
   to be the brightness/saturation bump from .hero-cta:hover in
   10-landing.css, not a color flip. */
a.hero-cta            { color: #ffffff !important; }
a.hero-cta-situation  { color: var(--ml-green-hi) !important; }
a.hero-cta:hover            { color: #ffffff !important; }
a.hero-cta-situation:hover  { color: var(--ml-green-hi) !important; }

/* Kill any leftover soft radii on inputs/chips/pills. */
input, textarea, select, .chip, .pill, .badge, .toast, .modal, .sheet {
  border-radius: 0 !important;
}
input, textarea, select {
  background: #ffffff !important;
  border: 0 !important;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-sh),
    inset -1px -1px 0 var(--ml-gray-hi-2),
    inset  2px  2px 0 var(--ml-gray-sh-2),
    inset -2px -2px 0 var(--ml-gray-hi) !important;
  font-family: var(--f-body) !important;
  color: var(--ml-ink) !important;
}

/* 2026-05-03 — radios + checkboxes were picking up the XP text-input
   chrome from the global `input, textarea, select` rules above (the
   `width: 100%; padding: 6px 8px;` block at L417 and the `!important`
   bevel block right above this comment). Symptom: on account.html the
   "Classic Memes" / "Marketer Mode" radio circles floated to the
   center of their label rows because each <input type="radio"> was
   stretched to 100% of its parent's width with chunky padding around
   it; the visible circle drew at the field's vertical center, far
   right of the label text. The `.toggle input[type="checkbox"]` block
   at L477 already does this reset for its own scoped use; this is the
   global version so any radio/checkbox on the site (account, admin,
   host's unhinged-mode toggle, future forms) renders at native size.
   `accent-color` matches the `.toggle` pattern so the checked state
   reads as the brand blue rather than UA default. */
input[type="radio"],
input[type="checkbox"] {
  width: auto !important;
  padding: 0 !important;
  margin: 0 6px 0 0;
  background: transparent !important;
  box-shadow: none !important;
  border: 0 !important;
  vertical-align: middle;
  accent-color: var(--luna-blue-1, #0058ee);
}

/* 2026-05-05 (X61) — meme-caption textareas (.submit-slot-input)
   carve-out from the Win95 form chrome above.

   Why this exists: the meme caption inputs in submit-stage.js are
   WYSIWYG over the meme image — the textarea floats directly on top
   of the meme and intentionally has zero chrome. submit-stage.js
   stamps `font-family: Impact`, `color: #ffffff`, and an outline
   text-shadow inline so what the player types looks like the
   finished meme.

   Why it kept regressing (Captain reported the white-text-turning-
   gray + white-frame bug ~15 times): every prior fix lived inside
   `.submit-slot-input { ... }` in `07-submit-stage.css` WITHOUT
   `!important`. The `input, textarea, select { ... !important }`
   block at the top of this file paints `background: #fff`,
   `color: var(--ml-ink)`, and the inset bevel `box-shadow`. CSS
   `!important` beats both a non-important class selector AND any
   non-important inline style that submit-stage.js sets via
   `ta.style.color = ...`. So 07's "transparent / no border / no
   box-shadow" never won the cascade. The carve-out has to live in
   THIS file (where the override originates) and use `!important`
   on the same properties to flip the cascade.

   Properties carved out here:
   - background → transparent (not the white textarea fill).
   - border + box-shadow → none (kills the inset XP bevel that
     read as a "white box framing the caption").
   - color → #ffffff (default white meme text; submit-stage.js
     also calls setProperty('color', ..., 'important') per slot
     STYLES preset, so a future preset with non-white text wins
     over this CSS rule via inline-with-!important specificity).
   - font-family → IMPACT_FONT (overrides --f-body so the textarea
     renders in Impact even before submit-stage.js's inline style
     applies — defends against the brief flash of body-font during
     mount).

   `:focus` gets the same treatment so tabbing into the slot
   doesn't repaint the bevel + blue glow. */
.submit-slot-input,
.submit-slot-input:focus {
  background: transparent !important;
  border: 0 !important;
  box-shadow: none !important;
  color: #ffffff !important;
  font-family: 'Impact', 'Anton', 'Arial Black', sans-serif !important;
  outline: none !important;
}

@media (prefers-reduced-motion: reduce) {
  .card:hover .xp-icon,
  .xp-icon:hover,
  .xp-icon-lg:hover { animation: none; }
}

/* ==========================================================================
   MEMELORD.COM STRAGGLER FIXES — catches the host/player/spectator pages
   where legacy XP-Luna styling was leaking through (cream pills, blue labels,
   neon-green logo, glow room code). Each selector here maps to a specific
   element I saw off-brand in the QA screenshots.
   ========================================================================== */

/* In-page logo (top of host.html/player.html/spectator.html). Index uses the
   .site-header wordmark instead. Here we replicate the memelord.com wordmark:
   bold bitmap, solid ink, no neon. */
.logo {
  font-family: var(--f-display) !important;
  color: var(--ml-ink) !important;
  text-shadow: none !important;
  text-transform: uppercase !important;
  letter-spacing: 0.04em !important;
  font-weight: 700 !important;
  font-size: 1.25rem !important;
  /* The base .logo rule uses inline-flex with gap:10px, which puts ugly
     whitespace between "memelord", ".", and "game". Kill the gap here so
     the wordmark reads as a single word like on memelord.com. */
  gap: 0 !important;
}
.logo::before {
  display: none !important;  /* kill the green neon dot */
}
.logo .dot { color: var(--ml-ink) !important; }

/* Room code — was a neon-green bitmap glow. memelord.com would render this
   as chunky W95FA in ink, on a white recessed panel. */
.room-code {
  font-family: var(--f-display) !important;
  color: var(--ml-ink) !important;
  text-shadow: none !important;
  background: #ffffff !important;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-sh),
    inset -1px -1px 0 var(--ml-gray-hi-2),
    inset  2px  2px 0 var(--ml-gray-sh-2),
    inset -2px -2px 0 var(--ml-gray-hi) !important;
  margin: 8px 0;
  padding: 14px 0;
}

/* Labels above inputs — was XP blue #0058ee. memelord.com uses ink black. */
.slot-label {
  font-family: var(--f-display) !important;
  color: var(--ml-ink) !important;
  font-weight: 700 !important;
  letter-spacing: 0.04em !important;
}

/* Timer HUD — was cream gradient. Win95 raised button chrome instead. */
.timer {
  font-family: var(--f-display) !important;
  color: var(--ml-ink) !important;
  background: var(--ml-gray) !important;
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-hi-2),
    inset -1px -1px 0 var(--ml-gray-sh-2),
    inset  2px  2px 0 var(--ml-gray-hi),
    inset -2px -2px 0 var(--ml-gray-sh) !important;
}
.timer.warn { background: var(--ml-gold) !important; }

/* Badges (host/player/spectator role pills) — gold chip, ink text. */
.badge {
  font-family: var(--f-display) !important;
  background: var(--ml-gold) !important;
  color: var(--ml-ink) !important;
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow:
    inset  1px  1px 0 #fff7b8,
    inset -1px -1px 0 #a17d00 !important;
  text-transform: uppercase !important;
  letter-spacing: 0.06em !important;
}

/* Player roster pills — were cream-to-beige gradient. Now Win95 button style. */
.player-pill {
  font-family: var(--f-body) !important;
  background: var(--ml-gray) !important;
  color: var(--ml-ink) !important;
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-hi-2),
    inset -1px -1px 0 var(--ml-gray-sh-2),
    inset  2px  2px 0 var(--ml-gray-hi),
    inset -2px -2px 0 var(--ml-gray-sh) !important;
}
.player-pill.submitted {
  background: var(--ml-green) !important;
  color: #ffffff !important;
}
.player-pill.voted {
  background: var(--ml-navy) !important;
  color: #ffffff !important;
}

/* Settings summary (the game settings disclosure) — navy title bar chrome
   to match .card > h2. */
.settings-card > summary.settings-summary,
.settings-summary {
  background: var(--ml-navy) !important;
  color: #ffffff !important;
  font-family: var(--f-display) !important;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  border: 0 !important;
  border-radius: 0 !important;
  padding: 6px 10px !important;
  margin: -10px -10px 10px !important;
  text-shadow: none;
}

/* Score rows, small muted text etc. — use Geist for copy, not the bitmap. */
p, .small, .muted, .scaffold, .score-row, label {
  font-family: var(--f-body) !important;
}


/* === inlined: styles/16-settings-gear.css === */
/* ──────────────────────────────────────────────────────────────────────
   Upper-right settings gear (⚙).
   Originally rendered inline in the host.html lobby row with a visible
   <summary> button. Now the user-facing trigger is the gear inside
   .site-header-actions (built by site-header.js), which programmatically
   toggles `<details id="settingsGear">.open`. To avoid showing two gears
   we hide the in-page <summary>, and we anchor the popover to the
   viewport's upper-right via position:fixed so it appears under the
   header gear regardless of where the (now-invisible) <details> wrapper
   sits in the document flow.
   ────────────────────────────────────────────────────────────────────── */
.settings-gear {
  /* Hosts the <details> state machine and the visible gear lives inside
     it (the <summary>). Setting `position: relative` makes this element
     the offset parent for the absolute-positioned `.settings-gear-panel`
     below, so the panel anchors to the gear button itself instead of
     the viewport. That way the panel scrolls with the page along with
     the gear, instead of staying pinned mid-screen when the user
     scrolls past the header. */
  position: relative;
  display: inline-block;
}
/* Hide the legacy in-page <summary> button. The header gear is the
   only user-facing trigger now. Kept the rule terse — the old
   button-shell styling below (.settings-gear-btn img / hover / [open])
   is dead but harmless and left in place to keep this diff small. */
#settingsGear > summary.settings-gear-btn {
  display: none;
}
.settings-gear-btn {
  /* reset <summary> chrome */
  list-style: none;
  cursor: pointer;
  user-select: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Match .btn.btn-sm shell so the Settings button is the same height as
     the Log in button in the .site-header-actions trio. Padding mirrors
     .btn-sm (4px 10px); font-size 12px keeps any text/icon vertically
     centered at the same eye-line as the auth button beside it. */
  width: auto;
  height: 28px;
  padding: 4px 10px;
  font-size: 12px;
  background: var(--luna-grey-btn, #ffffff);
  border: 1px solid var(--luna-blue-edge, #0a2a62);
  box-shadow: 1px 1px 0 rgba(0,0,0,0.2);
  color: var(--ml-ink);
}
.settings-gear-btn img { display: block; }
.settings-gear-btn::-webkit-details-marker { display: none; }
.settings-gear-btn:hover { background: #f4f1e4; }
.settings-gear[open] .settings-gear-btn {
  background: var(--ml-navy);
  color: #fff;
  box-shadow: inset 1px 1px 0 rgba(0,0,0,0.35);
}
.settings-gear-panel {
  /* Drops out of the .site-header gear button as a normal popover —
     position:absolute relative to `.settings-gear` (the <details>
     wrapper, made `position: relative` above). `top: calc(100% + 6px)`
     puts the panel just under the gear's bottom edge with a 6px
     breathing gap; `right: 0` right-aligns the panel with the gear's
     right edge so the panel grows leftward into the page.
     Captain feedback 2026-05-01b: the previous `position: fixed`
     anchored to the viewport meant that once the user scrolled past
     the header, the panel stayed pinned mid-screen, detached from the
     gear that opened it — and the bottom of the panel was unreachable
     because the page underneath couldn't scroll to it. Switching to
     position:absolute makes the panel scroll with the page like a
     normal dropdown: when the gear scrolls out of view, the panel
     does too, and the user can scroll the page to reach the bottom of
     the panel naturally. The X16 max-height/overflow-y cap is gone
     for the same reason — position:absolute panels are part of page
     flow, so the page itself is the scroll mechanism. */
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 260px;
  max-width: 320px;
  background: var(--luna-grey-face, #ece9d8);
  border: 1px solid var(--luna-blue-edge, #0a2a62);
  box-shadow: 2px 2px 0 rgba(0,0,0,0.25);
  padding: 12px;
  z-index: 1000;
}
.settings-gear-panel .settings-row {
  display: flex;
  flex-direction: column;
  margin-bottom: 10px;
}
.settings-gear-panel .settings-row:last-child { margin-bottom: 0; }
.settings-gear-panel .settings-row select,
.settings-gear-panel .settings-row input[type="number"] {
  width: 100%;
}
/* Account row button (the "Log in" / signed-in-email link injected by
   site-header.js into [data-auth-row]). On a narrow mobile panel
   (`width: min(92vw, 320px)` — see the @media block below), an email
   like "808and817@gmail.com" has no space-break opportunities so the
   default `overflow-wrap: normal` lets the text spill past the button's
   right edge. Allow breaking inside the email so it wraps cleanly
   inside the button instead. `min-width: 0` keeps the button shrinkable
   inside the flex-column row so `width: 100%` actually means
   "fill the row, no more." */
.settings-gear-panel [data-auth-row] .btn {
  min-width: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}


/* === inlined: styles/17-mobile-tweaks.css === */
/* ──────────────────────────────────────────────────────────────────────
   Mobile pitch-mode tweaks — keep the XP chrome readable on phones.
   Stops the header/menu-btn, hero prop, settings popover, and embedded
   iframe shell from crowding each other at <640px. Avatar picker goes
   4-wide on very small phones.
   ────────────────────────────────────────────────────────────────────── */
@media (max-width: 640px) {
  .site-header {
    padding: 10px 12px !important;
    /* Drop the desktop 96px min-height — on mobile the bar is a
       flex-column with the banner stacked above the button row, and
       the natural intrinsic height is what we want. */
    min-height: 0;
    flex-direction: column;
    justify-content: center;
    gap: 6px;
    text-align: center;
  }
  /* .wordmark is hidden globally (banner replaced it); nothing to override. */
  .site-header-banner {
    /* Revert the desktop absolute-centering: when .site-header collapses
       to flex-direction:column, the banner just stacks above the buttons
       like a normal first child. On mobile (≤640px) the <picture>
       element in site-header.js swaps the source to
       banner-memelord-game-header-mobile.png, which is a ~2.7:1 crop of
       the May 1 phone-mockup artwork (1444×538). At 96vw a 380px phone
       renders this at ~365×135, a 320px phone at ~307×114 — both well
       under the 180px max-height cap, so width-driven natural sizing
       wins and the banner never letterboxes. (Old 80px ceiling was tuned
       for the 8:1 desktop banner, which the picture source no longer
       loads here, but kept loose enough to also accommodate the desktop
       PNG as a fallback if a browser ignores the picture <source>.) */
    position: static;
    transform: none;
    pointer-events: auto;
    height: auto;
    width: 96vw;
    max-width: 100%;
    max-height: 180px;
    object-fit: contain;
    aspect-ratio: auto;   /* neutralise the desktop 8:1 cover-crop (added
                             2026-06-22); the mobile PNG is a clean 2:1 that
                             must show in full, never cropped to 8:1. */
  }
  /* Banner is in normal flow on mobile, so no overhang to clear and
     no shelf needed — the banner stacks naturally above the buttons
     when .site-header switches to flex-direction:column. */
  .site-header::before {
    display: none;
  }
  .site-header + main {
    margin-top: 0;
  }
  /* Captain feedback 2026-05-03: between the wide banner image (above)
     and the wide hero image (below), the header buttons read as gawky
     because .site-header-right's `margin-left: auto` (set on desktop so
     Settings drifts to the far right of the navy bar) was still firing
     on mobile. That pinned [MEMELORD.COM][MANUAL] to the left edge and
     [Settings] to the right edge with a big empty navy stripe between
     — the eye reads "two beautiful images sandwiching a half-empty
     bar." Mobile fix: center the row, kill the right-group's
     margin-left:auto, and tighten the inter-button gap so the three
     chips visually merge into one tight chrome-toolbar cluster sitting
     between the two pixel-art panels. */
  .site-header .site-header-actions {
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
  }
  .site-header .site-header-right {
    margin-left: 0;
  }
  /* On phones, .site-header collapses to flex-column with the banner
     stacking above the button row, so the chips no longer need to match
     the 84px banner height (there's no horizontal alignment to honor).
     Restore the pre-2026-05-01a compact 36px chrome for both wordmark
     chips here. Same set of properties the desktop base/MANUAL rules
     declared with !important — repeating !important to win that cascade. */
  .site-header .site-header-actions .btn.ml-com-wordmark-btn,
  a.ml-com-wordmark-btn,
  .site-header .site-header-actions .btn.ml-brand-wordmark-btn,
  a.ml-brand-wordmark-btn {
    height: 36px !important;
    padding: 0 16px !important;
    font-size: 16px !important;
    text-shadow: 0 2px 0 rgba(0, 0, 0, 0.55);
  }
  .site-header .site-header-actions .btn.ml-brand-wordmark-btn {
    padding: 0 10px !important;
    font-size: 12px !important;
  }
  /* Settings icon chip — match the wordmark chips' mobile 36px height so
     the three header buttons (MEMELORD.COM / MANUAL / Settings) line up
     across the bottom on phones. The desktop rule at L≈2315 pins Settings
     to 32×32 with !important; without this override the gear sat 4px
     shorter than its neighbors on mobile and the row read as ragged.
     Inner icon goes 16→18px so the icon-to-chip ratio (~50%) is held
     across the height bump. Captain feedback 2026-05-03. */
  .site-header .site-header-actions .btn.ml-brand-icon-btn {
    width: 36px !important;
    height: 36px !important;
  }
  .site-header .site-header-actions .btn.ml-brand-icon-btn img {
    width: 18px;
    height: 18px;
  }
  .site-header .menu-btn {
    position: static !important;
    transform: none !important;
    padding: 5px 10px;
    font-size: 12px;
  }
  .page, .container, .player-container {
    padding: 12px;
  }
  .hero {
    /* X149-2026-05-14: removed the `-12px` mobile escape margin so
       `.hero-art` renders inside `.page`'s 12-px-padded content area,
       matching `.phase-hero` inside `.container` on host/player at
       mobile (same 12-px padding, no escape). See the desktop rule
       in `10-landing.css` for the full rationale. */
    margin: 16px 0 14px;
    padding: 8px 0;
  }
  .hero-grid {
    /* Mirror .hero's mobile break-out so the cards row stays
       width-matched to the hero on phones. .page padding is 12px
       under this breakpoint, so the side margins drop from -20px
       to -12px. */
    margin: 12px -12px 0;
  }
  .hero-row {
    gap: 12px;
    text-align: center;
  }
  .hero-text {
    min-width: 0;
  }
  .hero-dept {
    justify-content: center;
    letter-spacing: 0.08em;
  }
  .recruiter {
    width: 100%;
    justify-content: center;
    text-align: center;
  }
  .hero-gun {
    width: 44px;
  }
  .battlefield-tag {
    letter-spacing: 0.1em;
  }
  .settings-gear-panel {
    /* Panel is now `position: absolute` anchored to the gear button
       (see L≈3209 base rule), so on mobile it just drops below whatever
       wrapped position the gear ended up in inside the flex-column
       header — no viewport-centering hack needed anymore, no max-height
       cap needed. Cap width so the panel still fits a narrow phone
       viewport (the gear is in the right sub-group of
       .site-header-actions, so the panel grows leftward from
       `right: 0` and could otherwise overshoot a 320px screen at
       desktop's 260–320px min/max-width). */
    width: min(92vw, 320px);
  }
}

/* -----------------------------
   Compact in-round header (2026-07-07 mobile audit)
   -----------------------------
   The full banner + button band consumed roughly a third of a phone
   screen on EVERY in-game view — mid-round, the situation started far
   below the fold. host.js / player.js / spectator.js stamp
   body[data-phase] on every broadcast render; outside the lobby (and
   only once a phase exists — join pages have none and keep the full
   glory) the banner cover-crops to a wide strip centered on the
   wordmark and the chrome row tightens. Total header drops from
   ~280px to ~110px while staying unmistakably Memelord. */
@media (max-width: 640px) {
  body[data-phase]:not([data-phase=""]):not([data-phase="LOBBY"]) .site-header {
    padding: 6px 10px !important;
    gap: 4px;
  }
  body[data-phase]:not([data-phase=""]):not([data-phase="LOBBY"]) .site-header-banner {
    max-height: 64px;
    width: 96vw;
    object-fit: cover;
    /* The mobile banner art carries the wordmark in its middle band —
       bias the crop there so the strip still reads MEMETIC WARFARE
       ACADEMY, not empty sky. */
    object-position: center 42%;
    aspect-ratio: auto;
  }
  body[data-phase]:not([data-phase=""]):not([data-phase="LOBBY"]) .site-header .btn,
  body[data-phase]:not([data-phase=""]):not([data-phase="LOBBY"]) .site-header .settings-gear-btn {
    padding: 4px 10px;
    font-size: 11px;
    min-height: 0;
  }
}

@media (max-width: 420px) {
  .avatar-picker {
    grid-template-columns: repeat(4, 1fr);
  }
  .score-row {
    gap: 8px;
  }
}

@media (max-width: 375px) {
  .page, .container, .player-container {
    padding: 8px;
  }
  .site-header {
    padding: 8px 10px !important;
  }
  .site-header .wordmark {
    font-size: 1.35rem !important;
  }
  .avatar-picker {
    grid-template-columns: repeat(3, 1fr);
  }
  .score-row {
    gap: 6px;
  }
  .btn, .btn-secondary, .btn-accent2, button.btn, a.btn {
    padding: 8px 14px;
    font-size: 12px;
  }
  .hero-gun {
    width: 36px;
  }
  input, textarea, select {
    font-size: 16px;
  }
}


/* === inlined: styles/18-monitoring-layer.css === */
/* ==========================================================================
   Memetic Warfare monitoring layer — 2026-04-23
   35 army / 65 memelord. The Win95 base carries the memelord weight.
   These primitives sprinkle in Dept-of-Memetic-Warfare flavor: chyrons,
   faint CRT scanlines on active surfaces, tasteful rubber stamps.

   DO NOT add: crosshairs, camo patterns, rifle/combat iconography, or
   "TARGETING" / "CONFIRMED HIT" / "COLLATERAL" language. This is
   keyboard-warrior monitoring-station vibe, not trench ops.
   See MEMELORD_DESIGN_BIBLE.md.
   ========================================================================== */

/* ── .mw-chyron ────────────────────────────────────────────────────
   A small green-phosphor status strip. Renders as a dept watermark
   at the bottom of an in-game card, or inline under a heading.
   Auto-scrolls with a gentle marquee so it feels "live".
   Keeps to one line; tiny type; monospace. */
.mw-chyron {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 10px -10px -10px;
  padding: 4px 10px;
  background: #06130a;
  color: var(--ml-green-hi);
  font-family: var(--f-pixel);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border-top: 1px solid var(--ml-green-dark);
  box-shadow: inset 0 1px 0 rgba(64, 208, 116, 0.25);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mw-chyron::before {
  /* X65 follow-up (Captain, two passes): original was a 1.6s hard-step
     100%↔25% flash. First pass calmed it to a 6s gentle pulse. Captain
     quote (second pass): "Let's switch it from once every 6 seconds to
     once every 15 seconds." Cycle is now 15s — the dot sits at 85%
     opacity for ~94% of each cycle and brightens briefly to full at
     the 96% mark, then eases back. Once every 15 seconds, ambient
     otherwise. Smooth ease, no hard step. */
  content: "";
  display: inline-block;
  width: 7px;
  height: 7px;
  background: var(--ml-green-hi);
  box-shadow: 0 0 3px rgba(64, 208, 116, 0.5);
  opacity: 0.85;
  animation: mwAmbientPulse 15s ease-in-out infinite;
  flex: 0 0 auto;
}
@keyframes mwAmbientPulse {
  0%, 94%, 100% { opacity: 0.85; box-shadow: 0 0 3px rgba(64, 208, 116, 0.5); }
  96%           { opacity: 1.00; box-shadow: 0 0 5px rgba(64, 208, 116, 0.85); }
}
/* Legacy alias — older styles or markup outside this repo may still
   reference mwBlink. Map it to the same calmed-down 15s pulse so
   nothing regresses to the hard flash. */
@keyframes mwBlink {
  0%, 94%, 100% { opacity: 0.85; }
  96%           { opacity: 1.00; }
}
@media (prefers-reduced-motion: reduce) {
  .mw-chyron::before { animation: none; opacity: 0.85; }
}
/* chyron variants */
.mw-chyron.mw-chyron-muted {
  background: #0b1a13;
  color: var(--ml-green);
}
.mw-chyron.mw-chyron-amber {
  background: #1a1102;
  color: var(--ml-gold);
  border-top-color: #8a6a00;
}
.mw-chyron.mw-chyron-amber::before {
  background: var(--ml-gold);
  box-shadow: 0 0 4px var(--ml-gold);
}

/* ── .mw-crt ───────────────────────────────────────────────────────
   Apply to an element to layer faint CRT scanlines + a vignette on
   top. Uses ::after so the underlying content is untouched. */
.mw-crt {
  position: relative;
  isolation: isolate;
}
.mw-crt::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(
      to bottom,
      rgba(0, 160, 67, 0.055) 0,
      rgba(0, 160, 67, 0.055) 1px,
      transparent 1px,
      transparent 3px
    ),
    radial-gradient(
      ellipse at center,
      transparent 55%,
      rgba(0, 0, 0, 0.18) 100%
    );
  mix-blend-mode: multiply;
  z-index: 1;
}
/* When content inside must sit above the scanline overlay */
.mw-crt > * { position: relative; z-index: 2; }

/* ── .mw-stamp ─────────────────────────────────────────────────────
   Rotated rubber-stamp text. Use sparingly — one per view, max.
   Default color is faded green (MONITORED / LOGGED); .red variant
   is for RECRUIT stamps on player pills; .amber is for PENDING. */
.mw-stamp {
  display: inline-block;
  padding: 2px 8px;
  border: 2px solid var(--ml-green-dark);
  border-radius: 0;
  color: var(--ml-green-dark);
  font-family: var(--f-caption);
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  transform: rotate(-4deg);
  opacity: 0.78;
  background: transparent;
  white-space: nowrap;
  /* Faint distressed edge, not painterly */
  box-shadow: inset 0 0 0 1px currentColor;
}
.mw-stamp.red {
  color: var(--ml-red-dark);
  border-color: var(--ml-red-dark);
}
.mw-stamp.amber {
  color: #8a6a00;
  border-color: #8a6a00;
}

/* ── .mw-id-chip ───────────────────────────────────────────────────
   Small rank chip that can sit inside a player pill next to the name.
   E.g. "E-1 SHITPOSTER" / "RECRUIT" / "MEMELORD". Intentionally bland
   institutional typography so it reads as a dept label, not a taunt. */
.mw-id-chip {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  background: var(--ml-navy);
  color: #ffffff;
  font-family: var(--f-pixel);
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border: 1px solid var(--ml-navy-dark);
  vertical-align: middle;
}

/* ── .mw-dept-line ─────────────────────────────────────────────────
   Small text line that reads "DEPT OF MEMETIC WARFARE // <tag>".
   Drop it under a heading as institutional flavor. */
.mw-dept-line {
  font-family: var(--f-pixel);
  font-size: 10px;
  color: var(--ml-navy);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.72;
  margin-top: -4px;
  margin-bottom: 8px;
}

/* 19-auth-pages.css — auth surfaces only (login/signup/account/reset/
   verify/subscribe/subscribe-success/subscribe-cancel). Linked directly
   on each. Was imported here through 2026-05-05 (X67). Kept off the
   shared chain so game pages don't ship the ~8 KB of auth-form +
   auth-card chrome that only renders on those eight pages. The
   .upsell-chip pattern lives in 05-meme-stage.css, not here, so
   marketermode.html keeps working without the auth-pages stylesheet. */

/* === inlined: styles/20-bcm-modal.css === */
/* ──────────────────────────────────────────────────────────────────────
   Boot Camp Manual modal — added 2026-04-27. See MEMELORD_DESIGN_BIBLE.md
   "Boot Camp Manual modal" entry. The trigger chip lives in flex flow
   inside .site-header (after the wordmark, before the absolutely-
   positioned memelord.com link), so on mobile when the header collapses
   to a vertical column it stacks naturally.

   IMPORTANT: nothing in this block touches html/body backgrounds —
   tests/test-style-guard.js will fail npm test if that ever changes.
   ────────────────────────────────────────────────────────────────────── */

/* Trigger chip — small, raised XP bevel, paper-white face. The
   .menu-btn link is text-only on teal; this chip needs to read as a
   clickable control, so it gets the inset bevel like a Win95 button. */
.site-header .bcm-trigger {
  /* Compact square chip — moved 2026-04-30 from the left-of-wordmark
     flex slot into the right-side .site-header-actions cluster. The
     label wraps to two lines ("Boot Camp" / "Manual") so the chip reads
     as an icon-tile, not a wordy pill. Sized to roughly match the
     .btn.btn-sm height-doubled for two text rows + icon. */
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  margin-left: 0;
  width: 70px;
  min-height: 46px;
  padding: 3px 4px;
  background: #ffffff;
  color: var(--ml-ink);
  border: none;
  font-family: var(--f-body);
  font-weight: 700;
  font-size: 9.5px;
  line-height: 1.05;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-align: center;
  white-space: normal;
  cursor: pointer;
  /* Raised bevel: matches the button bevel pattern used elsewhere. */
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-hi),
    inset -1px -1px 0 var(--ml-gray-sh-2),
    inset  2px  2px 0 var(--ml-gray-hi-2),
    inset -2px -2px 0 var(--ml-gray-sh);
}
.site-header .bcm-trigger:hover { background: #fffbe6; }
.site-header .bcm-trigger:active {
  /* Pressed bevel: invert the highlights/shadows. */
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-sh),
    inset -1px -1px 0 var(--ml-gray-hi-2),
    inset  2px  2px 0 var(--ml-gray-sh-2),
    inset -2px -2px 0 var(--ml-gray-hi);
}
.site-header .bcm-trigger:focus-visible {
  outline: 2px dashed #000;
  outline-offset: 2px;
}
.site-header .bcm-trigger .xp-icon-tiny {
  /* No invert — chip face is white, icon stays its native color.
     Sized a touch larger than 1em so the icon reads from across the
     header even with the smaller two-line label below it. */
  filter: none;
  margin-right: 0;
  width: 18px;
  height: 18px;
  vertical-align: baseline;
}
.site-header .bcm-trigger-label {
  display: inline-block;
  white-space: normal;
  line-height: 1.05;
}
@media (max-width: 640px) {
  .site-header .bcm-trigger {
    margin-left: 0;
    align-self: center;
    width: 64px;
    min-height: 42px;
    font-size: 9px;
    padding: 2px 4px;
  }
}

/* Modal scaffolding. Uses a high z-index to clear the .site-header
   (z:10), embedded view shells, and any settings popovers. The
   `[hidden]` attribute on the root element handles open/closed
   visibility — JS just toggles it. */
.bcm-modal {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.bcm-modal[hidden] { display: none; }

/* Spectator share modal — allows host to share watch link + QR code */
.spectator-share-modal {
  padding: 20px !important;
}
.spectator-share-modal.hidden {
  display: none !important;
}

.bcm-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 32, 0.55);
  cursor: pointer;
}

/* The modal "window" mimics the XP window chrome used elsewhere on
   the site — Luna-blue gradient titlebar, white client area, raised
   bevel, square corners. Width caps at 520px so the copy stays in
   one comfortable column on desktop and goes full-width on phones. */
.bcm-window {
  position: relative;
  width: 100%;
  max-width: 520px;
  max-height: calc(100vh - 40px);
  display: flex;
  flex-direction: column;
  background: #ffffff;
  border: 1px solid var(--luna-blue-edge, #0a3b8a);
  box-shadow:
    0 14px 40px rgba(0, 0, 0, 0.45),
    inset  1px  1px 0 var(--ml-gray-hi),
    inset -1px -1px 0 var(--ml-gray-sh-2);
}

.bcm-titlebar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 6px 5px 8px;
  background:
    linear-gradient(180deg,
      var(--luna-blue-1) 0%,
      #2a79ef 48%,
      var(--luna-blue-3) 100%);
  color: #fff;
  font-family: var(--f-body);
  font-weight: 700;
  font-size: 0.92rem;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.4);
}
.bcm-titlebar-icon {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
}
.bcm-titlebar-text {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bcm-titlebar-controls {
  margin-left: auto;
  display: inline-flex;
  gap: 3px;
}
.bcm-x {
  width: 22px;
  height: 20px;
  border: 1px solid var(--luna-blue-dark, #06245c);
  background: linear-gradient(180deg, #ee7a5d 0%, #c12b1d 100%);
  color: #fff;
  font-family: var(--f-body);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
.bcm-x:hover { filter: brightness(1.1); }
.bcm-x:focus-visible {
  outline: 2px dashed #fff;
  outline-offset: -3px;
}

.bcm-body {
  /* Fill the remaining height inside the .bcm-window flex column after
     the titlebar, AND allow shrinking below intrinsic content height —
     `min-height: 0` defeats the default flex `min-height: auto` that
     was keeping the body at its content height and pushing the window
     past its `max-height: calc(100vh - 40px)` cap on mobile. With both
     declarations in place, `overflow-y: auto` actually engages and the
     manual scrolls internally instead of falling off the viewport. */
  flex: 1 1 auto;
  min-height: 0;
  padding: 16px 18px 18px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  font-family: var(--f-body);
  font-size: 14px;
  line-height: 1.45;
  color: var(--ml-ink);
}
.bcm-body p { margin: 0 0 10px; }
.bcm-lede {
  font-family: var(--f-display, var(--f-body));
  font-size: 1.05rem;
  letter-spacing: 0.04em;
}
.bcm-section-head {
  margin-top: 14px !important;
  font-weight: 800;
  letter-spacing: 0.06em;
  font-size: 0.78rem;
  color: var(--ml-navy);
  text-transform: uppercase;
}
.bcm-roles {
  list-style: none;
  margin: 4px 0 6px;
  padding: 0;
}
.bcm-roles li {
  margin: 0 0 6px;
  padding-left: 14px;
  position: relative;
}
.bcm-roles li::before {
  content: "▸";
  position: absolute;
  left: 0;
  color: var(--ml-navy);
}
.bcm-role {
  display: inline-block;
  background: #fff09a;
  color: var(--ml-ink);
  font-weight: 800;
  font-size: 0.78rem;
  letter-spacing: 0.05em;
  padding: 0 6px;
  margin-right: 6px;
  box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5);
}
/* MARKETER MODE wordmark variant — used inside the BCM role list
   instead of the yellow `.bcm-role` chip for the Marketer Mode entry
   only. The wrapping <a> routes to /signup.html (Captain 2026-05-04:
   "make that image clickable to go to the signup page"). The image
   is `img/marketer-mode-wordmark.png` (ChatGPT-generated 2026-04-28,
   originally `Graphics/ChatGPT Image Apr 28, 2026, 11_41_28 PM.png`,
   promoted into public/img on 2026-05-04).

   2026-05-04 follow-up (Captain): originally sat inline at height
   2em with `vertical-align: middle`, which had two problems —
   (a) the centered "MARKETER MODE" letters inside the asset (wizard,
   guitars, lightning, flames around a small middle wordmark) were
   too small to read at that height; (b) every line that wrapped
   through the inline image inherited the image's 2em line-box,
   leaving a visibly larger gap between sentences in the bullet than
   the surrounding body line-height. Captain: "the Marketer Mode
   graphic ... needs to be bigger, and cropped a bit so that we can
   read the words" + "the line spacing between [first sentence] and
   [second sentence] is too wide. it should be normal line spacing."

   Fix: take the link out of inline flow (`display: block`) so the
   bullet text wraps as a normal paragraph below it (consistent
   line-height, no inline image bloating the line-box), and crop the
   asset via CSS so the words read at the larger size without dragging
   the surrounding artwork along. The fixed `width × height` box plus
   `object-fit: cover` scales the image until it covers the box and
   overflows are clipped — the box is wider/shorter than the asset's
   natural ~4:3 aspect, so the top wizard hat and bottom flames are
   clipped while the centered "MARKETER MODE" wordmark fills the
   visible area at a much larger pixel size. `max-width: 100%` keeps
   it from punching out on narrow viewports.

   Identical markup in `public/index.html` static modal and
   `public/js/site-header.js` BCM_MODAL_HTML — keep both in lockstep
   per the X30-2026-05-03 device-neutral rule. */
.bcm-role-img-link {
  display: block;
  margin: 0 0 4px;
  text-decoration: none;
  color: inherit;
  max-width: 100%;
}
.bcm-role-img-link img {
  display: block;
  width: 14em;
  height: 5em;
  max-width: 100%;
  object-fit: cover;
  object-position: center;
}
.bcm-round {
  background: #f3f3ee;
  padding: 8px 10px;
  font-size: 13px;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-sh),
    inset -1px -1px 0 var(--ml-gray-hi-2);
}

/* "Which station do I pick?" — plain-language decision tree under the
   yellow-chip role list. Same arrow bullet idiom as .bcm-roles, but
   the lines are longer prose so we widen the indent and let them
   wrap naturally. The <strong> opener carries the question; the body
   answer follows the → arrow. */
.bcm-decide {
  list-style: none;
  margin: 4px 0 6px;
  padding: 0;
}
.bcm-decide li {
  margin: 0 0 8px;
  padding-left: 14px;
  position: relative;
  font-size: 13px;
  line-height: 1.45;
}
.bcm-decide li::before {
  content: "▸";
  position: absolute;
  left: 0;
  color: var(--ml-navy);
}
.bcm-decide strong { color: var(--ml-navy); }

/* FAQ accordion — native <details>/<summary>. Each item is a chrome
   tile with a square-corner navy hover/open border, matching the XP
   bevel idiom used elsewhere in the manual. No JS — the browser
   handles open/close. The disclosure caret is replaced with a
   yellow-chip ▸ that rotates 90° when the item is open. */
.bcm-faq {
  margin: 4px 0 6px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.bcm-faq-item {
  background: #f3f3ee;
  border: 1px solid transparent;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-hi),
    inset -1px -1px 0 var(--ml-gray-sh-2);
}
.bcm-faq-item[open] {
  background: #fffbe6;
  border-color: var(--ml-navy);
  box-shadow: none;
}
.bcm-faq-item > summary {
  cursor: pointer;
  list-style: none;
  padding: 7px 10px 7px 26px;
  font-weight: 700;
  font-size: 13px;
  color: var(--ml-navy);
  position: relative;
  user-select: none;
}
.bcm-faq-item > summary::-webkit-details-marker { display: none; }
.bcm-faq-item > summary::before {
  content: "▸";
  position: absolute;
  left: 9px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--ml-navy);
  transition: transform 120ms ease-out;
}
.bcm-faq-item[open] > summary::before {
  transform: translateY(-50%) rotate(90deg);
}
.bcm-faq-item > summary:hover { color: #000; }
.bcm-faq-item > summary:focus-visible {
  outline: 2px dashed var(--ml-navy);
  outline-offset: -3px;
}
.bcm-faq-item > p {
  margin: 0 !important;
  padding: 0 12px 10px 26px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ml-ink);
}

.bcm-foot {
  margin-top: 10px;
  font-style: italic;
  color: #444;
}
.bcm-actions {
  margin-top: 14px;
  display: flex;
  justify-content: flex-end;
}

/* When the modal is open, lock body scroll so the underlying landing
   doesn't drift while the recruit is reading. JS toggles .bcm-open. */
body.bcm-open { overflow: hidden; }

@media (max-width: 480px) {
  /* Pull the window inward on phones so a real backdrop strip is visible
     above and below — gives recruits an obvious tap-off target and stops
     the manual from reading as a full-screen takeover. The previous
     `calc(100vh - 24px)` cap was actually TALLER than the 40px-padding
     zone, so the window overflowed the backdrop and there was effectively
     no visible "click off" area; combined with a 22×20 `×` that's under
     Apple's 44×44 HIG tap-target minimum, mobile users had no easy way
     out. We now match the modal padding (60+60=120px) to the window
     max-height cap, then beef up the close button. */
  .bcm-modal { padding: 60px 12px; }
  .bcm-window { max-height: calc(100vh - 120px); }
  .bcm-body { padding: 14px 14px 16px; font-size: 13px; }
  .bcm-titlebar-text { font-size: 0.82rem; }
  .bcm-x {
    width: 36px;
    height: 30px;
    font-size: 16px;
  }
}


/* === inlined: styles/21-phase-heroes.css === */
/* ==========================================================================
   PHASE HEROES — round-flow banner art
   --------------------------------------------------------------------------
   Captain commissioned a set of GPT-generated heroes for every phase of a
   game (lobby waiting / write-a-banger / crown-the-banger / round-complete /
   tied / final-round / winner-crowned / no-submissions). They live under
   public/img/heroes/ and drop in as the first child of each phase card so
   the round → vote → score rhythm has a visual beat instead of jumping cold
   between text headers. See MEMELORD_DESIGN_BIBLE.md for the asset map.
   Promoted from Graphics/ on 2026-04-27.
   ========================================================================== */
.phase-hero {
  /* History: 600 (pre-X65) → 880 (X65-2026-05-05 unified 880 lane) →
     720 (X78-2026-05-08 "feels too wide" revert) → 660 (X79-2026-05-08
     "let's try 660 px on everything"). Tracks .container /
     .player-container / .page across all three swings. Variants below
     stay at their intentional smaller sizes — -tall (360) is for
     vertically-leaning art that would crop ugly at full width, -crowned
     (560) is the T6-2026-04-30 end-screen spec, -mini (320) is the
     player phone shrink-down. Inline max-width overrides on host.html /
     spectator.html track the lane the same way. */
  display: block;
  width: 100%;
  max-width: 660px;
  height: auto;
  margin: 0 auto 14px;
  border-radius: 6px;
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-hi-2),
    inset -1px -1px 0 var(--ml-gray-sh),
    0 2px 6px rgba(0, 0, 0, 0.18);
  background: var(--ml-gray);
  image-rendering: auto;
}
.phase-hero.phase-hero-tall {
  /* Vertical-leaning art (deploying splash etc.) — cap shorter. */
  max-width: 360px;
}
.phase-hero.phase-hero-crowned {
  /* Crowned-gigachad end-screen hero — 560px per T6-2026-04-30 spec.
     Intentionally narrower than the X65-2026-05-05 880 default; this
     one stays at the original spec width because the art was
     framed/composed for a 560-wide presentation slot. */
  max-width: 560px;
}
.phase-hero.phase-hero-mini {
  /* Player.html shrinks the hero on phone-width screens so it doesn't eat
     the whole stage above the situation prompt. */
  max-width: 320px;
  margin-bottom: 8px;
}
.player-join-hero {
  /* Wide hero above the join card on player.html — the "GET YOURSELF
     RECRUITED · MEMES WANTS YOU" Uncle-Sam-pointing-at-viewer scene set
     against the moonlit training depot. Full container width, no
     max-width cap so the hero fills edge-to-edge on phone; border-radius
     softens the hard corners. Asset history:
       2026-05-01 (later): swapped from
         hero-player-waiting-for-recruits.png to
         hero-player-get-yourself-recruited.png — Captain's call: the
         recruiter pose maps better to the player join-screen action
         than the static "we are waiting" framing. Same 1672×941
         dimensions, so this rule didn't need a sizing retune.
       2026-05-01 (earlier): replaced banner-memelord-game-header.png
         (which was already in the site header — duplicate use made the
         page redundant) with hero-player-waiting-for-recruits.png.
     Current asset: img/heroes/hero-player-get-yourself-recruited.png
     (1672×941). */
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  margin-bottom: 12px;
  object-fit: cover;
}

/* Memetic Warfare departmental seal — the embroidered crest pairs with the
   existing .mw-dept-line text chyron. Small enough to read as a faux "official
   seal" stamp; not a hero in its own right. */
.dept-seal-icon {
  width: 22px;
  height: 22px;
  vertical-align: middle;
  margin-right: 6px;
  border-radius: 50%;
  flex-shrink: 0;
}
.mw-dept-line .dept-seal-icon {
  margin-right: 6px;
}

/* Bot avatar tile — settings-gear dropdown preview shows a representative
   wojak (per AGI / Normie / NPC tier; mapping in host.js BOT_TIER_AVATAR).
   Square, 56px so it matches the Wojak picker tile size without dominating
   the row. X## (2026-05-10): pivoted from the bot-{tier}.png notebook-paper
   tier portraits to wojak thumbs (transparent background) per Captain's
   ask — the prior assets read as white tiles on the lobby panel. The
   var(--ml-gray) backdrop now shows behind the transparent wojak, which
   reads consistently with the rest of the recruit-lineup chrome. */
.bot-avatar-img {
  display: inline-block;
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: 6px;
  vertical-align: middle;
  background: var(--ml-gray);
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-hi-2),
    inset -1px -1px 0 var(--ml-gray-sh);
}
.bot-avatar-img.bot-avatar-img-sm {
  width: 36px;
  height: 36px;
}

/* Reaction stamps — overlay on the winning meme during the reveal beat.
   Rotated slightly + transparent background so it reads like a rubber stamp
   slammed onto the meme. Currently surfaced via .meme-stamp on
   #scoreSubmissions winner cards in host.html / spectator.html. */
.meme-stamp-wrap {
  /* position: relative comes from the .submission-canvas-wrap rule above.
     display stays block (not inline-block) so the wrap fills its grid cell.
     Item 7 fix (T6-2026-04-30): inline-block was causing card overlap in
     the 3-column scoreSubmissions grid. */
  position: relative;
}
.meme-stamp {
  position: absolute;
  top: 8%;
  right: 6%;
  width: 38%;
  max-width: 240px;
  pointer-events: none;
  transform: rotate(-8deg);
  filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.25));
  z-index: 3;
}
.meme-stamp.meme-stamp-classic {
  /* gold "certified hood classic" — slightly larger feels right for a
     once-per-night cherry-on-top stamp. */
  width: 44%;
  transform: rotate(-12deg);
}
.meme-stamp.meme-stamp-default {
  /* green "WINNING!... BY DEFAULT" — swapped onto the round-winner card
     when the sole-submission auto-tally path fired (lib/rooms/room.js
     ~L1322 — submissions.length <= 1 skips VOTE and goes straight to
     SCORE). Slightly wider than the red BANGER stamp because the rubber-
     stamp art is a longer horizontal rectangle, and a softer rotation so
     the longer caption stays legible. Captain 2026-05-04: "one
     submission auto-wins with no fanfare ... even just a hero-art swap
     with a one-liner ('BANGER BY DEFAULT')." Asset:
     public/img/stamps/stamp-winning-by-default.png. */
  width: 46%;
  transform: rotate(-6deg);
}

/* ---------------------------------------------------------------------
   Edge-state banner above #scoreSubmissions — fires when the SCORE phase
   landed via the auto-tally branch (room.js ~L1319: submissions.length
   <= 1 skips VOTE). Two flavors via modifier classes:
   .is-default-win (1 submission) — chyron-only strip with "BANGER BY
     DEFAULT" headline + dept-line context. The green WINNING-BY-DEFAULT
     stamp on the meme card carries the visual; this strip just gives
     the moment a name so it doesn't read as a regular round.
   .is-abandoned (0 submissions) — surfaces the existing
     hero-no-submissions-stand-by.png art (the same one renderVote()
     swaps to when nobody submitted) above the empty card area, with
     just a dept-line "no transmissions received" caption underneath.
     Headline element is hidden in this variant — Captain 2026-05-04
     follow-up: the original "ROUND ABANDONED" headline duplicated
     the hero art's own NO TRANSMISSIONS // STAND BY copy.
   Captain 2026-05-04: "Empty/single-submission states feel
   anticlimactic. Zero submissions reads as a glitch; one submission
   auto-wins with no fanfare. Both are real moments worth designing
   for." Wired in host.js / player.js / spectator.html renderScore.
   --------------------------------------------------------------------- */
.score-edge-banner {
  text-align: center;
  margin-bottom: 16px;
}
.score-edge-banner-hero {
  max-width: 360px;
  width: 100%;
  height: auto;
  margin: 0 auto 10px;
  display: block;
}
/* Honor the HTML [hidden] attribute that host.js / player.js / spectator.js
   set on the .is-default-win branch (subCount === 1). Without this rule,
   the .score-edge-banner-hero class's `display: block` ties with UA's
   `[hidden] { display: none }` on specificity and wins by source order, so
   the no-transmissions hero leaks onto the default-win SCORE card.
   X07-2026-05-08 — Captain reported the no-submissions hero showing on a
   1-submission (bot-only) round next to the BANGER BY DEFAULT caption. */
.score-edge-banner-hero[hidden] {
  display: none;
}
.score-edge-banner-headline {
  font-family: var(--f-pixel);
  font-size: 1.4rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 4px;
  color: var(--ml-navy);
}
.score-edge-banner.is-default-win .score-edge-banner-headline {
  /* Match the green stamp's rubber-ink hue so the strip and the stamp
     read as one design beat. */
  color: #2d6b1e;
}

/* ---------------------------------------------------------------------
   Save disc button — small circular 💾 icon positioned bottom-right of
   the image it saves. Sits inside a .canvas-save-wrap (position:relative)
   so it overlays the canvas corner. T6-2026-04-30 (item 18).
   --------------------------------------------------------------------- */
.save-disc-btn {
  position: absolute;
  bottom: 10px;
  right: 10px;
  width: 36px;
  height: 36px;
  min-width: 0;
  padding: 0;
  border-radius: 50%;
  background: rgba(2, 0, 127, 0.82);
  color: #fff;
  border: 2px solid rgba(255, 255, 255, 0.55);
  font-size: 1.1rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 4;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
  transition: background 0.15s, transform 0.1s;
  /* Prevent the button from appearing in canvas-to-blob renders */
  pointer-events: auto;
}
.save-disc-btn:hover {
  background: var(--ml-navy, #02007F);
  transform: scale(1.08);
}
.save-disc-btn--share {
  right: 54px; /* offset left of the download button */
}

/* ---------------------------------------------------------------------
   Solo Studio — "Go Full Marketer Mode" CTA
   Absolute-positioned over the .sit-monitor-frame laptop hero.
   Only rendered for Classic (free) variant; hidden on Marketer Mode
   (checked via localStorage memelord_pack_pref or MemelordPaid.isPaid).
   S2-2026-04-30.
   --------------------------------------------------------------------- */
.solo-marketer-cta {
  position: absolute;
  bottom: 5%;
  right: 8%;
  z-index: 10;
  display: inline-block;
  padding: 5px 11px;
  font-family: var(--f-display, 'Impact', sans-serif);
  font-size: 0.68rem;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #fff;
  background: rgba(2, 0, 127, 0.82);
  border: 1px solid rgba(255, 255, 255, 0.45);
  border-radius: 3px;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s, border-color 0.15s;
}
.solo-marketer-cta:hover {
  background: rgba(2, 0, 180, 0.96);
  border-color: rgba(255, 255, 255, 0.75);
}


/* === inlined: styles/22-end-game-extras.css === */
/* ==========================================================================
   VICTORY CARDS CASCADE — Windows-Solitaire-style win celebration overlay
   --------------------------------------------------------------------------
   After the END-phase scorecard reveal completes, this overlay drops in for
   a few seconds: cards fly out of the right side of the viewport, fall with
   gravity, and bounce off the bottom — every frame is painted on top of the
   last so the cards leave streaks across the screen, exactly like the
   Win 3.1 / 95 / 98 / XP Solitaire victory animation. A "Skip" button in
   the top-right tears the overlay down on demand. Triggered + driven by
   public/victory-cascade.js. prefers-reduced-motion users skip the trail
   animation entirely.
   ========================================================================== */
.victory-cascade {
  position: fixed;
  inset: 0;
  z-index: 1500;            /* above academy/scoreboard, below toast (1000)
                               isn't quite right — toasts are 1000, settings
                               popovers are 50. We want this above the page
                               but below modals. 1500 lands in that window. */
  pointer-events: none;     /* canvas paints; only the skip btn takes clicks */
  background: transparent;
  animation: victoryCascadeFadeIn 200ms ease-out both;
}
.victory-cascade--leaving {
  animation: victoryCascadeFadeOut 280ms ease-in both;
}
@keyframes victoryCascadeFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes victoryCascadeFadeOut {
  from { opacity: 1; }
  to   { opacity: 0; }
}
.victory-cascade-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* Cards are pixel-art rectangles; let the browser keep them crisp. */
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
  image-rendering: crisp-edges;
}
/* Tiny "this is a Solitaire callback" label — sits in the top-left so
   Captain (and any first-time viewer) recognizes the visual as the old
   Win 3.1 / 95 / 98 / XP Solitaire victory cascade and not a glitch.
   Low contrast on purpose; this is a footnote, not a chyron. */
.victory-cascade-label {
  position: absolute;
  top: 22px;
  left: 22px;
  pointer-events: none;
  font-family: var(--f-pixel, 'W95FA', 'Tahoma', sans-serif);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.78);
  background: rgba(2, 0, 127, 0.55);
  padding: 4px 8px;
  border: 1px solid rgba(255, 255, 255, 0.35);
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.55);
  max-width: 70vw;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (max-width: 480px) {
  .victory-cascade-label { font-size: 9px; padding: 3px 6px; }
}
.victory-cascade-skip {
  position: absolute;
  top: 18px;
  right: 22px;
  pointer-events: auto;
  font-family: var(--f-display, 'Tahoma', sans-serif);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ml-ink);
  background: var(--ml-gray);
  border: 0;
  padding: 6px 14px;
  cursor: pointer;
  /* Win95 raised bevel — matches the override layer's button chrome. */
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-hi-2),
    inset -1px -1px 0 var(--ml-gray-sh-2),
    inset  2px  2px 0 var(--ml-gray-hi),
    inset -2px -2px 0 var(--ml-gray-sh);
}
.victory-cascade-skip:hover { filter: brightness(1.04); }
.victory-cascade-skip:active {
  /* Pressed bevel: invert highlights/shadows. */
  box-shadow:
    inset  1px  1px 0 var(--ml-gray-sh),
    inset -1px -1px 0 var(--ml-gray-hi-2),
    inset  2px  2px 0 var(--ml-gray-sh-2),
    inset -2px -2px 0 var(--ml-gray-hi);
  transform: translate(1px, 1px);
}
.victory-cascade-skip:focus-visible {
  outline: 2px dashed var(--ml-navy);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  /* Skip the whole show for users who asked to. JS short-circuits the loop
     and never appends the overlay; this rule is just belt-and-suspenders. */
  .victory-cascade { display: none !important; }
}

/* ==========================================================================
   DRILL SERGEANT TIP — random easter egg, ~25% of lobby loads
   --------------------------------------------------------------------------
   Surfaced in host.html / player.html LOBBY views via the
   `[data-drill-tip]` mount and the public/js/drill-sergeant-tips.js module.
   The mount stays `hidden` when the dice misses; on a hit the module
   strips the attribute and paints `.drill-tip-tag` + `.drill-tip-body`
   inside. Voice matches the Boot Camp Manual modal — the tip is meant
   to read like a sergeant's aside while the recruit waits for the host
   to start. Yellow tag chip mirrors `.bcm-role`; the body sits on a
   parchment-tan card with an XP-era inset bevel so it reads like a
   pinned notice.
   ========================================================================== */
.drill-tip {
  margin: 0 auto;
  padding: 10px 12px 12px;
  background: #fffbe6;
  border: 1px solid #c4a83c;
  box-shadow:
    inset  1px  1px 0 #ffffff,
    inset -1px -1px 0 rgba(196, 168, 60, 0.55),
    0 2px 6px rgba(0, 0, 0, 0.15);
  font-family: var(--f-body);
  color: var(--ml-ink);
}
.drill-tip-tag {
  display: inline-block;
  background: #fff09a;
  color: var(--ml-ink);
  font-weight: 800;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 1px 8px;
  margin-bottom: 8px;
  box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5);
}
.drill-tip-body {
  font-size: 0.95rem;
  line-height: 1.45;
  font-style: italic;
  color: #1a1a1a;
}
@media (max-width: 480px) {
  .drill-tip { padding: 8px 10px 10px; }
  .drill-tip-body { font-size: 0.9rem; }
}

/* (BRIEFING-card styles removed 2026-05-01 — the "Round out"
   interstitial between SCORE and SUBMIT was retired, so .briefing-card,
   .briefing-title, .briefing-stat, .briefing-skip, and the
   @keyframes briefing-in animation are no longer needed.) */

/* ---------------------------------------------------------------------
   RECAP CANVAS — "best moments" share artifact reveal animation.
   When public/recap.js finishes composing the post-game card and host.js
   appends the <canvas> into #recapCanvasWrap, the canvas develops in
   like a polaroid: starts blurred, undersaturated, slightly small and
   transparent, settles into focus over ~700ms. Pure CSS — no JS timing
   coordination required, the animation runs once on element mount.
   Honors prefers-reduced-motion. The animation runs ONLY on the canvas
   itself; the wrap and its sibling controls (Save / Share / CSV) stay
   put so the eye lands on the new artifact, not on layout shift.
   --------------------------------------------------------------------- */
#recapCanvasWrap canvas {
  animation: recap-develop 700ms cubic-bezier(0.2, 0.7, 0.25, 1) both;
}
@keyframes recap-develop {
  0% {
    opacity: 0;
    transform: scale(0.96);
    filter: blur(8px) saturate(0.4) brightness(1.05);
  }
  60% {
    opacity: 1;
    filter: blur(2px) saturate(0.9) brightness(1.02);
  }
  100% {
    opacity: 1;
    transform: scale(1);
    filter: blur(0) saturate(1) brightness(1);
  }
}
@media (prefers-reduced-motion: reduce) {
  #recapCanvasWrap canvas { animation: none; }
}

/* ==========================================================================
   VICTORY CELEBRATION — winner-name plaque under the gigachad CROWNED hero
   --------------------------------------------------------------------------
   X106-2026-05-10. The post-cascade #view-victory-celebration shows the
   hand-painted hero-winner-crowned-gigachad.png (pixel-art Greek/Roman
   temple, gold "CROWNED" lettering, banners, laurel-clad columns) with
   Play Again + Back to Home buttons stacked beneath. The painted art
   doesn't say WHO got crowned — it just shows "CROWNED" as the static
   typography. Without an overlay, every win on every game looks
   identical: same temple, same fists, same word.

   Captain feedback: "I like this page, but we need to know who is the
   new gigachad. Add the winner's name somewhere under that image, but
   make it look cool, and like it fits with that page."

   The .victory-winner-card sits between the hero PNG and the button
   stack — close enough to the temple chrome that it reads as part of
   the engraving, not as modern UI bolted underneath. Visual vocabulary
   borrowed from the painted art:
     - Dark ember-brown plaque background (matches the cracked-stone
       monument plinth in the painting).
     - Gold double-border + offset shadow (matches the painted gold
       frame around the hero image).
     - Cream→gold→bronze gradient name text (matches the painted
       "CROWNED" letters' top-lit metallic shading).
     - Pixel-rendered text shadow + 1px stroke for hard edges (the
       hero PNG uses image-rendering: pixelated; the plaque text
       follows the same crisp-edge convention so the two read as
       one composition).
     - ★ ornaments on either side of the name (matches the
       five-star row painted across the bottom of the hero panel).
   No eyebrow / subtitle — Captain explicitly asked for ONLY the
   winner name on the page (BUILDING_RULES "Don't overexplain").
   An earlier draft had a "A new gigachad rises" all-caps label
   above the name; removed in the same X106 thread per Captain
   QA: "you didn't add any new wording, right? Just the winner
   name?"
   ========================================================================== */
.victory-winner-card {
  position: relative;
  display: block;
  /* X152-2026-05-15 (Captain: "there is a space between them and the
     name; there's a space between the name and the Chad thing. They
     should all look like one thing"). Pre-X152 bottom margin was 14px,
     which left a visible gap between the BRIMLEY plaque and the Save /
     Share chip row below. Set to 0 so the share-corner sits flush
     under the plaque; the chip row's own margin-top also goes to 0
     below for the same reason. -10px top is preserved so the plaque
     still tucks into the hero PNG's painted gold frame above. */
  margin: -10px auto 0;
  /* Slight pull-up overlap with the bottom of the hero PNG so the
     plaque visually attaches to the gold frame rather than floating
     in dead space. The hero already has a hand-painted gold border
     baked into the PNG; -10px tucks the plaque's top edge under the
     painted frame's bottom shadow. */
  padding: 14px 36px 16px;
  text-align: center;
  background:
    linear-gradient(180deg,
      #1d100a 0%,
      #2c1a10 50%,
      #1a0d07 100%);
  border: 2px solid #c89a4f;
  border-radius: 4px;
  box-shadow:
    0 0 0 1px #6e4a1a,
    0 6px 18px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 210, 140, 0.32),
    inset 0 -1px 0 rgba(0, 0, 0, 0.6);
  /* Match the hero PNG's pixelated rendering so antialiased borders
     don't read soft against the crunchy painting above. */
  image-rendering: pixelated;
}
.victory-winner-name {
  display: inline-block;
  position: relative;
  padding: 0 36px;
  font-family: var(--f-body);
  font-size: clamp(1.5rem, 4.6vw, 2.6rem);
  font-weight: 900;
  letter-spacing: 0.08em;
  line-height: 1;
  text-transform: uppercase;
  /* Cream-to-bronze gradient text — same top-lit metallic feel as the
     painted "CROWNED" letters in the hero PNG. Webkit-clip + transparent
     fallback so non-clip browsers still get readable gold text.

     X153-2026-05-14 readability pass #2. Captain on the "WILFORD
     BRIMLEY" screenshot after the prior X??-2026-05-15 attempt: "We
     redid the name on this a little bit to make it more readable,
     and it's still not readable." Root cause of the failed prior
     pass: the 4-direction 1px black sub-pixel stroke implemented via
     text-shadow rendered ON TOP of the gradient fill (text-shadow
     paints BEHIND text, but for thin Lato Heavy letters at clamp(24-
     42px) the 1px offsets in every direction collide with the gold
     fill and visually swamp it). Combined with #8a5a1a dark bronze
     at the bottom gradient stop sitting at near-identical luminance
     to the plaque background (#2c1a10 brown), and the 2px/3px black
     offset shadows piling more dark mass on top — the eye reads the
     whole letter as solid black on dark brown.

     The new approach drops the text-shadow stroke stack entirely
     and uses `-webkit-text-stroke` with `paint-order: stroke fill`,
     which puts a real stroke OUTSIDE the gradient fill instead of
     stacking shadows that compete with it. Gradient stops also
     shifted brighter: the dark bronze #8a5a1a bottom is gone,
     replaced by amber #d49526 which has actual contrast against
     the dark brown plaque. Single offset shadow + warm glow halo
     provide depth without crushing the fill. */
  background: linear-gradient(180deg,
    #fff8e0 0%,
    #ffe188 20%,
    #f5c357 50%,
    #e0a839 78%,
    #d49526 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: #f0c060;
  -webkit-text-fill-color: transparent;
  /* Real stroke (not stacked text-shadows) so the silhouette holds
     against the dark plaque without competing with the gold gradient
     fill. paint-order: stroke fill renders the stroke BEHIND the
     fill so the gradient extends to the letter edge and the stroke
     only shows outside — preserves gold pop on thin letters. The
     stroke is a very dark warm brown (not pure black) so it reads
     as "outlined gold lettering on wood" rather than "black stencil
     on wood." 1.5px is the sweet spot for the clamp(24-42px) font
     range — thinner and it disappears, thicker and it bites into
     the fill on letters with narrow strokes (E, F, T). */
  -webkit-text-stroke: 1.5px #1a0d04;
  paint-order: stroke fill;
  /* Depth + glow only — the silhouette is handled by the stroke
     above, so the shadow stack here just adds a small offset and
     a warm halo. Pure black offset (was a mix of pure black and
     rgba) for max contrast against the plaque. Glow alpha bumped
     0.45 → 0.6 so the halo carries on short names ("HOST") that
     don't have enough mass to be visible on their own. */
  text-shadow:
    2px 2px 0 rgba(0, 0, 0, 0.65),
    0 0 14px rgba(255, 200, 100, 0.6);
  /* Soft golden glow halo behind the text — keeps the gradient pop
     even when the player name is short ("HOST", "ZACH"). */
}
.victory-winner-name::before,
.victory-winner-name::after {
  content: "★";
  position: absolute;
  top: 50%;
  font-size: 0.7em;
  color: #c89a4f;
  -webkit-text-fill-color: #c89a4f;
  background: none;
  -webkit-background-clip: initial;
  background-clip: initial;
  text-shadow:
    1px 1px 0 #000,
    0 0 6px rgba(200, 154, 79, 0.6);
  transform: translateY(-50%);
  pointer-events: none;
}
.victory-winner-name::before { left: 4px; }
.victory-winner-name::after  { right: 4px; }

/* Mobile: tighten plaque padding and font so the name still fits
   inside the 660px-capped hero column on narrow viewports. The hero
   PNG already scales down (max-width:100% within the 660px cap), so
   the plaque needs to track the same. */
@media (max-width: 560px) {
  .victory-winner-card { padding: 10px 18px 12px; margin-top: -6px; }
  .victory-winner-name { padding: 0 28px; font-size: clamp(1.2rem, 6.4vw, 1.9rem); }
  .victory-winner-name::before { left: 2px; }
  .victory-winner-name::after  { right: 2px; }
}

/* ==========================================================================
   SHARE CORNER — small Save / Post-to-X chip row under every shareable
   hero on the END / victory-celebration surfaces.
   --------------------------------------------------------------------------
   X124-2026-05-12. Captain ask: "we also need download and share buttons
   on the winner screen and the Giga Chad screen and the final scoreboards
   so that people can save those or post those… make them small and in
   the bottom corner of the image under the image. Every hero, every
   year, big image that has a score on it should be shareable."

   The chips sit BELOW the hero (or scoreboard hero overlay) and are
   right-aligned so they read as a low-chrome affordance instead of a
   primary CTA. Visual vocabulary intentionally borrows from the per-meme
   `.meme-actions` toolbar (green Save + blue Post-to-X) used on the
   submission / vote / score cards so the share affordance is the same
   thing everywhere on the site, just smaller here because these heroes
   already carry the visual weight of the moment.

   IMPORTANT: this is the post-X89 reversal of the gigachad-page button
   strip. X89 stripped Save / Post off `#view-victory-celebration` under
   Captain's earlier "remove everything other than play again + back to
   home" ask, but the X106 winner-name plaque (the same surface now
   carries WHO won) changed the calculus — the page is shareable in a
   way it wasn't pre-X106, so the buttons earn their slot again. See the
   X112 thread snippet under hand-off/threads/ for the full handoff.
   ========================================================================== */
.share-corner {
  /* X152-2026-05-15 (Captain "they should all look like one thing"):
     justify-content flipped from `flex-end` to `center` and margin-top
     dropped from 6px to 0 so the chip row sits flush-centered under
     the BRIMLEY winner plaque rather than floating right with a gap.
     Combined with the victory-winner-card's margin-bottom:0 above,
     this makes the hero + plaque + chips read as one stacked unit. */
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 6px;
  margin: 0 auto;
  max-width: 660px;
  /* Slight right-edge inset so the chips don't crash into the rounded
     corner of the hero PNG above. Matches the visual gutter on the
     gigachad / situation-room / mission-complete hero frames. */
  padding: 0 4px;
}
/* X152-2026-05-15: was `.share-corner .btn` — the buttons no longer
   carry the `.btn` class (it triggered the 15-mlcom-overrides gray
   !important rule and made every chip look like a Win95 chrome button
   instead of the brand green/navy chip). Switched selector to
   `.meme-action-btn` so the share-corner-specific sizing still
   applies. The .meme-save-btn / .meme-share-btn rules in
   05-meme-stage.css carry the green/navy backgrounds now. */
.share-corner .meme-action-btn {
  /* "Really small" per Captain — tighter padding + smaller text than
     the default per-meme chips. The chip stays tap-friendly (32px tall
     meets WCAG min target via line-height + padding) without visually
     competing with the hero. */
  padding: 4px 10px;
  font-size: 0.85rem;
  line-height: 1.1;
  min-width: 0;
  min-height: 28px;
  border-radius: 6px;
}
/* Mobile: the heroes scale down but the chip row can stay the same
   size — already small enough. Just nudge the right margin in so it
   doesn't overflow the narrower lane. */
@media (max-width: 560px) {
  .share-corner { padding: 0 8px; }
}

/* Reduced-motion has nothing to disable here today (the plaque is
   static — no animation), but if a future thread adds a "land with
   weight" pop like .academy-name-photoshop--pop, this is the
   anchor. */


/* === inlined: styles/23-keyboard-shortcuts.css === */
/* ==========================================================================
   KEYBOARD SHORTCUTS — focus ring on cards + chip hints under primary CTAs
   --------------------------------------------------------------------------
   Wired by public/js/keyboard.js's `gridNav()` helper which adds/removes a
   `.kb-focused` class on the navigated card. Used by host.js / player.js /
   spectator.html across SUBMIT (hand picker) and VOTE (submission grid).
   Hidden on touch-primary devices via @media (pointer: coarse) so a
   tap-flicker doesn't leave a stale highlight.
   Ground rules:
   - Don't fight the existing brand vocabulary on cards. Submission cards
     already use `.voted` / `.your-own` / `.winner` / `.audience-favorite`
     for state. The keyboard ring is its own concern — a clear inset shadow
     plus a subtle scale so it reads "I'm aimed at this" without colliding
     with vote-state coloring.
   - Hint chips under buttons mirror the boot-camp manual's `<kbd>` style
     vocabulary so users who've seen the BCM modal recognize the chips.
   ========================================================================== */
.kb-focused {
  /* Layered shadow: thin black outline + thick brand-yellow halo + soft
     glow. The halo is the loud part; the black outline is the
     belt-and-suspenders edge so it stays visible against any card
     background (white sticker, voted-gold, audience-favorite glow, etc.). */
  outline: none; /* suppress browser default — we paint our own */
  box-shadow:
    0 0 0 2px #000,
    0 0 0 5px var(--ml-yellow, #ffe47a),
    0 0 14px 4px rgba(255, 228, 122, 0.55) !important;
  /* Tiny lift so the keyboard-aimed card pops above its peers without
     causing layout shift (transform doesn't trigger reflow). */
  transform: translateY(-1px) scale(1.01);
  /* Make sure the focus ring isn't clipped by the card's own border. */
  position: relative;
  z-index: 2;
  /* Keep the transition snappy — keyboard nav should feel instant, not
     animated. */
  transition: box-shadow 80ms ease-out, transform 80ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .kb-focused { transition: none; transform: none; }
}
@media (pointer: coarse) {
  /* Touch users don't get a focus ring left behind after a tap. */
  .kb-focused {
    box-shadow: none !important;
    transform: none;
  }
}

/* Chip hints under primary CTAs — "↵ Submit · ← → Navigate · Esc Back".
   Rendered inline by host.html / player.html under the relevant phase
   buttons. The chip-line uses the boot-camp manual's faded-paper
   vocabulary so it reads as a "tip" rather than another button.
   Hidden on touch-primary devices: the hint is for desktop users with a
   physical keyboard; mobile players don't need it cluttering their UI. */
.kb-hints {
  margin: 6px 0 0;
  padding: 0;
  font-family: var(--f-body);
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--ml-ink-muted, #6b6b6b);
  text-align: center;
  /* Word-spacing so the · separators between chip groups breathe a little
     even when the row wraps onto two lines on a narrow viewport. */
  word-spacing: 2px;
  letter-spacing: 0.01em;
}
.kb-hints kbd {
  display: inline-block;
  min-width: 1.4em;
  padding: 1px 5px;
  margin: 0 1px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.78em;
  font-weight: 600;
  color: #2b2b2b;
  background: #f4ecd6;
  border: 1px solid #b9a766;
  border-radius: 3px;
  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.18);
  text-align: center;
  vertical-align: baseline;
  white-space: nowrap;
}
@media (pointer: coarse) {
  /* On a touch device the hints are noise — hide them entirely. The
     :not(.kb-hints-force) escape hatch is for any future surface that
     wants to keep the hints visible in spite of the touch-detect (e.g.
     a chromebook in tablet mode where the user IS using a keyboard). */
  .kb-hints:not(.kb-hints-force) { display: none; }
}


/* === inlined: styles/24-situation-room.css === */
/* ==========================================================================
   Situation Room — Marketer Mode wall-of-monitors layout — 2026-05-05 (X65)
   ==========================================================================
   The "Monitoring the Situation" trading-floor meme, made literal. Each card
   on /marketer.html is now a bezeled CRT monitor mounted on a dim wall. The
   layout is a grid of monitors: situation feeds across the top row, the
   captioning Reactor + newest-meme strip across the main row, an activity
   chyron at the bottom. Every monitor has a Pop Out button so a paid user
   can drag any single panel onto a real second physical monitor.

   Pairs with the existing .mw-crt + .mw-chyron primitives in
   18-monitoring-layer.css — those handle the scanlines + green-phosphor
   status bars. This file adds the bezel chrome and the room-level grid.

   Hard rules:
   - Mobile (< 720px) stacks every monitor to a single column. The bezels
     thin out so a phone doesn't look like it's wearing a parka.
   - Pop-out URL: /marketer.html?panel=<id>&popout=1. The page CSS responds
     to body[data-popout="1"] by hiding everything except the requested
     monitor and stretching it to fill the viewport.
   - Don't add hint text under the bezels (BUILDING_RULES "Don't overexplain").
     The popout button + panel title are the only chrome on each frame.
   ========================================================================== */

/* ── HUD palette — situation-room-only accent colors ───────────────────
   X102-2026-05-08. Captain: "replace the current green color scheme
   with this one. But don't touch the layout, and leave the current
   main white font. So you are basically just redoing the accent
   colors." These tokens replace the X65 green-phosphor scheme
   (var(--ml-green-*) + var(--sr-panel-bg) / var(--sr-panel-bg-2) etc.) with a cyan / amber
   command-center palette. Scoped to .sr-page (on <main> for
   marketer.html / marketermode.html) so the rest of the site's
   green-phosphor primitives in 18-monitoring-layer.css are not
   affected — only their cosmetic appearance INSIDE the situation
   room is overridden, via the .sr-room overrides at the bottom of
   this file. */
.sr-page {
  --sr-cyan:         #66e6ff;
  --sr-cyan-dark:    #1e4a64;
  --sr-cyan-mid:     #3aa6c8;
  --sr-panel-bg:     #050d1a;
  --sr-panel-bg-2:   #0a1828;
  --sr-panel-bg-3:   #0e1f30;
  --sr-btn-bg:       #0e2840;
  --sr-btn-bg-hover: #15426a;
  --sr-text-dim:     #93b3c8;
}

/* ── Page-level container opt-out ────────────────────────────────────────
   The shared .container max-width is 660px (was 880 X65 → X78 720
   → X79 660; the lane has swung three times). That's right for
   game/host/player pages where shared widgets must render the same
   size across roles. The situation room has no such constraint — it
   wants lateral room to fit a wall of 7 monitors PLUS a 220px meme
   selector rail.

   X118-2026-05-10 — Captain: "this page needs to be exempted from the
   rules that are keeping the infinite scroll wheel on the side for
   meme selection and the situation monitor on the left side from
   co-existing in the same space. They need to be both at the same
   exact time, wider than any of the other pages, because it's two
   elements that are working in tandem."

   The prior cap at `min(96vw, 1280px)` was too narrow on standard
   laptops: 1280px minus container padding (48px) minus .sr-room
   padding (28px) minus the 220px rail minus the 14px wall↔rail gap
   left only ~970px for the 4-column wall-grid, i.e. ~240px per
   monitor column. With the locked-overlay on the rail visually
   pinning the right side of the screen, the wall-grid felt
   compressed and the visual hierarchy broke (rail and monitor
   cluster fought for the same horizontal lane).

   X118 drops the cap entirely (down to a small breathing margin from
   the viewport edge). On a 1920px screen the wall-grid now gets ~1620px
   to spread across — each monitor column ~390px, plenty for the side
   text + chrome. On a 1366px laptop it gets ~1075px — each column
   ~260px, still comfortable. Below 920px the mobile fallback (line
   ~1000) kicks in and the rail stacks below the wall, where width
   isn't the constraint anymore.

   This override only fires when `.sr-page` is on the element — so
   game/host/player and every other surface keep their existing 660px
   cap, and the 660px `.card` cap inside auth gates etc. is unaffected. */
.container.sr-page {
  max-width: calc(100vw - 24px);
  width: 100%;
}

/* ── Workstation full-screen mode ────────────────────────────────────────
   X120-2026-05-10 — Captain: "I want this just to cover the entire page
   so they feel like there's no distractions; it's only the situation and
   the memes, and I want them both to stretch from top to bottom so we
   just focus on this as a workstation situation monitor workstation."

   The `sr-fullscreen` class on <body> (set in marketer.html and
   marketermode.html) flips the situation room into a chromeless
   full-viewport workstation: site-header banner + Riah footer badge
   hidden, .container.sr-page expands to the full viewport (overriding
   the X118 `calc(100vw - 24px)` breathing margin), .sr-room fills the
   entire viewport edge-to-edge, .sr-wall expands to fill the room
   minus the status strip + room padding so the rail + monitor cluster
   stretch top-to-bottom of the working area.

   Scoped to `body.sr-fullscreen` so it only fires on the two situation
   room pages. Other surfaces (host/player/spectator/index/auth) keep
   their normal site-header + footer chrome. The mobile fallback at
   `@media (max-width: 920px)` below the workstation rules restores
   default sizing for phones — the wall stacks the rail beneath the
   wall-grid on mobile (per the existing media query), so a fixed
   100vh wall would clip half the content. Captain framing on this:
   "people won't be using this on mobile, hopefully."

   X121-2026-05-12 (Captain QA: "for this page, can we get it so it
   doesn't scroll at all? Right now it's a slightly different size
   than the page, so you can scroll. It'd be best if this page was
   just no scroll"): the X120 workstation block locked the room to
   100vh but didn't actually prevent the page from scrolling. Two
   reasons the X120 attempt drifted past the viewport:

     (a) The X120 magic number `100vh - 90px` for the wall budgeted
         90px of room chrome (top padding + status strip + status
         bottom margin + bottom padding). It did NOT account for the
         X123 genre bar that landed after X120, or the existing
         .sr-ticker at the bottom of the room. With the genre bar at
         ~36px and the ticker at ~50px, the chrome budget is closer
         to 175px — so the wall was overshooting by ~85px and pushing
         the ticker past the room's 100vh edge.

     (b) `.sr-room` was `position: relative` but NOT `overflow:
         hidden`, and main was `min-height: 100vh` (not a hard cap),
         so when children overflowed they grew the page past the
         viewport instead of clipping. html + body still had default
         overflow:visible, so the document gained the scroll Captain
         was seeing.

   X121 fix:
     1. body + html both lock to 100vh + overflow:hidden in
        fullscreen mode. The html-side lock uses `:has()` so it
        stays scoped to the fullscreen surfaces (host/player/etc.
        keep normal html scroll behavior). 100dvh shipped as a
        companion unit so mobile dynamic-viewport bounce stays in
        sync; older browsers fall through to the 100vh declaration.
     2. main + .sr-room flip from min-height/height to a hard
        `height: 100vh` + `overflow: hidden`.
     3. .sr-room becomes `display: flex; flex-direction: column;`
        so its children (status strip, genre bar, wall, ticker)
        flow vertically and distribute via flex.
     4. .sr-wall drops the X120 magic `calc(100vh - 90px)` and gets
        `flex: 1 1 auto; min-height: 0;` instead — it now takes
        whatever's left after the natural-height chrome children,
        with no constant to keep in sync when chrome is added or
        removed.

   The paywall overlay (#paywallOverlay) is `position: absolute`
   inside .sr-room so it covers the room edge-to-edge regardless of
   the flex layout. The #srLegacy stub-shells block is `hidden`, so
   it doesn't take a flex track. */
html:has(body.sr-fullscreen),
body.sr-fullscreen {
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}
body.sr-fullscreen {
  margin: 0;
  padding: 0;
}
body.sr-fullscreen .site-header,
body.sr-fullscreen .footer-tag {
  display: none !important;
}
body.sr-fullscreen main.container.sr-page {
  max-width: 100vw;
  width: 100vw;
  padding: 0;
  margin: 0;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}
body.sr-fullscreen .sr-room {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;
  margin: 0;
  border: 0;
  border-radius: 0;
  overflow: hidden;
}
/* X121-2026-05-12: replaces the X120 `height: calc(100vh - 90px)`
   magic number with `flex: 1 1 auto; min-height: 0`. The wall now
   takes whatever vertical space is left after the room's other
   children (status strip + genre bar + ticker) lay out at their
   natural heights — so it doesn't need to be re-tuned every time a
   chrome element is added or removed. `min-height: 0` is the
   standard fix for "flex child with internal overflow doesn't
   shrink past its content min-height"; without it the wall would
   refuse to shrink below its tallest child and would push the
   ticker off-screen.

   The rail also picked up `.meme-strip { max-height: 70vh }` from
   the sticky-rail rule below (line ~446); the companion override
   below clears that cap so the rail stretches the full wall height
   along with the grid. */
@media (min-width: 921px) {
  body.sr-fullscreen .sr-wall {
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
    max-height: none;
  }
  body.sr-fullscreen .sr-meme-rail {
    max-height: none;
    min-height: 0;
  }
}

/* X162-2026-05-15: release the X121 "no-scroll workstation" lock on
   phones. The X121 block clamps html / body / main / .sr-room to
   `height: 100vh` + `overflow: hidden` so the desktop wall fits in
   exactly one screenful with no page scroll — Captain's framing was
   "for this page, can we get it so it doesn't scroll at all?" That
   stance assumed desktop. The X121 comment up at line ~106 even
   promises "The mobile fallback at `@media (max-width: 920px)` below
   the workstation rules restores default sizing for phones" — but
   that fallback never actually shipped. With the lock in force AND
   the X80/X138 mobile single-column wall (7 stacked monitors needing
   ~900px), phones rendered the genre bar + the top ~600px of the
   wall and clipped the rest into nothing — Captain's report: "no
   situation monitor, two lines of memes, three lines of select the
   category, then live monitoring the situation." That was the wall
   existing but trapped behind overflow:hidden with no way to scroll
   to it. Releasing the lock on ≤920px lets the page scroll naturally,
   which is the right mobile UX — phones are scroll-first surfaces. */
@media (max-width: 920px) {
  html:has(body.sr-fullscreen),
  body.sr-fullscreen {
    height: auto;
    min-height: 100vh;
    min-height: 100dvh;
    overflow: visible;
  }
  body.sr-fullscreen main.container.sr-page {
    height: auto;
    min-height: 100vh;
    min-height: 100dvh;
    overflow: visible;
  }
  body.sr-fullscreen .sr-room {
    height: auto;
    min-height: 100vh;
    min-height: 100dvh;
    overflow: visible;
  }
}

/* ── Wall backdrop ───────────────────────────────────────────────────────
   Marketer.html main container gets a dim charcoal wall instead of the
   default Bliss/desktop. Subtle vignette + a faint scanline overlay across
   the whole grid sells "we are looking at a wall of CRTs from the bar". */
.sr-room {
  position: relative;
  margin: 0 auto;
  padding: 18px 14px 28px;
  background:
    radial-gradient(ellipse at center top, #1a1f28 0%, #0a0d12 55%, #050608 100%);
  border: 2px solid #000;
  box-shadow:
    inset 0 0 0 1px #2a2f38,
    inset 0 60px 120px rgba(0, 0, 0, 0.55),
    0 8px 24px rgba(0, 0, 0, 0.5);
  isolation: isolate;
}
.sr-room::before {
  /* faint room-wide scanline overlay; sits under the monitors so the bezels
     stay crisp. Uses repeating-linear-gradient for cheap CRT vibe. */
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.012) 0,
    rgba(255, 255, 255, 0.012) 1px,
    transparent 1px,
    transparent 4px
  );
  z-index: 0;
}
.sr-room > * { position: relative; z-index: 1; }

/* ── Top status strip ────────────────────────────────────────────────────
   Thin bar that runs across the top of the wall — channel label, live
   indicator, time. Reads as "you are looking at the news desk". */
/* X139-2026-05-14: `.sr-status-strip` retired. Captain consolidated
   the channel ribbon ("LIVE · MONITORING THE SITUATION · clock ·
   CHANNEL 01 · MEMELORD") into the genre bar's far-right edge — see
   `.sr-genre-live` below + the matching marketer.html change. The
   strip's rule block is intentionally gone (not commented out) so a
   future stylesheet sweep doesn't accidentally re-mount a dead
   selector; the LIVE-dot styling reappears scoped to `.sr-genre-live`
   so the cyan-on-navy chip reads the same. */

/* ── Genre selector pill row (X123-2026-05-12) ─────────────────────────
   Horizontal row of category pills above the wall. Renders on both
   /marketer.html and /marketermode.html so paid and free users share the
   same filter shape. Populated by renderMainSituationTabs() in JS — one pill
   per slug in `mainSituation.categories` (server-echoed), labeled via
   TOP_OF_DAY_LABELS. The active pill gets a cyan outline; the rest
   render flush against the SR panel background. */
.sr-genre-bar {
  /* X140-2026-05-14: reverted to the pre-X139 flat-flex shape after
     the X139 nested-pills restructure broke the pill row (only ALL
     rendered). The LIVE · MONITORING THE SITUATION lockup is a
     sibling of the pills inside the same .sr-genre-bar (collapsed
     from the retired .sr-status-strip), pushed flush-right via the
     .sr-genre-live `margin-left: auto` rule below. renderMainSituationTabs
     preserves the LIVE node across its innerHTML wipe — see the
     X140 same-dated note in marketer.html. */
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  padding: 8px 12px;
  margin: 0 0 14px;
  background: var(--sr-panel-bg);
  border: 1px solid var(--sr-cyan-dark);
  border-radius: 0;
}
.sr-genre-bar .sr-genre-live {
  /* X140-2026-05-14: LIVE chip pinned flush-right inside the same row
     as the category pills. The migrated lockup carries the cyan-on-
     navy chip styling the retired `.sr-status-strip` used. */
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  color: var(--sr-cyan);
  font-family: var(--f-pixel);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.sr-genre-bar .sr-genre-live .sr-status-live {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.sr-genre-bar .sr-genre-live .sr-status-live::before {
  /* X65 follow-up (Captain): no flicker. The "live" dot is a steady
     soft glow — present, readable, not flashing. Same chip styling
     the retired `.sr-status-strip .sr-status-live::before` used. */
  content: "";
  width: 8px; height: 8px;
  border-radius: 50%;
  background: #c41e1e;
  box-shadow: 0 0 6px rgba(255, 60, 60, 0.55);
  opacity: 0.85;
}
.sr-genre-bar .sr-genre-pill {
  font-family: var(--f-pixel);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 5px 10px;
  background: transparent;
  color: var(--sr-cyan);
  border: 1px solid var(--sr-cyan-dark);
  border-radius: 0;
  cursor: pointer;
  line-height: 1.2;
}
.sr-genre-bar .sr-genre-pill:hover {
  background: rgba(102, 230, 255, 0.12);
}
.sr-genre-bar .sr-genre-pill[aria-pressed="true"] {
  outline: 2px solid var(--g-primary-hot);
  outline-offset: -2px;
  background: rgba(102, 230, 255, 0.18);
  color: #fff;
}
/* X126-2026-05-12: Custom Station pill (gold accent, matches the
   "1 credit" chip on the Automatic Banger button so the paid-only +
   user-owned vocabulary reads consistent across Marketer Mode). The
   pill is injected into mainSituation.categories by ensureCustomPill() only
   when state.user.customStationSet is true — free / unset paid users
   never see this rule. renderMainSituationTabs() carries data-slug on each
   pill (set in btn.dataset.slug). */
.sr-genre-bar .sr-genre-pill[data-slug="custom"] {
  border-color: #d4a017;
  color: #f0c850;
}
.sr-genre-bar .sr-genre-pill[data-slug="custom"]:hover {
  background: rgba(240, 200, 80, 0.12);
}
.sr-genre-bar .sr-genre-pill[data-slug="custom"][aria-pressed="true"] {
  outline-color: #f0c850;
  background: rgba(240, 200, 80, 0.18);
  color: #fff8d8;
}

/* ── The grid of monitors ───────────────────────────────────────────────
   Desktop: 12-column grid. Top row carries 3 small "TV" feeds (situation
   board / X / Reddit-or-HN). Main row carries the Reactor (wide) + the
   newest-meme strip (narrow). Activity ticker spans the full width.
   Mobile: single column, monitors stack in DOM order. */
.sr-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 14px;
}
.sr-grid > .sr-monitor {
  min-width: 0;            /* lets the inner content actually shrink */
}

/* span helpers */
.sr-grid .sr-span-3  { grid-column: span 3; }
.sr-grid .sr-span-4  { grid-column: span 4; }
.sr-grid .sr-span-5  { grid-column: span 5; }
.sr-grid .sr-span-6  { grid-column: span 6; }
.sr-grid .sr-span-7  { grid-column: span 7; }
.sr-grid .sr-span-8  { grid-column: span 8; }
.sr-grid .sr-span-9  { grid-column: span 9; }
.sr-grid .sr-span-12 { grid-column: span 12; }

@media (max-width: 920px) {
  .sr-grid {
    grid-template-columns: 1fr;
    gap: 10px;
  }
  .sr-grid .sr-span-3,
  .sr-grid .sr-span-4,
  .sr-grid .sr-span-5,
  .sr-grid .sr-span-6,
  .sr-grid .sr-span-7,
  .sr-grid .sr-span-8,
  .sr-grid .sr-span-9,
  .sr-grid .sr-span-12 { grid-column: 1 / -1; }
}

/* ── A single monitor ────────────────────────────────────────────────────
   Heavy plastic bezel + recessed dark screen + green-phosphor chyron header
   + popout button. Apply .mw-crt to the .sr-screen child to layer scanlines
   over the live content (we don't put .mw-crt on .sr-monitor itself because
   the bezel shouldn't have scanlines). */
.sr-monitor {
  position: relative;
  display: flex;
  flex-direction: column;
  background: linear-gradient(180deg, #2a2f38 0%, #1a1d22 100%);
  border: 1px solid #000;
  border-radius: 6px;
  /* outer plastic bezel + inner screen recess + ambient glow */
  box-shadow:
    inset 0 1px 0 #4a4f58,
    inset 0 -1px 0 #050608,
    inset 1px 0 0 #3a3f48,
    inset -1px 0 0 #15181c,
    0 2px 6px rgba(0, 0, 0, 0.6),
    0 0 24px rgba(102, 230, 255, 0.04);
  padding: 8px;
  min-height: 0;
}

/* monitor header — channel label + live LED + popout button */
.sr-monitor-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px;
  margin: -4px -4px 6px;
  background: var(--sr-panel-bg);
  color: var(--sr-cyan);
  font-family: var(--f-pixel);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--sr-cyan-dark);
  border-radius: 4px 4px 0 0;
  box-shadow: inset 0 1px 0 rgba(102, 230, 255, 0.18);
}
.sr-monitor-head .sr-monitor-led {
  /* X65 follow-up (Captain): no flicker on the wall. The LEDs sit lit
     at a soft ambient level — visible if you look, but fading to the
     background when you're not focused on them. The mwBlink animation
     that the original primitives ship is explicitly not applied here. */
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--sr-cyan);
  box-shadow: 0 0 4px rgba(102, 230, 255, 0.55);
  opacity: 0.7;
  flex: 0 0 auto;
}
.sr-monitor-head .sr-monitor-led.amber {
  background: var(--ml-gold);
  box-shadow: 0 0 4px rgba(255, 215, 0, 0.55);
}
.sr-monitor-head .sr-monitor-led.red {
  background: #c41e1e;
  box-shadow: 0 0 5px rgba(255, 60, 60, 0.55);
}
.sr-monitor-head .sr-monitor-title {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sr-monitor-head .sr-popout-btn {
  appearance: none;
  border: 1px solid var(--sr-cyan-dark);
  background: transparent;
  color: var(--sr-cyan);
  font-family: var(--f-pixel);
  font-size: 9px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 2px 6px;
  cursor: pointer;
  flex: 0 0 auto;
  border-radius: 0;
  line-height: 1;
}
.sr-monitor-head .sr-popout-btn:hover {
  background: rgba(102, 230, 255, 0.12);
  color: #fff;
}
.sr-monitor-head .sr-popout-btn:focus-visible {
  outline: 1px dotted var(--sr-cyan);
  outline-offset: 2px;
}

/* the screen itself — recessed, dark, scanlines come from .mw-crt */
.sr-screen {
  flex: 1 1 auto;
  min-height: 0;
  background: #0a0d12;
  color: var(--sr-cyan);
  border: 1px solid #000;
  border-radius: 0 0 4px 4px;
  padding: 10px;
  overflow: auto;
  font-family: var(--f-body);
  font-size: 0.92rem;
  /* recessed CRT look */
  box-shadow:
    inset 0 0 0 1px #1a1d22,
    inset 0 0 24px rgba(0, 0, 0, 0.7);
}
.sr-screen.sr-screen-tight { padding: 0; }

/* ── Per-monitor content tweaks ─────────────────────────────────────────
   Cards inside .sr-screen are pre-themed for the dark backdrop. We rewrite
   the surface colors on common children rather than rebuild every widget. */
.sr-screen .board-list { color: #fff; }
.sr-screen .board-list li {
  border-bottom-color: rgba(102, 230, 255, 0.12) !important;
  color: #fff;
}
.sr-screen .board-list li:hover {
  background: rgba(102, 230, 255, 0.06) !important;
}
.sr-screen .small.muted,
.sr-screen .muted { color: var(--sr-text-dim) !important; }
.sr-screen select,
.sr-screen input[type="url"],
.sr-screen input[type="text"] {
  background: var(--sr-panel-bg);
  color: var(--sr-cyan);
  border: 1px solid var(--sr-cyan-dark);
  border-radius: 0;
  padding: 4px 6px;
  font-family: var(--f-body);
}
.sr-screen .menu-btn {
  background: var(--sr-btn-bg);
  color: var(--sr-cyan);
  border: 1px solid var(--sr-cyan-dark);
  box-shadow: inset 0 1px 0 rgba(102, 230, 255, 0.15);
}
.sr-screen .menu-btn:hover {
  background: var(--sr-btn-bg-hover);
  color: #fff;
}
.sr-screen h2 {
  color: var(--sr-cyan);
  font-size: 0.95rem;
  margin: 0 0 8px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.sr-screen h2 .xp-icon { width: 16px; height: 16px; image-rendering: pixelated; }
.sr-screen hr { border-color: rgba(102, 230, 255, 0.18); }
.sr-screen p { color: #fff; }
.sr-screen .row { color: #fff; }

/* situation board rows — pop-classified look */
.sr-screen .board-row {
  font-size: 0.9rem;
  padding: 8px 10px !important;
  border-bottom: 1px solid rgba(102, 230, 255, 0.15) !important;
}

/* ── REACTOR layout (the main captioning monitor) ───────────────────────
   The Reactor is the biggest monitor in the wall — split horizontally
   into the captioning surface on the left and the meme strip on the
   right. On a real second-monitor pop-out the strip drops below for
   narrow windows. */
.sr-reactor {
  display: grid;
  grid-template-columns: 1fr 220px;
  gap: 12px;
  align-items: start;
}
.sr-reactor .reactor-main {
  min-width: 0;
}
@media (max-width: 720px) {
  .sr-reactor {
    grid-template-columns: 1fr;
  }
}

/* ── Newest meme strip — was missing from the styles split.
   This is the same affordance documented in
   docs/LIVE_SITUATION_LAUNCH_OPS.md "Newest-meme lane". Vertical sticky
   tile column, infinite scroll, click → load into the captioning surface.
   Originally landed 2026-05-05 in the monolith styles.css; the X53 split
   accidentally dropped these rules. Restored here as part of the
   situation-room layer so the strip works again on the dark backdrop. */
.meme-strip {
  position: sticky;
  top: 12px;
  display: flex;
  flex-direction: column;
  gap: 0;
  background: var(--sr-panel-bg);
  border: 1px solid var(--sr-cyan-dark);
  border-radius: 0;
  max-height: 70vh;
  min-height: 320px;
  overflow: hidden;
}
.meme-strip-header {
  /* X125b-2026-05-12: header now carries only the 🎲 button (the
     "Newest" / "Shuffled" title span was dropped per Captain:
     "we should remove the newest or shuffle wording"). Flipped from
     `justify-content: space-between` to `flex-end` so the lone
     shuffle button stays right-aligned on the cyan bar; with a single
     child, space-between would have collapsed it to the left. */
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  padding: 6px 8px;
  background: var(--sr-panel-bg-2);
  border-bottom: 1px solid var(--sr-cyan-dark);
  color: var(--sr-cyan);
  font-family: var(--f-pixel);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
/* X125b-2026-05-12: `.meme-strip-title` rule removed — the span it
   styled is no longer in the markup on either marketer.html or
   marketermode.html. `.meme-strip-shuffle[aria-pressed="true"]`
   selector also removed — the shuffle button is no longer a toggle,
   so there's no pressed-state visual to carry. */
.meme-strip-shuffle {
  appearance: none;
  background: transparent;
  border: 1px solid var(--sr-cyan-dark);
  color: var(--sr-cyan);
  padding: 2px 6px;
  font-family: var(--f-pixel);
  font-size: 9px;
  letter-spacing: 0.08em;
  cursor: pointer;
  border-radius: 0;
}
.meme-strip-shuffle:hover { background: rgba(102, 230, 255, 0.12); color: #fff; }
/* X123-2026-05-12 (Captain): "No labels for any of the memes. People
   see what they are, and with the labels we're wasting space. We need
   to have more meme, and the memes need to fill out so less like boxes
   around the memes, more just meme meme meme. With slight separators."
   Pre-X123: 8px scroll padding + 8px gap + 1px tile border + 6px tile
   padding + a label row under each image = ~30px of chrome per tile +
   the label height. Post-X123: 4px scroll padding, no per-tile padding
   or border, no label, a 1px separator line between tiles. Tile
   image is now the only visible content; hover tooltip + aria-label
   carry the name for users who need it. */
.meme-strip-scroll {
  flex: 1 1 auto;
  overflow: auto;
  padding: 4px;
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
}
.meme-strip-tile {
  appearance: none;
  display: block;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  color: var(--sr-cyan);
  cursor: pointer;
  text-align: left;
  font-family: var(--f-body);
  /* Slight separator between tiles — 1px hairline in the dim cyan
     palette. First tile suppresses its own separator below. */
  border-top: 1px solid var(--sr-cyan-dark);
}
.meme-strip-tile:first-child { border-top: 0; }
.meme-strip-tile:hover .meme-strip-tile-img {
  filter: brightness(1.15);
}
.meme-strip-tile.is-active .meme-strip-tile-img {
  outline: 2px solid var(--ml-gold);
  outline-offset: -2px;
}
.meme-strip-tile-img {
  width: 100%;
  height: 120px;
  object-fit: cover;
  background: #050608;
  display: block;
}
.meme-strip-empty,
.meme-strip-loadmore,
.meme-strip-error {
  text-align: center;
  padding: 12px 8px;
  color: var(--sr-cyan-mid);
  font-family: var(--f-pixel);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.meme-strip-error { color: #ff7070; }

/* ── Reactor capture surface tweaks (dark background) ───────────────── */
.sr-screen #memePreview img {
  background: var(--sr-panel-bg-3);
  border: 1px solid var(--sr-cyan-dark);
}
.sr-screen #memePreview {
  text-align: center;
}
.sr-screen #slotStage {
  color: #fff;
}
.sr-screen #slotStage label,
.sr-screen #slotStage .slot-label {
  color: var(--sr-cyan);
}
.sr-screen #slotStage textarea,
.sr-screen #slotStage input {
  background: var(--sr-panel-bg-3);
  color: #fff;
  border: 1px solid var(--sr-cyan-dark);
}

/* ── Activity chyron / ticker — bottom of the wall ─────────────────────
   X65: scrolling green-phosphor strip; was pure flair carrying
   hard-coded marquee strings.
   X96 (2026-05-08): repurposed as the C-tier news ticker. Each span
   is now an interactive headline; the marquee pauses on hover so the
   click target is reachable, the leftmost element is a "Refresh
   feed" button that re-fetches /top-of-day, and an empty-state
   placeholder fills in when no C-tier items remain. The background +
   font + 28px height stay identical so the X65 visual register
   doesn't shift. */
.sr-ticker {
  position: relative;
  display: flex;
  align-items: stretch;
  overflow: hidden;
  white-space: nowrap;
  background: var(--sr-panel-bg);
  color: var(--sr-cyan);
  font-family: var(--f-pixel);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  border: 1px solid var(--sr-cyan-dark);
  height: 28px;
  box-sizing: border-box;
}
.sr-ticker-refresh {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  background: rgba(102, 230, 255, 0.12);
  color: var(--sr-cyan);
  border: 0;
  border-right: 1px solid var(--sr-cyan-dark);
  font-family: inherit;
  font-size: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
  user-select: none;
}
.sr-ticker-refresh:hover { background: rgba(102, 230, 255, 0.22); }
.sr-ticker-refresh:disabled { opacity: 0.4; cursor: progress; }
.sr-ticker-track {
  flex: 1 1 auto;
  display: inline-block;
  overflow: hidden;
  padding: 6px 0 6px 100%;
  /* Bumped from 60s to 90s — the longer C-tier list reads better at
     a slower pace and gives users time to spot a clickable headline. */
  animation: srTicker 90s linear infinite;
}
.sr-ticker:hover .sr-ticker-track {
  /* Pause-on-hover so the click target stops moving when the user is
     about to click. Same pattern as the YouTube / Twitch chat
     marquees Captain referenced in early Situation Room sketches. */
  animation-play-state: paused;
}
.sr-ticker-track > .sr-ticker-item,
.sr-ticker-track > span {
  display: inline-block;
  padding: 0 28px;
  border-right: 1px solid rgba(102, 230, 255, 0.25);
}
.sr-ticker-item {
  cursor: pointer;
  outline: none;
}
.sr-ticker-item:hover,
.sr-ticker-item:focus-visible {
  background: rgba(102, 230, 255, 0.18);
  color: #fff;
}
.sr-ticker-tag {
  display: inline-block;
  margin-right: 8px;
  padding: 0 5px;
  font-size: 0.85em;
  letter-spacing: 0.08em;
  background: rgba(102, 230, 255, 0.22);
  color: var(--sr-cyan);
  border-radius: 3px;
}
.sr-ticker-empty {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  padding: 0 12px;
  opacity: 0.55;
}
@keyframes srTicker {
  from { transform: translateX(0); }
  to   { transform: translateX(-100%); }
}
@media (prefers-reduced-motion: reduce) {
  .sr-ticker-track { animation: none; padding-left: 12px; }
  .sr-ticker:hover .sr-ticker-track { animation: none; }
}

/* ── Per-monitor refresh button (X96) ─────────────────────────────────
   Small ↻ icon docked in each side monitor's header. Click rotates
   that one slot to a different unused C-tier item without a server
   call. Sized to fit alongside the .sr-monitor-led + chyron text
   without disturbing the X80d two-tier header layout. */
.sr-monitor-head .sr-side-refresh {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  background: transparent;
  color: var(--sr-cyan);
  border: 1px solid var(--sr-cyan-dark);
  border-radius: 3px;
  font-family: inherit;
  font-size: 11px;
  cursor: pointer;
  opacity: 0.55;
  transition: opacity 120ms ease, background 120ms ease;
}
.sr-monitor-head .sr-side-refresh:hover,
.sr-monitor-head .sr-side-refresh:focus-visible {
  opacity: 1;
  background: rgba(102, 230, 255, 0.18);
  outline: none;
}

/* 2026-05-14 (Captain): Refresh All chip lives inside the primary monitor's
   head (X153 moved it here from the wall corner — that location overlapped
   one of the right-column side monitors and read like a per-slot refresh
   on the wrong cell). Inherits .sr-side-refresh's cyan register; the
   .sr-primary-refresh-all modifier just widens it slightly so the "all"
   label fits next to the ↻ glyph. */
.sr-monitor-head .sr-primary-refresh-all {
  width: auto;
  padding: 0 6px;
  gap: 3px;
  font-size: 9px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.sr-primary-refresh-all-label {
  font-weight: 700;
}

/* ── Result + share row ────────────────────────────────────────────────
   Below the wall, when a meme is rendered. Captain wants the Twitter
   share button right there next to the download. */
.sr-share-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 12px;
}
.sr-share-row .menu-btn { font-size: 0.9em; }
.sr-post-x {
  background: #000 !important;
  color: #fff !important;
  border: 1px solid #fff !important;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18) !important;
}
.sr-post-x:hover {
  background: #1d9bf0 !important;
  border-color: #1d9bf0 !important;
}

/* ── Automatic Banger (X95-2026-05-08, chip-as-link X121-2026-05-12) ──
   Premium one-shot power-up that lives next to the Render button in
   the primary monitor. The "1 credit" chip is a permanent badge to
   the right of the button so the cost is visible at rest, not just
   on hover. The result card mounts below #resultCanvas — kept
   visually distinct from the user's own render so the comparison
   reads ("AI did this for 1 credit; you did this with your own
   captioning").

   X121-2026-05-12: the chip moved from being a <span> INSIDE the
   button to a sibling <a> outside it, wrapped together with the
   button inside `.sr-banger-group` so they still read as a unit. The
   chip is now a navigation surface (memelord.com buy-credits page —
   memelord's "meme AGI" credit purchase flow), not just a cost
   label. Selector was broadened: `.sr-banger-btn .sr-banger-cost`
   → `.sr-banger-cost` (the chip lives outside the button now).

   X121 (Captain correction): the X95 :disabled state on the button
   was retired — the product model is "a situation is picked 100%
   of the time," so there's no no-pick state to fade for. The
   `.sr-banger-btn:disabled` rule below is kept as a defensive
   fallback for any transient disabled state (e.g. mid-generation),
   not as the resting state. */
.sr-banger-group {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
.sr-banger-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
}
.sr-banger-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.sr-banger-cost {
  font-size: 0.72em;
  letter-spacing: 0.04em;
  padding: 2px 6px;
  border-radius: 999px;
  background: rgba(255, 200, 50, 0.18);
  color: #ffd95a;
  border: 1px solid rgba(255, 200, 50, 0.45);
}
/* The chip-as-link variant — kills <a> default underline + inherits
   the gold pill above. Hover/focus bump nudges the chip toward the
   user without recoloring it (the gold tint is already the brand). */
a.sr-banger-cost-link {
  text-decoration: none;
  cursor: pointer;
}
a.sr-banger-cost-link:hover,
a.sr-banger-cost-link:focus-visible {
  background: rgba(255, 200, 50, 0.32);
  border-color: rgba(255, 200, 50, 0.7);
  outline: none;
}
.sr-banger-result {
  margin-top: 14px;
  padding: 12px;
  border: 1px dashed rgba(255, 200, 50, 0.35);
  border-radius: 6px;
  background: rgba(255, 200, 50, 0.04);
}
.sr-banger-result-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 8px;
}
.sr-banger-result-tag {
  font-weight: 700;
  letter-spacing: 0.04em;
  color: #ffd95a;
}
.sr-banger-result-inner img {
  display: block;
}

/* ── Pop-out mode ──────────────────────────────────────────────────────
   When marketer.html is opened with ?popout=1&panel=<id>, we set
   body[data-popout="1"] data-panel="<id>" and hide everything except the
   matching .sr-monitor. That single monitor stretches to fill the
   viewport so the user can drag the window onto a second physical screen
   without seeing chrome from the rest of the wall. */
body[data-popout="1"] {
  background: #050608;
}
body[data-popout="1"] .site-header,
body[data-popout="1"] .phase-hero,
body[data-popout="1"] .footer-tag,
/* X139-2026-05-14: `.sr-status-strip` selector retired (the strip
   is gone). Adding `.sr-genre-bar` here so popout panels still
   ship the wall-only frame Captain wants — the genre filter row
   plus its LIVE chip would be noise in a single-panel popout. */
body[data-popout="1"] .sr-genre-bar,
body[data-popout="1"] .row.logo-row,
body[data-popout="1"] .logo,
body[data-popout="1"] .sr-ticker {
  display: none !important;
}
body[data-popout="1"] #mainContent {
  padding: 0;
  max-width: none;
}
body[data-popout="1"] .sr-room {
  padding: 6px;
  min-height: 100vh;
  border: none;
  box-shadow: none;
  background: #050608;
}
body[data-popout="1"] .sr-grid {
  grid-template-columns: 1fr;
  gap: 0;
}
body[data-popout="1"] .sr-grid > .sr-monitor { display: none !important; }
body[data-popout="1"] .sr-ticker             { display: none !important; }
body[data-popout="1"][data-panel="board"]    #boardCard:not([hidden])      { display: flex !important; }
body[data-popout="1"][data-panel="reactor"]  #reactorCard:not([hidden])    { display: flex !important; }
body[data-popout="1"][data-panel="mainsituation"] #mainSituationCard:not([hidden])   { display: flex !important; }
body[data-popout="1"][data-panel="result"]   #resultCard:not([hidden])     { display: flex !important; }
body[data-popout="1"][data-panel="history"]  #historyCard:not([hidden])    { display: flex !important; }
body[data-popout="1"][data-panel="history"]  #recentDropsCard:not([hidden]){ display: flex !important; }
body[data-popout="1"] .sr-monitor {
  min-height: calc(100vh - 16px);
  border-radius: 0;
  grid-column: 1 / -1 !important;
}

/* In popout, swap the popout button for a "← Re-dock" link that just
   navigates back to the parent /marketer.html. */
body[data-popout="1"] .sr-popout-btn::after { content: " ← dock"; }

/* ── Mobile gentle-down ────────────────────────────────────────────────
   On phones the bezels visually overpower the screen content; soften the
   shadow and shrink the head so each panel is mostly its content. */
@media (max-width: 720px) {
  .sr-room {
    padding: 10px 8px 18px;
    border-radius: 4px;
  }
  .sr-monitor {
    box-shadow: inset 0 1px 0 #4a4f58, 0 1px 3px rgba(0,0,0,0.5);
  }
  .sr-monitor-head { font-size: 9px; padding: 3px 6px; }
  .sr-screen { padding: 8px; }
  /* X162-2026-05-15: meme rail was eating most of the mobile viewport —
     a 360px sticky-from-base panel with a 2-column tile grid (`120px`
     tiles × 2 cols = the "two lines of memes" Captain reported) was
     pushing the wall of monitors below the fold. Halved the cap and
     reverted to a single-column tile strip so the rail reads as a
     compact horizontal slot the user scrolls inside, not a second
     full-screen surface fighting the wall for space. */
  .meme-strip {
    position: static;
    max-height: 220px;
    min-height: 0;
  }
  .meme-strip-scroll {
    grid-template-columns: 1fr;
  }
  /* X162-2026-05-15: genre bar collapses from 3+ wrap-lines of pills
     (+ a 4th line for the LIVE lockup pinned right via the desktop
     `margin-left: auto` rule) into a single horizontally-scrollable
     strip on phones. With 12 STANDARD_TOP_SLUGS pills at desktop
     sizing the wrap-flow produced "3 lines of Select the category"
     + "live monitoring the situation" on its own line — Captain
     reported the genre bar alone was eating ~120px of vertical
     viewport before the wall could even start rendering. Now: one
     line of pills, swipe-right to reach the LIVE lockup at the end
     of the scroll. */
  .sr-genre-bar {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    padding: 6px 8px;
    gap: 4px;
    scrollbar-width: thin;
    -webkit-overflow-scrolling: touch;
  }
  .sr-genre-bar > * {
    /* Belt-and-braces against any flex-shrink on the live lockup or
       a future child making the row collapse instead of scrolling. */
    flex: 0 0 auto;
  }
  .sr-genre-bar .sr-genre-pill {
    font-size: 9px;
    padding: 4px 8px;
    letter-spacing: 0.06em;
  }
}

/* The .container > .card max-width: 660px guard (style-guard test) doesn't
   apply to .sr-room because the room replaces the card stack entirely.
   We don't use .card inside the room — every panel is a .sr-monitor. The
   .container > .card cap continues to govern the rest of the site. */

/* ── No flicker inside the wall (defense in depth) ──────────────────────
   Captain pulled all flicker / pulse from the Situation Room. The
   sr-monitor-led + sr-status-live::before rules above already declare
   no animation, but if a future edit reintroduces .mw-chyron, .logo,
   or any other chyron primitive inside the room (whether through a
   shared widget, a child of .sr-screen, or markup inherited from
   elsewhere), the site-wide mwAmbientPulse / neonPulse keyframes
   would attach and start pulsing again. This rule cuts that off
   unconditionally for anything inside the room. The .sr-ticker
   marquee is a transform-based scroll, not an opacity animation, and
   doesn't match these selectors — it keeps running. */
.sr-room .mw-chyron::before,
.sr-room .logo::before,
.sr-room [class*="-pulse"],
.sr-room [class*="-blink"] {
  animation: none !important;
  opacity: 0.85;
}

/* ============================================================================
   X80-2026-05-08 — SITUATION WALL LAYOUT
   ----------------------------------------------------------------------------
   New 7-monitor wall for marketer.html. Replaces the .sr-grid 12-column
   flow above (which lives on for any future page that wants the linear
   card grid). The wall has a left infinite-meme rail + a 4-col × 3-row
   grid of monitors with the primary spanning 2×2 in the center.

   Layout:
     +------+------+------+------+
     |  TL  |  TL  |  TR  |  TR  |   row 1: 2 wide top monitors
     +------+------+------+------+
     |  ML  |               |  MR  |
     +------+   PRIMARY     +------+
     |  BL  |   (2x2)       |  BR  |
     +------+------+------+------+

   Side monitors are clickable (.sr-side) — click promotes that
   situation into the primary via pickSituation(). The .sr-monitor
   primitive (above) handles the bezel; this section just composes the
   layout and the click affordance.

   Captain 2026-05-08: "It's meant to be a tool for marketers to monitor
   all current world situations, and quickly make memes about them."
   Visual reference: GPT-rendered "trading-desk wall" preliminary asset
   shared in chat — 6 stacked sides flanking a big center. The CSS
   here doesn't depend on the eventual asset; if Captain ships a
   backdrop PNG, drop it on .sr-wall-grid as background-image and tune
   each grid-area to overlay the asset's screen rectangles. */

.sr-wall {
  display: grid;
  /* X96-2026-05-08 — Captain: "can we have the meme feed be on the
     right-hand side of the monitors, not the left-hand side?" Flipped
     grid-template-columns so the wide wall column comes first and the
     220px meme rail comes second. The DOM order is unchanged (rail
     still appears first in markup); the per-child `grid-column` pins
     below place each item in the right column regardless of DOM order
     so the mobile fallback's `order: 99` convention on the rail still
     works without re-shuffling the markup on the host or paid pages. */
  grid-template-columns: 1fr 220px;
  gap: 14px;
  /* X112-2026-05-10 (initial) + X117-2026-05-10 (this thread) — Captain:
     "the meme selector and the monitor cluster HAVE to be on the
     screen at the same time. Maybe it's a container issue?" Yes —
     `align-items: start` (the X96 default) let the rail and the
     wall-grid each shrink to their OWN min-heights so the rail came
     up short and read as a tiny empty column next to a much taller
     wall. The X112 first pass added `align-items: stretch` and
     `max-height: calc(100vh - 220px)` but that left two cracks:
       (a) `max-height` does NOT force children to shrink — the
           wall-grid's `min-height: 520px` still won at short viewports,
           so the wall overflowed BELOW the visible page and the meme
           rail rendered alongside an off-screen wall-bottom that
           pushed the visible monitor row down to where it appeared
           orphaned at the bottom of the viewport with empty space
           above.
       (b) Without `overflow: hidden` on `.sr-wall`, the overflow was
           visible (and confusing) rather than hidden / clipped.
     X117 swaps `max-height` → a definite `height` derived from the
     viewport, caps it at 640px on tall screens (no need for monitor
     rows to grow past their natural readable size), floors at 360px
     on short screens (fits 3 rows × ~110px monitors), and adds
     `overflow: hidden` so the wall-grid and the meme rail are FORCED
     to fit inside the cap. The wall-grid's own `min-height` drops to
     0 (below) so the children share the parent's definite height
     instead of fighting it. Internal scroll continues to live on
     `.meme-strip-scroll` (rail) and per-monitor `.sr-screen { overflow:
     auto }`, so anything that doesn't fit inside the cap stays
     reachable via scroll within its own container. */
  align-items: stretch;
  /* X139-2026-05-14: cap bumped 640 → 680 and viewport offset
     200 → 160 so the wall grows into the ~40px freed when Captain
     retired `.sr-status-strip` from the top of /marketer.html
     (see same-dated comments in marketer.html + the
     .sr-status-strip removal banner above). Captain: "We can make
     this situation a touch larger to fill the screen perfectly."
     Numbers chosen to match the actual breakdown — the retired
     strip was padding 6+6 + ~12px content + 14px bottom margin +
     ~2px border ≈ 40px — so subtracting 40 from the 200 offset
     gives the wall the same fraction of the viewport it had before
     plus the strip's share. The cap bump 640 → 680 lets tall
     viewports (~1080px) actually realize the gain instead of
     bumping into the prior cap; X117's "no need for monitor rows
     to grow past their natural readable size" rationale still
     holds at 680 — it's a single monitor-row's worth taller, not
     a full new row. */
  height: min(680px, calc(100vh - 160px));
  min-height: 360px;
  overflow: hidden;
}

/* Right meme rail (X96-2026-05-08 — moved from left to right per
   Captain). Static column inside .sr-wall. The .meme-strip rules
   above (sticky, internal scroll) still apply to the same #memeStrip
   element; here we override `position` so the rail flows in the grid
   rather than sticking to the viewport. `grid-column: 2` pins the
   rail to the second (220px) column independent of DOM order.

   X112-2026-05-10: dropped the rail's own `min-height: 480px` /
   `max-height: calc(100vh - 220px)` constraints — those were what
   made the rail visually shorter than the wall before X112. With
   `.sr-wall { align-items: stretch }` the rail now matches the
   wall-grid's height automatically, and the wall-level viewport cap
   handles the "fits on screen" guarantee. `min-height: 0` lets it
   actually shrink to the row height instead of forcing 480px. */
.sr-meme-rail {
  position: static;
  /* X119-2026-05-10: BOTH `grid-column` AND `grid-row: 1` are required.
     With only `grid-column: 2` (and the wall-grid pinned to `grid-column: 1`)
     and no explicit row on EITHER child, the CSS Grid sparse auto-placement
     algorithm walks DOM order: rail (DOM index 0) lands at row 1 col 2,
     and the wall-grid (DOM index 1) gets pushed to row 2 col 1 because the
     auto-flow cursor doesn't backtrack to fill row 1 col 1. Verified live
     via Chrome MCP: rail at y=156-398, wall-grid at y=412-637 — two rows,
     not the side-by-side layout the column pin alone implies. Pinning both
     children to `grid-row: 1` forces them into the same row. (`grid-auto-flow:
     dense` on `.sr-wall` would also work, but explicit row pinning is
     more readable + matches how the per-monitor `.sr-side-tl { grid-area }`
     pins below already work.) */
  grid-column: 2;
  grid-row: 1;
  min-height: 0;
  /* X120-2026-05-10: explicit `max-height: none` defeats the inherited
     `.meme-strip { max-height: 70vh }` rule, which still applies to
     this same #memeStrip element (it carries both `.sr-meme-rail` and
     `.meme-strip` classes). Pre-X120, on viewports ~800-915px tall the
     70vh cap was tighter than the wall's `min(640px, 100vh-200px)`
     cap, so the rail ended up 50-100px shorter than the wall —
     visibly mismatched even after the X117/X119 fixes. With max-height
     none here the rail's height is bound only by the parent
     `.sr-wall`'s definite `height` and the grid's `align-items:
     stretch`, so they finally match on EVERY viewport size. The
     `min-width: 0` is the standard grid-item escape hatch — without
     it the implicit `min-width: auto` lets long meme tile names
     bulge the rail past its 220px column. */
  max-height: none;
  min-width: 0;
  background: linear-gradient(180deg, #2a2f38 0%, #1a1d22 100%);
  border: 1px solid #000;
  border-radius: 6px;
  padding: 8px;
  overflow: hidden;  /* internal .meme-strip-scroll handles scroll */
  display: flex;
  flex-direction: column;
}

.sr-wall-grid {
  display: grid;
  /* X96-2026-05-08: pin to column 1 of the parent .sr-wall so the wall
     of monitors stays on the left and the meme rail (column 2) moves
     to the right per Captain's request.
     X119-2026-05-10: also pin `grid-row: 1` so the wall-grid sits in the
     same row as the rail. See same-dated comment on `.sr-meme-rail` for
     the auto-placement quirk that pushed the wall-grid to row 2. */
  grid-column: 1;
  grid-row: 1;
  grid-template-columns: 1fr 1fr 1fr 1fr;
  grid-template-rows: 1fr 1fr 1fr;
  /* X138-2026-05-14: TC (top-center) slot removed. Captain on
     screenshot: "Remove that top situation so it'll be six on the
     side, six on the side, and one in the middle ... If we move it
     to only one in the middle, then we have the option to actually
     see the whole meme when we are making it." The TC monitor used
     to span the middle two columns of row 1; PR now extends up to
     occupy that band, giving the active reactor (meme preview +
     caption stage + render buttons) the full vertical height of
     the wall to breathe in. TL and TR still each take one column
     so the left and right stacks remain visually anchored at the
     top. The X80b "7 sides + 1 primary" arrangement is retired.
     Earlier history: X80b (2026-05-08) added TC; X80 set up the
     four-column grid in the first place. */
  grid-template-areas:
    "tl pr pr tr"
    "ml pr pr mr"
    "bl pr pr br";
  gap: 12px;
  /* X112-2026-05-10: min-height 640 → 520 so the wall fits more
     comfortably alongside the meme rail inside the viewport cap on
     `.sr-wall`.
     X117-2026-05-10: min-height 520 → 0 now that `.sr-wall` carries
     a definite `height` (see same-dated X117 comment above). Letting
     the wall-grid inherit its parent's height means `1fr 1fr 1fr`
     row tracks split that height evenly, monitors stretch to fill
     each row, and the wall-grid + meme rail end up exactly the same
     height. Pre-X117 a `min-height: 520px` here fought the parent's
     `max-height` and won, pushing the wall to overflow below the
     visible viewport and orphaning the top monitor row at the
     bottom of the screenshot Captain shipped. */
  min-height: 0;
  /* Belt-and-braces: prevent monitors from forcing the wall-grid
     taller than the parent's `height` cap if any single .sr-screen's
     content is genuinely huge. Per-screen `overflow: auto` already
     handles in-monitor scrolling; this hides any residual horizontal
     gap-edge bleed. */
  overflow: hidden;
}

.sr-side-tl { grid-area: tl; }
/* X138-2026-05-14: .sr-side-tc retired — TC area is gone from the
   grid; the TC monitor element was removed from marketer.html in
   the same pass. Rule kept commented as a tombstone so a future
   thread doesn't accidentally re-introduce the slot.
   .sr-side-tc { grid-area: tc; } */
.sr-side-tr { grid-area: tr; }
.sr-side-ml { grid-area: ml; }
.sr-side-mr { grid-area: mr; }
.sr-side-bl { grid-area: bl; }
.sr-side-br { grid-area: br; }
.sr-primary { grid-area: pr; }

/* Side monitors — clickable. Cursor pointer, hover lift, focus ring,
   selected-state glow when promoted to primary. */
.sr-side {
  cursor: pointer;
  transition: transform 0.12s, box-shadow 0.12s;
  user-select: none;
}
.sr-side:hover {
  transform: translateY(-2px);
  box-shadow:
    inset 0 1px 0 #4a4f58,
    inset 0 -1px 0 #050608,
    0 4px 12px rgba(0, 0, 0, 0.7),
    0 0 32px rgba(102, 230, 255, 0.12);
}
.sr-side:focus-visible {
  outline: 2px solid var(--sr-cyan);
  outline-offset: 2px;
}
.sr-side[aria-selected="true"] {
  /* Currently-promoted-to-primary feedback. */
  box-shadow:
    inset 0 0 0 1px var(--sr-cyan),
    inset 0 1px 0 #4a4f58,
    0 0 32px rgba(102, 230, 255, 0.22);
}
.sr-side .sr-side-text {
  margin: 0;
  font-size: 0.9rem;
  line-height: 1.35;
  color: #fff;
  /* Clamp to ~4 lines so each side cell stays visually tidy regardless
     of underlying situation length. */
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* X80d (2026-05-08): two-tier chyron — account name BIG, platform
   name small + clickable to source URL. Captain: "the source should be
   small and clickable to go to the source (the original X post, etc).
   With the name of the account source bigger than the source site (X,
   Reddit, etc)." Override the .sr-monitor-head cell layout so the
   .sr-side-cat row grows to host both children inline.

   The `.sr-side-cat` span sits inside .sr-monitor-head's flex row; we
   make it itself a flex row so the account text can shrink-with-ellipsis
   while the platform-name link stays at its intrinsic size on the right.
   The flex: 1 + min-width: 0 combo lets the account text actually
   ellipsis instead of overflowing — long handles like "@PowerfulJRE"
   would otherwise push the link off the row. */
.sr-side .sr-side-cat {
  display: flex;
  align-items: baseline;
  gap: 6px;
  flex: 1;
  min-width: 0;
  overflow: hidden;
}
/* X80e (2026-05-08): typography toned down per Captain — the chyron
   reads as a quiet metadata strip, not a header. Account small, site
   source even smaller. The situation title in the body is the focal
   point of each side monitor. */
.sr-side-account {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: none;          /* @handles read better mixed-case */
  color: var(--sr-cyan);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sr-side-source,
.sr-side-account-link {
  flex: 0 0 auto;
  font-size: 8px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--sr-cyan);
  opacity: 0.6;
  text-decoration: none;
  /* Tiny clickable target — bump padding for fingertip reach without
     blowing up the chyron row height. */
  padding: 0 3px;
  border: 1px solid transparent;
  border-radius: 2px;
  transition: opacity 0.12s, border-color 0.12s, background 0.12s;
}
.sr-side-source:hover,
.sr-side-account-link:hover {
  opacity: 1;
  border-color: var(--sr-cyan-dark);
  background: rgba(102, 230, 255, 0.08);
}
.sr-side-source:focus-visible,
.sr-side-account-link:focus-visible {
  outline: 1px dotted var(--sr-cyan);
  outline-offset: 1px;
  opacity: 1;
}
/* When the platform name doubles as the chyron account (HN /
   memelord-feed rows with no per-account handle in the URL), match
   the account size — still small, just slightly heavier weight than
   the auxiliary source link. */
.sr-side-account-link {
  font-size: 10px;
  font-weight: 600;
  opacity: 0.85;
}

/* Primary visual emphasis — slightly thicker bezel + faint warm rim
   so it reads as the focal monitor at a glance even before content. */
.sr-primary {
  box-shadow:
    inset 0 1px 0 #5a5f68,
    inset 0 -1px 0 #050608,
    inset 1px 0 0 #4a4f58,
    inset -1px 0 0 #15181c,
    0 4px 12px rgba(0, 0, 0, 0.75),
    0 0 48px rgba(196, 30, 30, 0.08);
}
.sr-primary .sr-screen { font-size: 1rem; }

/* Mobile fallback. The wall is a desktop-tool surface; phones get a
   single-column stack with primary first, sides below in DOM order,
   meme rail at the bottom. Phones aren't the target use case here,
   but the page shouldn't break. */
@media (max-width: 920px) {
  .sr-wall {
    grid-template-columns: 1fr;
    /* X162-2026-05-15: release the desktop viewport-cap
       `height: min(680px, calc(100vh - 160px))` + `overflow: hidden`
       on phones. The cap exists so the desktop wall stays alongside
       the meme rail inside one screenful; once children stack
       vertically the cap is actively harmful — 7 monitors at ~120px
       each + the rail need ~900px to render, but the cap squeezes
       them into ~640px with the overflow clipped to nothing. The
       visible symptom was "no situation monitor" on mobile — the
       wall existed but was hidden below an invisible fold inside an
       overflow:hidden container with no way to reach it. */
    height: auto;
    min-height: 0;
    overflow: visible;
  }
  .sr-meme-rail {
    /* X96-2026-05-08: clear the desktop `grid-column: 2` pin so the
       rail places into the single mobile column. `order: 99` still
       pushes it below the wall on phones.
       X162-2026-05-15: ALSO release the desktop `grid-row: 1` pin.
       With both children pinned to row 1 and only one explicit
       column, CSS Grid was creating an implicit column 2 for
       whichever child got placed second, so the rail ended up
       side-by-side with the wall-grid in a 380px viewport instead
       of stacked below it. `order: 99` only sorts auto-placed items;
       an explicit `grid-row` trumps it. Releasing to `auto` lets
       the order + DOM placement actually take effect. */
    grid-column: auto;
    grid-row: auto;
    order: 99;       /* meme rail moves below the wall on phones */
    max-height: 480px;
    min-height: 0;
  }
  .sr-wall-grid {
    /* Same: clear the desktop `grid-column: 1` pin for mobile.
       X162-2026-05-15: also release `grid-row: 1` — see same-dated
       comment on `.sr-meme-rail` above for the implicit-column-2
       bug this fixes. */
    grid-column: auto;
    grid-row: auto;
    grid-template-columns: 1fr;
    grid-template-rows: auto;
    /* X138-2026-05-14: TC slot removed (see same-dated comment on
       the desktop grid-template-areas above). */
    grid-template-areas:
      "pr"
      "tl"
      "tr"
      "ml"
      "mr"
      "bl"
      "br";
    min-height: 0;
  }
}

/* Legacy hidden-monitor shells (kept in DOM for JS show()/hide()/by-id
   lookups; never visible). The `hidden` attribute already handles this
   for most browsers; the !important here is belt-and-braces in case a
   future stylesheet sets display:block on a generic [hidden] override. */
.sr-legacy { display: none !important; }

/* ─────────────────────────────────────────────────────────────────────
   X12-2026-05-08 — LOCKED WALL VARIANT (free tier marketermode.html)
   ─────────────────────────────────────────────────────────────────────
   marketermode.html mirrors the paid /marketer.html wall (X80) but
   greys out the 6 side monitors and the meme rail. The primary
   monitor stays interactive and hosts the existing solo-studio
   practice flow (#soloSetupCard → #soloStage). Captain spec:
     "The unpaid version should still have the same 7 window monitor
     and the same exact setup as the paid version, just with all but
     the main monitor greyed out, and a very minimal statement that
     The Situation Room and up to the minute meme templates are
     unlockable in paid mode."
   The .sr-side-locked class is the lock treatment: greyscale +
   reduced opacity + cursor:not-allowed + no hover lift. The lock
   icon overlay sits on the bottom-right of each side monitor as a
   small 🔒 chip. The .sr-paywall-statement at the bottom is the
   minimal upsell line. */

/* Wrapper modifier: anchors the locked side rules so they don't
   accidentally apply to the paid /marketer.html wall (which doesn't
   carry .sr-wall-locked on its outer wrapper). */
.sr-wall-locked .sr-side-locked,
.sr-side-locked {
  /* Visual: greyscale + dim. Filters compose so a future hover-disabled
     rule can layer without resetting these. */
  filter: grayscale(0.85) brightness(0.65);
  opacity: 0.78;
  cursor: not-allowed;
  /* Override the paid wall's clickable-side defaults — no hover lift,
     no green focus rim, no aria-selected glow. */
  pointer-events: none;
}
.sr-side-locked:hover,
.sr-side-locked:focus,
.sr-side-locked:focus-visible {
  transform: none !important;
  box-shadow: inherit !important;
  border-color: inherit !important;
}
.sr-side-locked .sr-monitor-led {
  /* LED on locked monitors stays dim red — "monitoring is paused" -ish.
     Paid wall's per-row LED is more saturated; this dulls it. */
  filter: saturate(0.4) brightness(0.7);
}
.sr-side-locked .sr-side-screen {
  /* Slightly lower contrast on the screen text so the placeholder
     copy doesn't compete with the active primary monitor. */
  opacity: 0.85;
}

/* Lock icon overlay anchored to the side monitor's lower-right corner.
   The .sr-monitor wraps already have position:relative (set up in the
   X80 base rule), so absolute positioning here is grounded. */
.sr-side-lock-overlay {
  position: absolute;
  bottom: 8px;
  right: 8px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(255, 200, 60, 0.92);
  color: #1a1300;
  font-size: 13px;
  line-height: 22px;
  text-align: center;
  /* Lift above the .sr-screen content + the greyscale filter — the
     filter applies to the whole .sr-monitor; adding `filter: none`
     here would re-color the lock icon to its true gold tint, but
     that pulls the overlay out of the locked-feeling treatment.
     Leaving it under the filter so the lock reads as part of the
     same dimmed beat. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4),
              0 1px 2px rgba(0, 0, 0, 0.45);
  pointer-events: none;
}

/* Locked meme rail (X12 ships the locked treatment; X96 moved the
   rail from the left side of the wall to the right per Captain).
   The X80 .sr-meme-rail base rule provides
   the layout; this just dims the rail when it carries
   .meme-strip-locked (which the X12 markup does on this surface). */
.sr-wall-locked .sr-meme-rail.meme-strip-locked {
  filter: grayscale(0.5);
  /* Don't disable pointer-events on the rail itself — the locked
     overlay's upsell-chip <a> needs to stay clickable. The
     .meme-strip-locked-overlay rules in 06-submissions-vote.css /
     14-auth-pages.css already pin the chip's clickability. */
}

/* Minimal upsell statement under the wall. Single line, single CTA.
   Captain spec: "very minimal statement that The Situation Room and
   up to the minute meme templates are unlockable in paid mode." */
.sr-paywall-statement {
  margin: 14px auto 4px;
  max-width: 660px;
  text-align: center;
  font-family: var(--f-pixel);
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  color: var(--ml-navy);
  opacity: 0.88;
  line-height: 1.5;
}
.sr-paywall-statement a {
  color: var(--sr-cyan-dark);
  font-weight: 700;
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  padding-bottom: 1px;
}
.sr-paywall-statement a:hover,
.sr-paywall-statement a:focus-visible {
  color: var(--sr-cyan-mid);
}

/* ── X127-2026-05-12: locked-tier wall treatment ─────────────────────
   Captain X127 consolidated /marketer.html + /marketermode.html into a
   single page. The body now carries `.is-locked-tier` when the visitor
   isn't paid (anon, free user, or expired sub). Spec: "block it off or
   have a lock icon and stuff grayed out. You can see exactly what it
   is, but everything's grayed out … I want people to be able to see
   what they're missing out on and just not be able to click on it."

   Pre-X127 the same lock state used `.sr-room.is-paywalled` + a giant
   overlay card. The new treatment is intentionally lighter — soft
   grayscale that lets the wall stay legible, plus per-element lock
   icons + pointer-events:none on the side monitors so the visitor
   sees the wall populated with real data (via free-feed fallback) but
   can't interact with anything paid-gated. The action row's Save /
   Post / AI Edit / Banger buttons are replaced at render time by
   .upsell-chip <a>s, which stay clickable. */
body.is-locked-tier .sr-room {
  filter: grayscale(0.45) brightness(0.92);
}
/* Side monitors: read-only feel + small lock dot top-right. */
body.is-locked-tier .sr-side {
  cursor: not-allowed;
}
body.is-locked-tier .sr-side::after {
  content: '🔒';
  position: absolute;
  top: 6px;
  right: 6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: rgba(255, 200, 60, 0.85);
  color: #1a1300;
  font-size: 10px;
  line-height: 18px;
  text-align: center;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
  pointer-events: none;
  z-index: 2;
}
/* Primary monitor stays interactive enough to read the active
   headline but the inner action row carries its own locked state. */
body.is-locked-tier .sr-primary {
  filter: none; /* primary is the upsell focal point — keep it crisp */
}
/* Meme rail on the locked tier: it's already a `.meme-strip-locked`
   surface on the free flow; here we just make sure the dim doesn't
   compound when combined with the body-level filter. */
body.is-locked-tier .sr-meme-rail {
  filter: grayscale(0.2);
}
/* The legacy `.sr-paywall-overlay` X101 card is now redundant —
   `setLockedTier()` keeps it hidden in JS, but belt-and-suspenders
   in CSS in case a future thread re-enables it. */
body.is-locked-tier .sr-paywall-overlay {
  display: none !important;
}

/* ── X101-2026-05-08: PAYWALL OVERLAY ─────────────────────────────────
   /marketer.html used to gate non-paid users behind two `.card` blocks
   (#authGate / #subGate) shown ABOVE the wall — non-paid users never
   saw the Situation Wall layout. Captain X101: "It should always 100%
   be situation room, but sometimes the screen should be grayed out if
   you're not paid." So now the wall always renders and #paywallOverlay
   sits absolutely-positioned over it, covering it with a semi-opaque
   backdrop + a centered subscribe / log-in card. The wall behind goes
   greyscale + dim via `.sr-room.is-paywalled`.
   - The overlay sits at z-index 5 (above .sr-room > * which is at 1).
   - `pointer-events: auto` on the card itself so the buttons are
     clickable; the backdrop swallows any wall clicks behind. */
/* X139-2026-05-14: `.sr-status-strip` selector retired; grayscale dim
   now applies to the genre+LIVE bar instead so the paywalled wash
   covers the LIVE chip in the same pass it covers the wall + ticker. */
.sr-room.is-paywalled .sr-genre-bar,
.sr-room.is-paywalled .sr-wall,
.sr-room.is-paywalled .sr-ticker,
.sr-room.is-paywalled .sr-legacy {
  filter: grayscale(0.85) brightness(0.55);
  pointer-events: none;
  user-select: none;
}
/* `display: flex` below defeats the user-agent's `[hidden] { display: none }`
   default — paid users would see an empty `.sr-paywall-card` shell because
   `hidden` is set on the wrapper at boot but ignored by the explicit
   display rule. Explicit reset below restores the expected behaviour. */
.sr-paywall-overlay[hidden] { display: none; }
.sr-paywall-overlay {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(
    ellipse at center,
    rgba(5, 6, 8, 0.45) 0%,
    rgba(5, 6, 8, 0.7) 100%
  );
  /* Soft backdrop-blur on the wall behind the card so the dim is felt
     even if a browser doesn't honor the grayscale filter (Safari < 14). */
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  /* Tile the chrome scanlines onto the overlay too so it reads as
     part of the same CRT room, not a modal over the room. */
  pointer-events: auto;
}
.sr-paywall-card {
  background: linear-gradient(180deg, #f4f7fb 0%, #d9e3ef 100%);
  color: #0d1219;
  border: 2px solid #02007F;
  border-radius: 8px;
  padding: 28px 36px;
  max-width: 440px;
  width: calc(100% - 48px);
  text-align: center;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.85),
    0 12px 40px rgba(0, 0, 0, 0.45);
  font-family: var(--f-body);
}
.sr-paywall-card h2 {
  margin: 0 0 12px;
  font-family: var(--f-pixel);
  font-size: 1.15rem;
  letter-spacing: 0.04em;
  color: var(--ml-navy);
}
.sr-paywall-card p {
  margin: 8px 0;
  font-size: 0.95rem;
  line-height: 1.45;
}
.sr-paywall-card .menu-btn {
  margin: 4px 6px 0;
}
.sr-paywall-card .small.muted {
  margin-top: 14px;
  font-size: 0.78rem;
  opacity: 0.72;
}

/* ── X102-2026-05-08 — HUD palette overrides for shared mw-* primitives
      INSIDE the situation room ─────────────────────────────────────────
   The .mw-crt scanline overlay and .mw-chyron strip live in
   18-monitoring-layer.css and are shared across host / player /
   spectator / marketer pages. They ship with the green-phosphor
   palette by default. The accent-color swap above is scoped to
   .sr-* classes; the mw-* primitives inherit through the cascade.
   These rules retint them inside .sr-room only — every other page
   keeps the green-phosphor look. */
.sr-room .mw-crt::after {
  background:
    repeating-linear-gradient(
      to bottom,
      rgba(102, 230, 255, 0.055) 0,
      rgba(102, 230, 255, 0.055) 1px,
      transparent 1px,
      transparent 3px
    ),
    radial-gradient(
      ellipse at center,
      transparent 55%,
      rgba(0, 0, 0, 0.18) 100%
    );
}
.sr-room .mw-chyron {
  background: var(--sr-panel-bg);
  color: var(--sr-cyan);
  border-top-color: var(--sr-cyan-dark);
  box-shadow: inset 0 1px 0 rgba(102, 230, 255, 0.25);
}
.sr-room .mw-chyron::before {
  background: var(--sr-cyan);
  box-shadow: 0 0 3px rgba(102, 230, 255, 0.5);
}
.sr-room .mw-chyron.mw-chyron-muted {
  background: var(--sr-panel-bg-2);
  color: var(--sr-cyan-mid);
}

/* ── X140-2026-05-14: tweet-card body for side monitors ───────────────
   Captain X140: "we need to make the situation monitoring room look
   like tweets … they need to load tweets instead of what it currently
   is, just kind of a list of green text." Each `.sr-side-text`'s
   contents are now a tweet card (avatar circle + name/handle stack +
   body text + faux engagement bar) rather than the pre-X140 green
   VT323 textContent paint. The monitor's outer chrome (dark CRT
   frame, scanlines, chyron header) is unchanged — only the inside
   of the screen changes.

   Scoped to `.sr-side-screen` so the primary monitor + any future
   non-side rendering sites aren't affected. The cyan-on-black
   `.mw-crt` glow that the screen still carries on its wrapper
   doesn't apply to the tweet card text because the card defines its
   own color values explicitly. */
.sr-side-screen {
  padding: 8px 10px;
}
/* X145-2026-05-14: rebuilt to look like an actual Twitter / X post.
   Captain on screenshot: "this doesn't look like Twitter at all.
   Let's go ahead and fix that." Coordinated rewrite alongside the
   renderSideTweetCard innerHTML change in marketer.html (X145):
   - Card envelope: dropped the rounded inner-panel background and
     border. Twitter posts in a feed sit bare on the background with
     a horizontal divider between them, not boxed.
   - Avatar bumped 28 → 36px to match Twitter's prominent identity
     puck.
   - Header row: name + verified badge + handle + bullet + time all
     inline on one row (flex, nowrap with ellipsis on the flexible
     children), Twitter's standard layout. Pre-X145 was two-row
     name-then-handle stack which read more like an old Mastodon
     post than a tweet.
   - Colors: Twitter's actual #e7e9ea (primary text) and #71767b
     (muted) replace the rgba(255,255,255,0.x) approximations.
   - Icons: SVGs sized 14px with currentColor fill so they read as
     real Twitter glyphs instead of emoji. Verified badge ships its
     own #1d9bf0 fill (Twitter blue) — do NOT recolor it.
   - Action row: 4 columns (reply / retweet / like / views) spread
     across the width with space-between so the bar reads as a
     proper engagement strip instead of a tight cluster. */
:where(.sr-side-screen, #activeSituationText) .sr-tweet {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 4px 0;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-empty {
  opacity: 0.5;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-avatar {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--f-body);
  font-size: 15px;
  font-weight: 700;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
  background: hsl(200, 30%, 35%);   /* overridden by inline style */
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-avatar-empty {
  background: rgba(255, 255, 255, 0.1);
  color: var(--sr-cyan-mid);
  font-size: 18px;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-stack {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-id {
  display: flex;
  gap: 4px;
  align-items: center;
  flex-wrap: nowrap;
  line-height: 1.2;
  overflow: hidden;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-name {
  font-family: var(--f-body);
  font-size: 13px;
  font-weight: 700;
  color: #e7e9ea;                      /* Twitter primary text */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 0 1 auto;
  min-width: 0;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-verified {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  display: inline-block;
  vertical-align: -3px;                /* baseline-tweak to match X */
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-handle,
:where(.sr-side-screen, #activeSituationText) .sr-tweet-dot,
:where(.sr-side-screen, #activeSituationText) .sr-tweet-time {
  font-family: var(--f-body);
  font-size: 12px;
  font-weight: 400;
  color: #71767b;                      /* Twitter muted text */
  white-space: nowrap;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-handle {
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 0 1 auto;
  min-width: 0;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-dot,
:where(.sr-side-screen, #activeSituationText) .sr-tweet-time {
  flex: 0 0 auto;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-body {
  font-family: var(--f-body);
  font-size: 13px;
  line-height: 1.4;
  color: #e7e9ea;
  /* X150-2026-05-14: removed the 4-line clamp. Captain on screenshot:
     "we can't see most of them." Pre-X150 the body clamped at 4 lines
     with an ellipsis AND `.sr-screen` (the cell parent) has
     `overflow: auto`, so the clamp truncated mid-word in the body
     AND pushed the action row (reply / retweet / like / views) below
     the cell's visible area — readers got header + 4 garbled lines +
     nothing else, no clear "this is a tweet" signal. Without the
     clamp, the body renders full-length; short tweets fit cleanly and
     long ones extend the card. The cell's existing `overflow: auto`
     handles overflow when a long tweet pushes total height past the
     cell — preferable to the truncated-and-hidden-action-row state,
     because the reader can scroll within the cell to see the rest of
     the tweet AND the action bar always sits at the natural end. */
  word-break: break-word;
  margin-top: 2px;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 6px;
  font-family: var(--f-body);
  font-size: 11px;
  color: #71767b;                      /* Twitter muted */
  margin-top: 6px;
  max-width: 340px;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-act {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: inherit;
  /* Cursor stays default — these are faux affordances. The whole
     monitor is the real click target (promote-to-primary). */
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-icon {
  width: 14px;
  height: 14px;
  display: inline-block;
  fill: currentColor;
  flex: 0 0 auto;
}
:where(.sr-side-screen, #activeSituationText) .sr-tweet-count {
  font-variant-numeric: tabular-nums;
}

/* X144-2026-05-14: Hacker News–styled card for hn-front-page rows.
   Same overall envelope as the tweet card (centered in the .sr-side-screen,
   8px padding, soft inner panel) so the wall stays visually consistent
   in size and weight — only the interior typography + accent flips. The
   orange Y mark + title-prominent layout + grey points/comments metadata
   line is the HN signature aesthetic; once you see it you know it's
   from HN, no chyron label needed. Captain X144: "If it's a tweet, it
   should look like a tweet, and if it's a Hacker News headline, it
   should look like Hacker News." */
:where(.sr-side-screen, #activeSituationText) .sr-hn {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(255, 102, 0, 0.18);   /* HN orange edge */
  border-radius: 4px;                          /* HN's stock corner is square-ish — keep less round than the tweet card */
  padding: 8px;
}
:where(.sr-side-screen, #activeSituationText) .sr-hn-empty {
  opacity: 0.5;
  border-color: rgba(255, 255, 255, 0.08);     /* drop the orange tint when there's nothing to show */
}
:where(.sr-side-screen, #activeSituationText) .sr-hn-mark {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: 2px;
  background: #FF6600;                         /* the canonical Hacker News orange */
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--f-body);
  font-size: 14px;
  font-weight: 800;
  /* HN's "Y" mark is set in Verdana; pick a slab-y feel via tighter
     letter-spacing + the body font so it reads as "logo" not "text". */
  letter-spacing: 0;
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
  margin-top: 1px;                             /* tiny optical lift so it baseline-aligns with the title */
}
:where(.sr-side-screen, #activeSituationText) .sr-hn-mark-empty {
  background: rgba(255, 255, 255, 0.1);
  color: var(--sr-cyan-mid);
}
:where(.sr-side-screen, #activeSituationText) .sr-hn-stack {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
:where(.sr-side-screen, #activeSituationText) .sr-hn-title {
  /* Title dominates the card. White on the situation-room dark backdrop
     matches the tweet body's color so the visual weight feels right,
     but bumped to 13px (vs the tweet body's 12.5) since HN's whole shape
     is "title-first." Two-line clamp keeps the side cells visually
     tidy regardless of headline length. */
  font-family: var(--f-body);
  font-size: 13px;
  line-height: 1.3;
  font-weight: 600;
  color: #fff;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}
:where(.sr-side-screen, #activeSituationText) .sr-hn-meta {
  /* HN's metadata line is a single horizontal row of muted text with
     "·" separators. Match that. */
  display: flex;
  gap: 4px;
  align-items: center;
  font-family: var(--f-body);
  font-size: 11px;
  color: rgba(255, 255, 255, 0.55);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
:where(.sr-side-screen, #activeSituationText) .sr-hn-sep {
  /* Slightly muted dot separator so the metadata reads as a single
     flowing line rather than a list. */
  opacity: 0.55;
}
:where(.sr-side-screen, #activeSituationText) .sr-hn-points {
  /* Orange tint on the points number so it reads as the most
     HN-distinctive part of the metadata — that's what HN itself
     emphasises with its "[X] points" sub-line. */
  color: rgba(255, 153, 51, 0.92);
  font-weight: 600;
}
/* Hide the legacy `.sr-side-text` paragraph styling since the body is
   no longer plain text. The element itself is the wrapper — the
   tweet-card markup lives inside it — so we just need to neutralise
   the inherited paragraph margin / clamp / color rules. */
.sr-side-screen .sr-side-text {
  margin: 0;
  /* Color / line-clamp / font-size now live on the inner
     .sr-tweet-body rule above; reset here so the wrapper doesn't
     leak the old green-VT323 styling into the card. */
  color: inherit;
  font-size: inherit;
  line-height: inherit;
  display: block;
  -webkit-line-clamp: unset;
  overflow: visible;
}
/* Locked-tier softens the card so it reads as a teaser rather than a
   live wire — the body still says what's on screen, but the
   engagement bar grays out. Pairs with the existing
   body.is-locked-tier .sr-side filter rule. */
body.is-locked-tier :where(.sr-side-screen, #activeSituationText) .sr-tweet-actions {
  opacity: 0.4;
}

/* ── X149-2026-05-14: genre pill row inside the primary monitor ────
   Captain X149: "The all button needs to be in the center monitor."
   Moved the pill row out of the top .sr-genre-bar and into the
   primary monitor's screen. The top bar now only carries the LIVE
   label; pills paint into #srGenreBarPrimary inside .sr-primary.
   Visually similar to the old top-bar pills but slightly tighter
   (the primary monitor is narrower than the full top bar width). */
.sr-genre-bar-primary {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 10px;
  padding: 0;
  /* X149c-2026-05-14: container query setup. Pills inside this bar
     scale their font with the bar's actual inline width (not the
     viewport's) — the right modern primitive for "tie the font to
     the button so it automatically shrinks." Browser support:
     Chrome 105+, Safari 16+, Firefox 110+ — all current versions. */
  container-type: inline-size;
  container-name: sr-genre-primary;
}
.sr-genre-bar-primary:empty {
  /* Drop the breathing-room margin when there are no pills yet
     (pre-first-paint, or no categories returned). Prevents a
     phantom gap above the situation text on cold load. */
  margin: 0;
}
.sr-genre-bar-primary .sr-genre-pill {
  font-family: var(--f-body);
  /* X149c-2026-05-14 (Captain): "tie the font to the button so it
     automatically shrinks. Do whatever people do nowadays."
     Container-query units (cqi = 1% of the inline-size of the
     nearest container with `container-type: inline-size` — that's
     .sr-genre-bar-primary above). The pill font scales with the
     pill row's actual width, not the viewport's, so the response
     curve matches the surface the user is actually looking at.
     - 8px floor: never smaller than readable.
     - 1.8cqi: at a 600px-wide pill row this is 10.8px, at 450px
       it's 8.1px, at 800px it's 14.4px → clamps to 11px.
     - 11px ceiling: never bigger than the original visual.
     Padding stays fixed so the button shrinks horizontally with
     the text. Letter-spacing and uppercase are typographic
     identity, not size-dependent, so they stay constant. */
  font-size: clamp(8px, 1.8cqi, 11px);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--sr-cyan-dark);
  background: rgba(0, 0, 0, 0.35);
  color: var(--sr-cyan);
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.sr-genre-bar-primary .sr-genre-pill:hover {
  background: rgba(102, 230, 255, 0.10);
  border-color: var(--sr-cyan);
}
.sr-genre-bar-primary .sr-genre-pill[aria-pressed="true"] {
  background: var(--sr-cyan);
  color: #04141a;
  border-color: var(--sr-cyan);
  font-weight: 700;
}
.sr-genre-bar-primary .sr-genre-pill[aria-pressed="true"]:hover {
  background: var(--sr-cyan-mid);
  border-color: var(--sr-cyan-mid);
}
.sr-genre-bar-primary .sr-genre-pill[data-slug="custom"] {
  /* Mirror the existing top-bar treatment for the BYO-custom-station
     pill — slight visual differentiation so the user reads it as a
     "your saved station" affordance rather than a server slug. */
  border-style: dashed;
}

/* ── X??-2026-05-15 — Tweet preview modal ───────────────────────────────
   Captain: "I want it to pop open a preview window of the actual tweet,
   so we can see who they're replying to." Opened by a SECOND click on an
   already-active side monitor (first click still promotes), or any click
   on the primary monitor when its row is a tweet.

   Visual vocabulary matches the rest of the situation room — dark panel
   bg, cyan accent border, square corners, no rounded `border-radius`
   (the sr-screen language is CRT, not modern card). Centered overlay
   with a dim backdrop. The Twitter widget renders its own dark-themed
   blockquote inside `.tp-modal-body` — we don't restyle that; we just
   give it a generous container and let widgets.js own the look of the
   tweet itself. */
.tp-modal {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.tp-modal[hidden] {
  /* Belt + suspenders — `hidden` attribute already removes it, but
     belt-and-suspenders so a child rule (e.g. an animation) can't
     accidentally override the attribute. */
  display: none;
}
.tp-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 8, 14, 0.78);
  /* Soft cyan vignette so the modal reads as a CRT-on-CRT window
     rather than a plain dim sheet. */
  background:
    radial-gradient(ellipse at center, rgba(0, 16, 28, 0.55) 0%, rgba(0, 6, 12, 0.92) 100%);
  cursor: pointer;          /* hint that clicking the backdrop dismisses */
}
.tp-modal-card {
  position: relative;
  z-index: 1;
  width: min(560px, 100%);
  max-height: min(86vh, 760px);
  display: flex;
  flex-direction: column;
  background: var(--sr-panel-bg, #050d1a);
  color: #fff;
  border: 1px solid var(--sr-cyan-dark, #1e4a64);
  box-shadow:
    0 0 0 1px rgba(102, 230, 255, 0.08),
    0 14px 40px rgba(0, 0, 0, 0.7),
    inset 0 1px 0 rgba(102, 230, 255, 0.15);
}
.tp-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px 6px;
  border-bottom: 1px solid var(--sr-cyan-dark, #1e4a64);
  background: var(--sr-panel-bg-2, #0a1828);
}
.tp-modal-title {
  font-family: var(--f-body);
  font-size: 12px;
  letter-spacing: 0.06em;
  color: var(--sr-cyan, #66e6ff);
  text-transform: uppercase;
}
.tp-modal-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  line-height: 1;
  font-size: 18px;
  background: var(--sr-btn-bg, #0e2840);
  color: var(--sr-cyan, #66e6ff);
  border: 1px solid var(--sr-cyan-dark, #1e4a64);
  cursor: pointer;
  font-family: var(--f-body);
}
.tp-modal-close:hover {
  background: var(--sr-btn-bg-hover, #15426a);
  color: #fff;
}
.tp-modal-body {
  flex: 1 1 auto;
  min-height: 0;
  padding: 16px;
  overflow: auto;
  /* Twitter's rendered iframe sets its own width inline; we just give
     it a parent that doesn't fight that. The pre-widget fallback
     blockquote uses our copy of the tweet text — style it lightly
     so it reads as a placeholder (not styled as a quote pull). */
  background: var(--sr-panel-bg, #050d1a);
}
.tp-modal-body .twitter-tweet {
  /* Center the rendered iframe horizontally inside the modal body. */
  margin-left: auto !important;
  margin-right: auto !important;
}
.tp-modal-body blockquote.twitter-tweet:not(.twitter-tweet-rendered) {
  /* Pre-widget fallback style — visible briefly while widgets.js loads,
     OR permanently if it fails to load (offline / blocked). Mimics the
     X140 tweet card vocabulary so the placeholder doesn't look broken. */
  margin: 0;
  padding: 12px 14px;
  background: #0d1620;
  border: 1px solid #15273b;
  color: #e7e9ea;
  font-family: var(--f-body);
  font-size: 14px;
  line-height: 1.4;
}
.tp-modal-body blockquote.twitter-tweet:not(.twitter-tweet-rendered) p {
  margin: 0 0 8px;
}
.tp-modal-body blockquote.twitter-tweet:not(.twitter-tweet-rendered) a {
  color: var(--sr-cyan, #66e6ff);
}

/* When the modal is open, lock the page behind it so backdrop scroll
   doesn't bleed through. */
body.tp-modal-open {
  overflow: hidden;
}

/* Cursor hint: primary + side monitors are clickable surfaces. The
   first click on a side promotes; the second click on the active one
   opens the preview. The hand cursor reads as "this is interactive"
   even before the user discovers the two-click pattern. */
.sr-monitor.sr-side[role="button"] { cursor: pointer; }
.sr-monitor.sr-primary { cursor: pointer; }

/* Mobile: full-bleed modal, drop the padding so the rendered Twitter
   embed has room on a narrow viewport. */
@media (max-width: 560px) {
  .tp-modal { padding: 0; }
  .tp-modal-card { max-height: 100vh; width: 100%; }
  .tp-modal-body { padding: 10px; }
}


/* === inlined: styles/25-ai-edit.css === */
/* ============================================================================
   25-ai-edit.css — "AI edit" button BELOW the meme card (X66 2026-05-05;
                    layout reworked X85 2026-05-08)
   ============================================================================
   Sits as a flow-positioned row directly BELOW the active meme card.
   Two variants:

     .ai-edit-btn               default (open, paid surfaces — marketer.html
                                and marketermode.html)
     .ai-edit-btn--locked       game-mode (host/player) — link to /subscribe
                                with a tiny lock glyph

   X85-2026-05-08: was an absolutely-positioned overlay sitting inside
   the slot-stage card (bottom 25% of the meme image). Captain feedback:
   "let's move the AI Edit button off of the meme image, and also, the
   sample isn't fully viewable." The overlay (a) covered the bottom
   quarter of the meme art, (b) overlapped slot textareas positioned
   in the bottom region, and (c) blocked part of long placeholder /
   sample text. Post-X85 the anchor is a sibling row right below the
   stage so the meme image is unobstructed, slot text fits, and the
   button still reads as a per-meme control (not a global page
   action) because of its physical adjacency to the meme card.

   When clicked on an open surface, the .ai-edit-prompt panel slides up
   from the button. After submit, .ai-edit-spinner replaces the prompt
   while the job is polling. Both the prompt and spinner now grow
   downward in the flow rather than over the meme.

   Companion JS: public/ai-edit.js
   Companion module: lib/image-edit-adapter.js (server)
   ========================================================================== */

/* ── Sibling anchor BELOW the slot stage (X85 layout) ─────────────────── */
.ai-edit-anchor {
  /* Static, flow-positioned row that sits between the meme card above
     and whatever next sibling comes after (typically the Submit button
     or the player's submit-status line). Width tracks the parent
     container; the .ai-edit-btn child's `align-self: flex-end` (set
     below) keeps the button right-aligned, matching the visual
     position it had in the pre-X85 absolute overlay. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  margin: 6px auto 4px auto;
  /* Match the meme card's max-width so the button row never extends
     wider than the image it controls. .submit-stage caps at 540px;
     the host-side override caps at 380px. We mirror those caps so
     the button visually reads as "attached to" the card above. */
  max-width: 540px;
  padding: 0;
}
#hostSubmitStage .ai-edit-anchor { max-width: 380px; }

/* ── The pill button ──────────────────────────────────────────────────── */
.ai-edit-btn {
  align-self: flex-end;                        /* bottom-right by default */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px 5px 10px;
  font-family: var(--f-display, 'Lato', sans-serif);
  font-weight: 900;
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-decoration: none;
  border-radius: 999px;
  cursor: pointer;
  background: linear-gradient(180deg, #4ea4ff 0%, #2c7bd6 100%);
  color: #fff;
  border: 1px solid #2563b3;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45),
              0 2px 4px rgba(0, 0, 0, 0.32);
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.35);
  transition: filter 0.1s, transform 0.06s;
}
.ai-edit-btn:hover  { filter: brightness(1.08); color: #fff; text-decoration: none; }
.ai-edit-btn:active { transform: translateY(1px); filter: brightness(0.94); }
.ai-edit-btn:focus-visible {
  outline: 2px solid #ffd84a;
  outline-offset: 2px;
}
.ai-edit-btn-spark {
  font-size: 12px;
  filter: drop-shadow(0 0 2px rgba(255, 230, 120, 0.6));
}
.ai-edit-btn-text { white-space: nowrap; }

/* ── Locked variant (game mode → click goes to /subscribe.html) ──────── */
.ai-edit-btn--locked {
  background: linear-gradient(180deg, #888 0%, #5a5a5a 100%);
  border-color: #444;
  color: #f0f0f0;
  /* Keep clickable — the click navigates to /subscribe.html. The dim look
     signals "not available here" without disabling the conversion path. */
}
.ai-edit-btn--locked:hover { color: #fff; filter: brightness(1.06); }
.ai-edit-btn-lock {
  font-size: 11px;
  opacity: 0.92;
}

/* ── Inline prompt panel (open variant, after button click) ──────────── */
.ai-edit-prompt {
  align-self: stretch;
  display: flex;
  gap: 6px;
  padding: 6px 8px;
  background: rgba(20, 24, 36, 0.92);
  border: 1px solid #2563b3;
  border-radius: 6px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
  animation: ai-edit-slide-up 0.16s ease-out;
}
.ai-edit-prompt-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 5px 8px;
  font-family: var(--f-body, 'Lato', sans-serif);
  font-size: 12px;
  color: #fff;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 4px;
  outline: none;
}
.ai-edit-prompt-input::placeholder {
  color: rgba(255, 255, 255, 0.55);
  font-style: italic;
}
.ai-edit-prompt-input:focus {
  border-color: #4ea4ff;
  box-shadow: 0 0 0 2px rgba(78, 164, 255, 0.35);
}
.ai-edit-prompt-submit,
.ai-edit-prompt-cancel {
  flex: 0 0 auto;
  padding: 5px 10px;
  font-family: var(--f-display, 'Lato', sans-serif);
  font-weight: 900;
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-radius: 4px;
  cursor: pointer;
  border: 1px solid #2563b3;
}
.ai-edit-prompt-submit {
  background: linear-gradient(180deg, #4ea4ff 0%, #2c7bd6 100%);
  color: #fff;
}
.ai-edit-prompt-cancel {
  background: rgba(255, 255, 255, 0.08);
  color: #ddd;
  border-color: rgba(255, 255, 255, 0.22);
}
.ai-edit-prompt-submit:hover { filter: brightness(1.08); }
.ai-edit-prompt-cancel:hover { background: rgba(255, 255, 255, 0.16); }
.ai-edit-prompt-submit:disabled {
  opacity: 0.5;
  cursor: progress;
}

/* ── Polling spinner ─────────────────────────────────────────────────── */
.ai-edit-spinner {
  align-self: stretch;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  background: rgba(20, 24, 36, 0.92);
  border: 1px solid #2563b3;
  border-radius: 6px;
  font-family: var(--f-display, 'Lato', sans-serif);
  font-size: 11px;
  color: #fff;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.ai-edit-spinner-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-top-color: #4ea4ff;
  animation: ai-edit-spin 0.9s linear infinite;
  flex: 0 0 auto;
}
.ai-edit-spinner-cancel {
  margin-left: auto;
  padding: 3px 8px;
  font-size: 10px;
  color: #ddd;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 4px;
  cursor: pointer;
}
.ai-edit-spinner-cancel:hover { background: rgba(255, 255, 255, 0.16); }

/* ── Toast for errors ────────────────────────────────────────────────── */
.ai-edit-error {
  align-self: stretch;
  padding: 6px 10px;
  background: rgba(120, 30, 30, 0.92);
  border: 1px solid #b04545;
  border-radius: 6px;
  font-family: var(--f-body, 'Lato', sans-serif);
  font-size: 11px;
  color: #fff;
}

@keyframes ai-edit-slide-up {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes ai-edit-spin {
  to { transform: rotate(360deg); }
}

/* Mobile tightening — prompt panel takes the full bottom strip width
   instead of competing with the button on a narrow card. */
@media (max-width: 480px) {
  .ai-edit-anchor { padding: 4px 6px 6px; }
  .ai-edit-btn-text { font-size: 10px; }
  .ai-edit-prompt-input { font-size: 11px; }
}

/* ── Situation room share-row override (X-NEXT 2026-05-15) ─────────────
   When the AI Edit button is mounted inside #primaryShareRow on
   marketer.html, it sits next to three .menu-btn siblings (Automatic
   Banger, Save, Share). Captain wants the four buttons to read as one
   set — same shape and size, same chrome — instead of the blue
   pill-gradient AI Edit chip standing apart from the cyan square
   menu-btn trio.

   This block:
     1) Flattens the .ai-edit-anchor wrapper that mountButton injects
        as a sibling of #aiEditMount. The default anchor centers itself
        with auto margins and caps at 540px wide (sized for a stand-
        alone row UNDER a meme card). Inside .sr-share-row we need it
        to behave like a normal flex item so the button sits flush
        with the menu-btn siblings. flex-direction: column is preserved
        so the prompt panel / spinner / error toast stack vertically
        below the button when expanded.
     2) Restyles .ai-edit-btn to inherit the .sr-screen .menu-btn look:
        square corners, cyan border, .9em font, no uppercase, no
        gradient. Locked + open variants both fall through to this
        override since they share the .ai-edit-btn base class.

   Scoped to .sr-share-row only — host/player meme-card AI Edit buttons
   keep their existing pill styling, which Captain wants because they
   sit on the meme image, not inside a row of peer chips. */
.sr-share-row .ai-edit-anchor {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  margin: 0;
  padding: 0;
  max-width: none;
}
.sr-share-row .ai-edit-btn {
  align-self: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 1px 6px;
  font-family: inherit;
  font-weight: normal;
  font-size: 0.9em;
  letter-spacing: normal;
  text-transform: none;
  text-shadow: none;
  border-radius: 0;
  background: var(--sr-btn-bg);
  color: var(--sr-cyan);
  border: 1px solid var(--sr-cyan-dark);
  box-shadow: inset 0 1px 0 rgba(102, 230, 255, 0.15);
}
.sr-share-row .ai-edit-btn:hover {
  background: var(--sr-btn-bg-hover);
  color: #fff;
  filter: none;
}
.sr-share-row .ai-edit-btn:active {
  transform: none;
  filter: none;
}
.sr-share-row .ai-edit-btn--locked {
  background: var(--sr-btn-bg);
  border-color: var(--sr-cyan-dark);
  color: var(--sr-cyan);
}
.sr-share-row .ai-edit-btn--locked:hover {
  background: var(--sr-btn-bg-hover);
  color: #fff;
}
.sr-share-row .ai-edit-btn-spark,
.sr-share-row .ai-edit-btn-lock {
  font-size: inherit;
  filter: none;
}

