/* ==========================================================================
   ROCMeta — "The Forge"
   --------------------------------------------------------------------------
   Illuminated craft, War Room heat: near-black opaque plates, a molten ember
   accent, engraved edges instead of glass, and rank expressed as temperature.

   HOW THIS FILE IS REACHED
   roc_head() in app/view.php links exactly one public stylesheet. A page opts
   in with 'css' => 'forge' and rolls back by deleting that array key. Nothing
   else switches. Pages that have not opted in still get styles.css and Cinzel,
   so this file may be incomplete without breaking anything that is still on
   the old sheet.

   RULES THIS FILE KEEPS
   1. Surfaces are OPAQUE. Elevation is a lightness step plus a 1px light top
      edge and a 1px dark bottom edge. No backdrop-filter, no tinted glass —
      that is what let 12 unrelated component types share one look before.
   2. Warmth encodes HOW GOOD (the heat ramp). Pigment encodes WHAT KIND (troop
      type). They never compete because they never mean the same thing.
   3. Every colour pair below was measured with tools/../c.py. Ratios are noted
      inline. Nothing that carries meaning sits under 4.5:1 as text.
   4. Motion seats, never floats. One easing, three durations. Hover effects are
      behind @media (hover:hover) so they cannot stick after a tap on Android.
   5. Minimum 44x44 for anything you can tap.

   CONTRACT WITH assets/js/app.js
   app.js builds markup inside innerHTML template strings, so its class names
   appear in no PHP file and cannot be found by grepping the templates. It also
   toggles the `hidden` attribute on components that carry their own display
   rule — hence the defensive [hidden] block at the end of this file, which is
   load-bearing, not boilerplate. Eight CSS rules key off ARIA state rather than
   a class ([aria-pressed], [aria-current]); those selectors are the interface.
   ========================================================================== */

/* ==========================================================================
   1. TOKENS
   ========================================================================== */
:root {
  /* -- ground & plates -------------------------------------------------- */
  --void:      #0e0f13;   /* page ground — brand Obsidian, sampled from lockup-dark.png */
  --plate:     #141418;   /* default raised surface */
  --plate-2:   #1e1e24;   /* nested / active surface */
  --plate-3:   #2a2a31;   /* highest — NOTE: --text-3 fails AA here, use --text-2 */

  /* engraved edge: light on top, dark underneath. This 2px carries all the
     material information — the inverse of a blur. */
  --edge-hi:   rgba(255, 255, 255, .06);
  --edge-lo:   rgba(0, 0, 0, .55);
  --hair:      #24242b;   /* 1px divider */
  --hair-soft: #1c1c22;

  /* -- text (warm neutrals; ratios vs --void / --plate) ------------------ */
  --text:      #f2efe9;   /* 17.24 / 16.01 */
  --text-2:    #b6aea3;   /*  9.02 /  8.37 */
  --text-3:    #8f8a82;   /*  5.77 /  5.36  — do not use on --plate-3 (4.16) */
  --text-3-up: #97918a;   /*  6.34 /  5.89  — the --plate-3 substitute (4.57) */

  /* -- ember accent ----------------------------------------------------- */
  --ember-1:   #ffc24d;   /* 12.32 / 11.44 — the readable one, use for text */
  --ember-2:   #ffa629;   /* 10.12 /  9.40 */
  --ember-3:   #ff4d2e;   /*  5.98 /  5.56 — gradient end + large marks only */
  --ember-ink: #1a0d02;   /* 11.86 on --ember-1 — text ON an ember fill */
  --ember-dim: rgba(255, 77, 46, .13);
  --ember-line:#56291a;
  --molten:    linear-gradient(168deg, var(--ember-1), var(--ember-3));

  /* -- heat ramp = rank ------------------------------------------------- */
  --heat-hot:  linear-gradient(180deg, var(--ember-1), var(--ember-3));
  --heat-mid:  #c2683a;   /* 5.03 / 4.67 */
  --heat-cold: #5a5d66;   /* 3.01 — RAIL ONLY. Never text. */
  --heat-cold-text: #8b8f9a; /* 6.12 / 5.68 — the labelled substitute */

  /* -- troop pigment = category (never rank) ---------------------------- */
  --cav:  #e07a5f;  /* 6.71 / 6.23 */
  --arc:  #9dbb72;  /* 9.22 / 8.56 */
  --foo:  #7fa8d0;  /* 7.93 / 7.36 */
  --mix:  #b08ac9;  /* 6.90 / 6.41 */

  /* -- status ----------------------------------------------------------- */
  --ok:   #6cc48f;
  --warn: #e0a33a;
  --bad:  #e8705f;

  /* -- type ------------------------------------------------------------- */
  --f-display: Fraunces, Georgia, "Times New Roman", serif;
  --f-body:    Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --f-mono:    "JetBrains Mono", ui-monospace, SFMono-Regular, Consolas, monospace;

  /* 8 steps. 16px body floor; 12px label floor. The old sheet had 54 distinct
     font-size values and 57 declarations below 0.8rem, down to 9.3px. */
  --fs-2xs: .75rem;    /* 12 — uppercase labels only */
  --fs-xs:  .8125rem;  /* 13 */
  --fs-sm:  .875rem;   /* 14 */
  --fs-md:  1rem;      /* 16 — body floor, and the iOS no-zoom input floor */
  --fs-lg:  1.0625rem; /* 17 — long-form prose */
  --fs-xl:  1.375rem;  /* 22 */
  --fs-2xl: 1.75rem;   /* 28 */
  --fs-3xl: clamp(2rem, 4.6vw, 3.1rem);

  /* -- space ------------------------------------------------------------ */
  --s-1: .25rem; --s-2: .5rem;  --s-3: .75rem; --s-4: 1rem;
  --s-5: 1.5rem; --s-6: 2rem;   --s-7: 3rem;   --s-8: 4rem;

  /* -- radii: near-square containers; pills for status chips ONLY -------- */
  --r-xs: 3px; --r-sm: 5px; --r-md: 7px; --r-lg: 10px; --r-pill: 999px;

  /* -- elevation -------------------------------------------------------- */
  --lift-1: inset 0 1px 0 var(--edge-hi), inset 0 -1px 0 var(--edge-lo);
  --lift-2: inset 0 1px 0 var(--edge-hi), inset 0 -1px 0 var(--edge-lo),
            0 2px 10px rgba(0, 0, 0, .45);
  --lift-3: inset 0 1px 0 var(--edge-hi), 0 18px 44px rgba(0, 0, 0, .62);

  /* -- motion ----------------------------------------------------------- */
  --ease: cubic-bezier(.2, .7, .3, 1);
  --t-1: 120ms; --t-2: 180ms; --t-3: 280ms;

  /* -- layers ----------------------------------------------------------- */
  --z-dock: 30; --z-scrim: 40; --z-sheet: 50; --z-subscribe: 55;
  --z-dialog: 60; --z-toast: 70; --z-skip: 100;

  --container: 1180px;
  --tap: 44px;

  color-scheme: dark;
}

/* ==========================================================================
   1b. LEGACY TOKEN BRIDGE — load-bearing, not tidiness
   --------------------------------------------------------------------------
   The templates carry 698 inline style="" attributes, and some of them
   reference the OLD sheet's custom properties directly:

     --line    glossary.php, methodology.php   (table borders)
     --soft    glossary.php, hero.php, methodology.php
     --muted   combos.php, hero.php, meta.php
     --panel   glossary.php
     --accent-2 methodology.php
     --gold-2  index.php

   An inline style beats any rule in this file, so if these names resolve to
   nothing, those borders disappear and that text turns default-coloured — on
   pages that otherwise look finished. Aliasing the whole old palette onto
   Forge values costs ~25 lines and makes forge.css safe to ship against markup
   that has not been touched yet.

   These are deliberately temporary. Each one dies as its page's inline styles
   are removed; the file should end the rollout with this block deleted.
   ========================================================================== */
:root {
  --accent:      var(--ember-2);
  --accent-2:    var(--ember-1);
  --accent-deep: var(--heat-mid);
  --gold:        var(--ember-1);
  --gold-2:      var(--ember-2);
  --gold-deep:   var(--heat-mid);
  --ice:         var(--ember-1);
  --ink:         var(--ember-ink);

  --bg:          var(--void);
  --bg-2:        var(--plate);
  --panel:       var(--plate);
  --panel-2:     var(--plate-2);

  --line:        var(--hair);
  --line-strong: var(--plate-3);

  /* --soft was #5f7f9c: measured 4.02:1, an AA failure that coloured entire
     article bodies. Re-pointed rather than reproduced — and at --text-2
     (8.37:1) not --text-3, because hero.php sets it inline on every paragraph
     of the write-up and the rationale, and long prose at the tertiary tone
     reads washed out. */
  --soft:        var(--text-2);
  --muted:       var(--text-2);
  --silver:      var(--text-2);

  --blue:   var(--foo);
  --green:  var(--ok);
  --red:    var(--bad);
  --purple: var(--mix);

  --radius:     var(--r-md);
  --radius-lg:  var(--r-lg);
  --glow:       rgba(255, 110, 50, .22);
  --shadow:      var(--lift-3);
  --shadow-soft: var(--lift-2);

  --font-body:    var(--f-body);
  --font-display: var(--f-display);
}

/* ==========================================================================
   2. RESET & BASE
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--void);
  color: var(--text);
  font: 400 var(--fs-md)/1.6 var(--f-body);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* A 128px grain tile at 3.5%. This does more to kill the "flat CSS gradient"
   look than any other single rule, and costs ~0.4 KB inline. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: .035;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='128' height='128'><filter id='n'><feTurbulence baseFrequency='.85' numOctaves='3'/></filter><rect width='128' height='128' filter='url(%23n)'/></svg>");
}
body > * { position: relative; z-index: 1; }

/* THE reset the old sheet was missing. Without it, form controls fall back to
   the UA's 13.33px system font, which is why .pill and .btn.small computed to
   37px tall and 18 of 19 controls missed the 44px floor. */
button, input, select, textarea { font: inherit; color: inherit; }

img, svg, video { max-width: 100%; height: auto; display: block; }

a { color: var(--ember-1); text-decoration-color: var(--ember-line); text-underline-offset: 3px; }
a:hover { text-decoration-color: currentColor; }

hr { border: 0; border-top: 1px solid var(--hair); margin: var(--s-6) 0; }

::selection { background: var(--ember-2); color: var(--ember-ink); }

/* One visible focus ring, everywhere. The old sheet had ten `outline:none`
   sites and a single :focus-visible rule buried at line 1414. */
:focus-visible {
  outline: 2px solid var(--ember-1);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* Anchors the templates rely on. */
#finder, #rankings, #toplists, #faq, #top, #ranked-list { scroll-margin-top: var(--s-6); }

/* ==========================================================================
   3. TYPOGRAPHY
   ========================================================================== */
h1, h2, h3, h4 {
  font-family: var(--f-display);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -.005em;
  color: var(--text);
  margin: 0 0 var(--s-3);
  font-variation-settings: "SOFT" 0, "WONK" 1;
}
h1 { font-size: var(--fs-3xl); }
h2 { font-size: var(--fs-2xl); margin-top: var(--s-6); }
h3 { font-size: var(--fs-xl); margin-top: var(--s-5); }
h4 { font-size: var(--fs-lg); }

p { margin: 0 0 var(--s-4); }

/* Long-form prose. The old sheet set article bodies in --soft (#5f7f9c,
   measured 4.02:1) which is why the guides read washed out. */
main p, main li { color: var(--text-2); font-size: var(--fs-lg); }
main p strong, main li strong { color: var(--text); font-weight: 600; }
main ul, main ol { margin: 0 0 var(--s-4); padding-left: 1.25em; }
main li { margin-bottom: var(--s-2); }
main li::marker { color: var(--ember-3); }

/* Every figure, everywhere, in tabular mono. A column of numbers that lines up
   is the cheapest "this is authoritative" signal available, and the site is
   almost entirely numbers. */
.num, .score, .rank, .stat b, .stat-value {
  font-family: var(--f-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}

.eyebrow, .mini-eyebrow {
  font: 700 var(--fs-2xs)/1 var(--f-body);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ember-2);
  margin: 0 0 var(--s-2);
  display: block;
}

/* On a phone the 22px lede ran six lines and dominated the screen above the
   tool. It steps up only when there is room for it. */
.hero-lede { font-size: var(--fs-md); line-height: 1.55; color: var(--text-2); max-width: 62ch; }
@media (min-width: 720px) { .hero-lede { font-size: var(--fs-xl); line-height: 1.5; } }

/* Same problem on the page eyebrow — it wrapped to two cramped lines. */
@media (max-width: 480px) { .lander .eyebrow { letter-spacing: .1em; line-height: 1.5; } }

/* ==========================================================================
   4. LAYOUT
   ========================================================================== */
.section {
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--s-6) var(--s-4);
}
@media (min-width: 720px) { .section { padding-inline: var(--s-5); } }

main { display: block; }

/* Skip link — visible the moment it is focused. */
.skip-link {
  position: absolute; left: var(--s-3); top: var(--s-3);
  z-index: var(--z-skip);
  transform: translateY(-200%);
  background: var(--ember-1); color: var(--ember-ink);
  font-weight: 700; font-size: var(--fs-sm);
  padding: var(--s-3) var(--s-4); border-radius: var(--r-sm);
  min-height: var(--tap); display: inline-flex; align-items: center;
}
.skip-link:focus { transform: translateY(0); }

/* ==========================================================================
   5. COMPONENTS
   ========================================================================== */

/* -- engraved plate ---------------------------------------------------- */
.plate, .rank-cta {
  background: var(--plate);
  border-radius: var(--r-md);
  box-shadow: var(--lift-1);
  padding: var(--s-5);
}

/* -- buttons ----------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  min-height: var(--tap);
  padding: var(--s-3) var(--s-5);
  border: 1px solid var(--hair);
  border-radius: var(--r-sm);
  background: var(--plate-2);
  color: var(--text);
  font: 600 var(--fs-sm)/1 var(--f-body);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--t-1) var(--ease), border-color var(--t-1) var(--ease),
              transform var(--t-1) var(--ease);
}
/* Things seat; they do not lift. Works identically on touch and mouse. */
.btn:active { transform: translateY(1px) scale(.995); }

.btn.primary {
  background: var(--molten);
  color: var(--ember-ink);
  border-color: transparent;
  font-weight: 700;
  /* The signature 45-degree notch. Used on exactly three components:
     .btn.primary, the tier chip, and the rank plate. Nowhere else. */
  clip-path: polygon(0 0, calc(100% - 13px) 0, 100% 13px, 100% 100%, 0 100%);
  box-shadow: 0 6px 20px rgba(255, 77, 46, .22);
}
.btn.ghost { background: transparent; color: var(--text-2); }
.btn.small { padding: var(--s-2) var(--s-4); font-size: var(--fs-xs); }

/* Hover is a progressive enhancement, never the only affordance. */
@media (hover: hover) and (pointer: fine) {
  .btn:hover { background: var(--plate-3); border-color: var(--hair); }
  .btn.primary:hover { filter: brightness(1.06); }
  .btn.ghost:hover { color: var(--text); background: var(--plate); }
}

/* -- chips ------------------------------------------------------------- */
.chip, .pill {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: 34px; padding: var(--s-2) var(--s-3);
  border: 1px solid var(--hair); border-radius: var(--r-sm);
  background: var(--plate);
  font: 600 var(--fs-xs)/1 var(--f-body);
  color: var(--text-2);
  cursor: pointer;
}
/* State lives on the ARIA attribute, not a class — app.js drives these. */
.pill[aria-pressed="true"] {
  background: var(--molten); border-color: transparent;
  color: var(--ember-ink); font-weight: 700;
}
.tier-chip {
  font: 700 var(--fs-2xs)/1 var(--f-body);
  letter-spacing: .09em; text-transform: uppercase;
  padding: 5px 7px; border-radius: var(--r-xs);
  clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 6px, 100% 100%, 0 100%);
}
.tier-chip.is-current { background: var(--molten); color: var(--ember-ink); }
.tier-chip.is-mid     { background: var(--heat-mid); color: var(--ember-ink); }
.tier-chip.is-start   { background: var(--plate-3); color: var(--heat-cold-text); }

/* -- heat rail: the ordinal device ------------------------------------- */
.rail { display: block; width: 3px; border-radius: 1px; background: var(--heat-cold); }
.rail.is-current { background: var(--heat-hot); }
.rail.is-mid     { background: var(--heat-mid); }

/* -- troop pigment ------------------------------------------------------ */
.t-cavalry { color: var(--cav); }
.t-archer  { color: var(--arc); }
.t-footmen { color: var(--foo); }
.t-mixed   { color: var(--mix); }

/* -- breadcrumbs -------------------------------------------------------- */
.breadcrumbs {
  font-size: var(--fs-sm); color: var(--text-3);
  margin-bottom: var(--s-3);
}
.breadcrumbs a { color: var(--text-2); text-decoration: none; }
.breadcrumbs a:hover { color: var(--ember-1); text-decoration: underline; }

/* -- call to action block ---------------------------------------------- */
.rank-cta { text-align: center; }
.rank-cta h2 { margin-top: 0; }
.rank-cta p { color: var(--text-2); }

/* -- icons -------------------------------------------------------------- */
.ic { width: 1.15em; height: 1.15em; display: inline-block; vertical-align: -.15em; }

/* ==========================================================================
   6. SHELL — header, nav, footer
   ========================================================================== */
.site-header {
  /* NOTE: no overflow:hidden. The old sheet clipped the open mobile menu on
     /404 and /subscribe, leaving those pages with unreachable navigation. */
  border-bottom: 1px solid var(--hair);
  background:
    radial-gradient(120% 70% at 50% -20%, rgba(255, 110, 50, .10), transparent 62%),
    var(--void);
}

.nav {
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--s-3) var(--s-4);
  display: flex; align-items: center; gap: var(--s-4); flex-wrap: wrap;
}

.brand { display: flex; align-items: center; gap: var(--s-3); text-decoration: none; color: var(--text); margin-right: auto;
  min-height: var(--tap); }   /* was 38px tall on every page */
/* THE WORDMARK, in the owner's own face.
   Anton is a single-weight condensed display face, so 700 would be synthesised
   into a smeared fake bold — 400 is the only real weight it has. It also sets
   noticeably larger on the body than Fraunces at the same size, hence the step
   down to --fs-md, and it carries almost no sidebearing, hence the added
   tracking. Line-height 1 because Anton's own metrics already leave room. */
.brand strong {
  display: block;
  font: 400 var(--fs-md)/1 'Anton', var(--f-display);
  letter-spacing: .012em;
}
.brand small {
  display: block; margin-top: 2px;
  font-size: var(--fs-2xs); color: #7d7f8b;
  /* The brand guide sets UI labels in uppercase with wide tracking. Tracking is
     .14em rather than the guide's .4em: at this size, under a compact wordmark,
     .4em runs the strapline past the logo and wraps it on a 360px screen. The
     uppercase treatment is what carries the look; the exact tracking is a fit
     decision. Barlow Condensed is not loaded on purpose -- one more webfont
     request for a single line of 20 characters is not a trade worth making. */
  text-transform: uppercase;
  letter-spacing: .14em;
  white-space: nowrap;
}
/* Three tones, matching the lockup: the mark's own gold, bone, then steel.
   The gold is the brand hex rather than --ember-1, so the four letters sit
   beside the mark in exactly the colour the mark uses. */
.bm-hi  { color: #d9a441; }
/* Sampled from the brand lockup: ROC is Gold #d9a441, Meta is #9a9caa. Using
   --text made "Meta" near-white and lost the two-tone wordmark entirely. */
.bm-mid { color: #9a9caa; }
.bm-dim { color: #7d7f8b; }

/* THE SPLIT SHIELD, in the header.
   The mark carries its own colours in the markup (see roc_brand_mark) because
   the brand guide forbids recolouring it — so nothing here sets fill. What is
   set here is size, and it is fluid: the glyph tracks the header's own scale
   between a 320px phone and a wide desktop instead of sitting at one hard
   pixel value. The svg has a viewBox, so scaling it costs nothing. */
.brand-mark {
  flex: 0 0 auto; display: grid; place-items: center;
  width: clamp(30px, 2.4vw + 24px, 38px);
  height: clamp(30px, 2.4vw + 24px, 38px);
  background: var(--plate); border-radius: var(--r-sm); box-shadow: var(--lift-1);
}
/* ~82% fill. The mark's own artwork already carries margin inside its 72-unit
   box, so filling the plate more tightly than this reads as cramped. */
.bm-glyph { width: 82%; height: 82%; display: block; }
@media (hover: hover) and (pointer: fine) {
  .brand-mark { transition: box-shadow var(--t-2) var(--ease); }
  .brand:hover .brand-mark { box-shadow: var(--lift-2); }
}
@media (prefers-reduced-motion: reduce) { .brand-mark { transition: none; } }

.nav-toggle {
  min-height: var(--tap); min-width: var(--tap);
  padding: 0 var(--s-4);
  background: var(--plate); color: var(--text);
  border: 1px solid var(--hair); border-radius: var(--r-sm);
  font: 600 var(--fs-sm)/1 var(--f-body); cursor: pointer;
}
.nav-toggle:active { transform: translateY(1px); }

.nav-links { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.nav-links > a {
  display: inline-flex; align-items: center;
  min-height: var(--tap); padding: 0 var(--s-3);
  color: var(--text-2); text-decoration: none;
  font: 600 var(--fs-sm)/1 var(--f-body);
  border-radius: var(--r-sm);
  border-bottom: 2px solid transparent;
}
.nav-links > a[aria-current="page"] { color: var(--text); border-bottom-color: var(--ember-3); }
@media (hover: hover) and (pointer: fine) { .nav-links > a:hover { color: var(--text); background: var(--plate); } }

.nav-search { display: flex; align-items: center; gap: var(--s-2);
  background: var(--plate); border: 1px solid var(--hair); border-radius: var(--r-sm);
  padding: 0 var(--s-3); min-height: var(--tap); }
.nav-search-icon { color: var(--text-3); font-size: var(--fs-sm); }
.nav-search input {
  border: 0; background: transparent; color: var(--text);
  /* 16px floor: anything smaller makes iOS Safari zoom the whole page on focus. */
  font-size: var(--fs-md);
  min-height: var(--tap); min-width: 11ch; outline: none;
}
.nav-search input::placeholder { color: var(--text-3); }

@media (max-width: 860px) {
  .nav-toggle { order: 2; }
  .nav-links {
    order: 3; flex-basis: 100%;
    flex-direction: column; align-items: stretch;
    gap: 0; display: none;
    border-top: 1px solid var(--hair); margin-top: var(--s-3); padding-top: var(--s-2);
  }
  .nav-links.open { display: flex; }
  .nav-links > a { min-height: 48px; padding: 0 var(--s-2); }
  .nav-search { margin-top: var(--s-2); }
  .nav-search input { width: 100%; }
}
@media (min-width: 861px) { .nav-toggle { display: none; } }

/* -- page hero ---------------------------------------------------------- */
.rank-page-hero { padding-top: var(--s-5); padding-bottom: var(--s-5); }
.rank-page-hero h1 { margin: var(--s-2) 0 var(--s-3); }

/* -- footer -------------------------------------------------------------- */
.site-footer { border-top: 1px solid var(--hair); background: var(--plate); margin-top: var(--s-8); }
.footer-inner {
  max-width: var(--container); margin-inline: auto; padding: var(--s-6) var(--s-4);
  display: grid; gap: var(--s-5);
  grid-template-columns: 1fr;
}
@media (min-width: 720px) { .footer-inner { grid-template-columns: 1.4fr 1fr 1fr auto; } }
.footer-inner strong { display: block; font: 600 var(--fs-lg)/1.2 var(--f-display); margin-bottom: var(--s-3); }
.footer-inner p { color: var(--text-3); font-size: var(--fs-sm); }
.footer-note { font-size: var(--fs-xs); }
.footer-links { display: flex; flex-direction: column; }
.footer-links a {
  color: var(--text-2); text-decoration: none;
  font-size: var(--fs-sm);
  min-height: var(--tap); display: flex; align-items: center;
}
.footer-links a:hover { color: var(--ember-1); text-decoration: underline; }
.footer-actions { display: flex; flex-direction: column; gap: var(--s-2); align-items: stretch; }

/* ==========================================================================
   7. SUBSCRIBE PROMPT
   Opened by app.js / the footer script via the `hidden` attribute plus .open.
   ========================================================================== */
.sub-pop {
  position: fixed; inset: 0; z-index: var(--z-subscribe);
  display: grid; place-items: center;
  background: rgba(6, 6, 8, .72);
  padding: var(--s-4);
  opacity: 0; transition: opacity var(--t-2) var(--ease);
}
.sub-pop.open { opacity: 1; }
.sub-pop-card {
  position: relative;
  width: min(420px, 100%);
  background: var(--plate); border-radius: var(--r-lg);
  box-shadow: var(--lift-3);
  padding: var(--s-5);
}
.sub-pop-card h3 { margin: var(--s-1) 0 var(--s-2); font-size: var(--fs-xl); }
.sub-pop-card p { color: var(--text-2); font-size: var(--fs-sm); }
.sub-close {
  position: absolute; top: var(--s-2); right: var(--s-2);
  width: var(--tap); height: var(--tap);
  display: grid; place-items: center;
  background: transparent; border: 0; color: var(--text-2);
  font-size: 1.5rem; line-height: 1; cursor: pointer; border-radius: var(--r-sm);
}
.sub-close:hover { color: var(--text); background: var(--plate-2); }
.sub-pop-card input[type="email"] {
  width: 100%; min-height: var(--tap);
  padding: 0 var(--s-3);
  background: var(--void); color: var(--text);
  border: 1px solid var(--hair); border-radius: var(--r-sm);
  font-size: var(--fs-md);   /* 16px floor — stops the iOS zoom */
}
.sub-pop-card input[type="email"]::placeholder { color: var(--text-3); }
.sub-consent {
  display: flex; align-items: flex-start; gap: var(--s-2);
  margin: var(--s-3) 0; font-size: var(--fs-xs); color: var(--text-2);
  min-height: var(--tap);
}
.sub-consent input { width: 20px; height: 20px; margin-top: 2px; accent-color: var(--ember-3); flex: 0 0 auto; }
.sub-pop-card .btn.primary { width: 100%; }
.sub-msg { margin: var(--s-3) 0 0; font-size: var(--fs-xs); min-height: 1.2em; }
.sub-msg.ok  { color: var(--ok); }
.sub-msg.bad { color: var(--bad); }
/* Honeypot — must stay in the DOM and out of sight and out of the tab order. */
.hp-field { position: absolute !important; left: -9999px !important; width: 1px; height: 1px; opacity: 0; }

@media (max-width: 680px) {
  /* A slim bottom sheet, never a full-screen interstitial (Google demotes those). */
  .sub-pop { place-items: end stretch; padding: 0; background: rgba(6, 6, 8, .55); }
  .sub-pop-card {
    width: 100%; border-radius: var(--r-lg) var(--r-lg) 0 0;
    padding-bottom: calc(var(--s-5) + env(safe-area-inset-bottom));
  }
}

/* ==========================================================================
   8. TOAST
   ========================================================================== */
.toast {
  position: fixed; left: 50%; bottom: calc(var(--s-5) + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: var(--z-toast);
  background: var(--plate-2); color: var(--text);
  border-radius: var(--r-sm); box-shadow: var(--lift-2);
  padding: var(--s-3) var(--s-5); font-size: var(--fs-sm);
}

/* ==========================================================================
   8b. ADS
   Shared by 10 of the 16 page types. The reserved height is what stops the
   unit collapsing the layout when it fills — CLS is scored on the worst shift,
   and an ad arriving late is the classic one.
   ========================================================================== */
.ad-slot {
  display: block;
  margin: var(--s-6) auto;
  padding: var(--s-3);
  max-width: var(--container);
  background: var(--plate);
  border-radius: var(--r-md);
  box-shadow: var(--lift-1);
  min-height: 140px;              /* reservation, not decoration */
  text-align: center;
}
.ad-label {
  display: block;
  font: 700 var(--fs-2xs)/1 var(--f-body);
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: var(--s-2);
}
.adsbygoogle { display: block; }
/* An unfilled unit should take no space at all rather than leave a hole. */
.ad-slot:has(ins[data-ad-status="unfilled"]) { display: none; }

/* ==========================================================================
   8c. CONTENT PAGES — guides, glossary, methodology, about, 404
   ========================================================================== */

/* -- guides index -------------------------------------------------------- */
.guide-grid { display: grid; gap: var(--s-4); grid-template-columns: 1fr; }
@media (min-width: 720px) { .guide-grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); } }
.guide-card {
  display: flex; flex-direction: column;
  background: var(--plate); border-radius: var(--r-md);
  box-shadow: var(--lift-1);
  padding: var(--s-5);
  text-decoration: none; color: var(--text);
  border-left: 3px solid transparent;
  transition: border-color var(--t-2) var(--ease), background var(--t-2) var(--ease);
}
.guide-card h2 { font-size: var(--fs-lg); margin: 0 0 var(--s-2); }
.guide-card p  { color: var(--text-2); font-size: var(--fs-sm); margin: 0 0 var(--s-4); }
.guide-card-foot {
  margin-top: auto;
  font: 600 var(--fs-2xs)/1 var(--f-body);
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--ember-1);
}
@media (hover: hover) and (pointer: fine) {
  .guide-card:hover { border-left-color: var(--ember-3); background: var(--plate-2); }
}
.guide-card:active { transform: translateY(1px); }

/* -- a single guide ------------------------------------------------------ */
.guide-article { max-width: var(--container); margin-inline: auto; }
.guide-body h2 { border-bottom: 1px solid var(--hair); padding-bottom: var(--s-2); }
.guide-body a  { color: var(--ember-1); }

/* -- tables (methodology's eleven-check scorecard) ------------------------
   The glossary used to share these rules; it is a <dl> now, so .gloss-table
   and .gloss-row td have been dropped rather than left behind as dead
   selectors that look like they still style something.
   methodology.php carries inline border/padding declarations that beat these
   rules; what is set here is everything it does NOT specify. */
.data-table {
  width: 100%; border-collapse: collapse;
  font-size: var(--fs-sm);
  background: var(--plate);
  border-radius: var(--r-md);
  overflow: hidden;
  box-shadow: var(--lift-1);
}
.data-table th {
  font: 700 var(--fs-2xs)/1.3 var(--f-body);
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--text-3); background: var(--plate-2);
}
.data-table td { color: var(--text-2); vertical-align: top; }
.data-table td:nth-child(2) {
  font-family: var(--f-mono); font-variant-numeric: tabular-nums; color: var(--ember-1);
}
.rank-block { margin-bottom: var(--s-6); }

/* -- glossary ------------------------------------------------------------
   175 terms. This was a <table> with a hard 220px term column, which on a
   390px phone left about 130px for the definition — every definition wrapped
   to six or eight lines and the page measured 37,000px. It is a definition
   list, so it is a <dl> now: two columns when there is room, stacked when
   there is not, and never a horizontal scroller. */
.gl-primer {
  background: var(--plate); border: 1px solid var(--hair); border-radius: var(--r-lg);
  padding: var(--s-4) var(--s-5); margin: 0 0 var(--s-5);
  border-left: 3px solid var(--ember-3);
}
.gl-primer h2 { margin-top: 0; }
.gl-primer p:last-child { margin-bottom: 0; }

/* The filter and the jump-list ride along with the reader. Without this you
   have to scroll several screens back up to change a search. */
.gl-tools {
  position: sticky; top: 0; z-index: 5;
  margin: 0 0 var(--s-5); padding: var(--s-3) 0 var(--s-2);
  background: linear-gradient(var(--bg) 78%, transparent);
  backdrop-filter: blur(6px);
}
.gl-search { display: block; }
.gl-search input {
  width: 100%;
  /* 16px is the iOS floor — anything smaller and Safari zooms the page on
     focus, which on this page means landing mid-glossary at 1.4x. */
  font: 500 16px/1.2 var(--f-body);
  padding: .7rem .9rem; min-height: var(--tap);
  color: var(--text); background: var(--plate-2);
  border: 1px solid var(--hair); border-radius: var(--r-md);
}
.gl-search input::placeholder { color: var(--text-3); }
.gl-search input:focus-visible { outline: 2px solid var(--ember-3); outline-offset: 1px; }
.gl-count { margin: var(--s-2) 0 0; font: 500 var(--fs-xs)/1 var(--f-mono); color: var(--ember-2); min-height: 1em; }

/* One row that scrolls sideways, never a block that grows downward — this bar
   is sticky, so every line it wraps to is a line stolen from the reader. */
.gl-nav {
  display: flex; flex-wrap: nowrap; gap: .35rem; margin-top: var(--s-2);
  overflow-x: auto; scrollbar-width: none;
  scroll-snap-type: x proximity;
  /* A fade at the trailing edge, not a negative-margin bleed. The bleed
     assumed the nav's padding matched the section's; it did not, so the first
     chip was cut in half. This says "there is more" without touching layout. */
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 2.5rem), transparent);
          mask-image: linear-gradient(90deg, #000 calc(100% - 2.5rem), transparent);
}
.gl-nav::-webkit-scrollbar { display: none; }
.gl-nav a { flex: 0 0 auto; scroll-snap-align: start; }
.gl-nav a {
  display: inline-flex; align-items: center; min-height: var(--tap);
  padding: 0 .7rem; border-radius: 999px;
  font: 600 var(--fs-2xs)/1 var(--f-body);
  color: var(--text-2); background: var(--plate-2);
  border: 1px solid var(--hair); text-decoration: none;
  transition: color var(--t-1) var(--ease), border-color var(--t-1) var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .gl-nav a:hover { color: var(--text); border-color: var(--ember-3); }
}
/* The sticky bar would otherwise land ON the heading it just jumped to. */
.gl-sec { scroll-margin-top: 8.5rem; }
.gl-sec > h2 { margin-bottom: var(--s-1); }
.gl-intro { margin: 0 0 var(--s-3); color: var(--text-3); font-size: var(--fs-sm); }

.gloss { margin: 0 0 var(--s-6); }
.gloss-row {
  padding: var(--s-2) 0;
  border-top: 1px solid var(--hair);
}
/* No content-visibility here, deliberately. It is the right call on the combo
   rows, which carry three portraits each. These rows are text: skipping their
   layout saves almost nothing, while contain-intrinsic-size has to guess a
   height for all 175 — and any guess is wrong at some viewport, which makes
   the scrollbar jump as you scroll and the document height report false. */
.gloss-row > dt {
  font: 700 var(--fs-sm)/1.3 var(--f-body); color: var(--text);
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .45rem;
}
.gl-kind {
  font: 500 var(--fs-2xs)/1 var(--f-mono);
  letter-spacing: .04em; color: var(--ember-2);
  padding: .2rem .45rem; border-radius: var(--r-sm);
  background: color-mix(in oklab, var(--ember-3) 12%, transparent);
  white-space: nowrap;
}
.gloss-row > dd {
  margin: .3rem 0 0; color: var(--text-2);
  font-size: var(--fs-sm); line-height: 1.55;
}
/* Two columns only once the definition can hold a readable measure. Below
   this the term sits above its definition, which is how the phone reads it. */
@media (min-width: 720px) {
  .gloss-row {
    display: grid; grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
    gap: 0 var(--s-4); align-items: start;
  }
  .gloss-row > dt { display: block; }
  .gl-kind { display: inline-block; margin-top: .25rem; }
  .gloss-row > dd { margin: 0; }
}
.gl-empty { color: var(--text-3); font-style: italic; padding: var(--s-5) 0; }

/* Tables are the one thing that must be allowed to scroll sideways rather
   than force the page to. */
.data-table { display: table; }
@media (max-width: 620px) {
  .rank-block, .guide-body { overflow-x: auto; -webkit-overflow-scrolling: touch; }
}

/* -- stat bar (about, hero, index, meta) --------------------------------- */
/* minmax(120px,...) forced three cards to 360px+ inside a 358px column on a
   390px phone, so the third was clipped off the right edge. minmax(0,...) with
   an explicit 3-up lets them shrink instead. */
/* 2-up on a phone. minmax(120px,...) forced three cards past the 358px content
   column and clipped the third; three columns then left a fourth card stranded
   alone on a second row. Two even rows is the only tidy answer at this width. */
.top-stat-bar {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-2); margin: var(--s-4) 0;
}
@media (min-width: 620px) { .top-stat-bar { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: var(--s-3); } }
.top-stat-bar strong { font-size: var(--fs-xl); }
@media (min-width: 620px) { .top-stat-bar strong { font-size: var(--fs-2xl); } }
.top-stat-bar > div {
  background: var(--plate); border-radius: var(--r-sm);
  box-shadow: var(--lift-1); padding: var(--s-3) var(--s-4);
}
.top-stat-bar strong {
  display: block;
  font: 700 var(--fs-2xl)/1 var(--f-mono);
  font-variant-numeric: tabular-nums; letter-spacing: -.03em;
  color: var(--ember-1);
}
.top-stat-bar span {
  display: block; margin-top: var(--s-1);
  font: 700 var(--fs-2xs)/1.2 var(--f-body);
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-3);
}

/* -- hero pair cards (hero, meta, 404) ----------------------------------- */
.hero-pairs { display: grid; gap: var(--s-2); grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
.hero-pair {
  display: flex; align-items: center; gap: var(--s-3);
  background: var(--plate); border-radius: var(--r-sm);
  box-shadow: var(--lift-1); padding: var(--s-2);
  text-decoration: none; color: var(--text);
  min-height: var(--tap);
}
/* CROP GEOMETRY - the rule these all follow.
   The sources are SQUARE. object-fit:cover crops along whichever axis
   overflows, so a box TALLER than wide (e.g. 44x50) makes cover scale by
   height, the vertical overflow is 0px, and object-position's Y value does
   nothing at all - only the sides get trimmed and the gold nameplate stays
   fully visible. Every one of these boxes used to be taller than wide, which
   is why hero-pair and wishlist cards still printed the hero's name twice.
   A box WIDER than tall (h/w ~0.78) makes cover scale by width, so the
   vertical overflow is real and object-position:50% 18% trims the plate. */
.hero-pair img {
  width: 50px; height: 39px; flex: 0 0 auto;
  object-fit: cover; object-position: 50% 18%;
  border-radius: 26% 26% var(--r-xs) var(--r-xs);
  border: 1px solid var(--hair);
}
.hero-pair h4 { margin: 0; font: 600 var(--fs-sm)/1.2 var(--f-body); }
.hero-pair small { color: var(--text-3); font-size: var(--fs-2xs); }
@media (hover: hover) and (pointer: fine) { .hero-pair:hover { background: var(--plate-2); } }

/* -- "how these scores work" --------------------------------------------- */
/* Its own line, not inline in the prose.
   This was inline-flex with min-height:var(--tap). A 44px-tall inline box
   sitting inside a paragraph makes the line box it lands in 44px tall, so the
   note's last line was shoved down by a stray gap and the text broke mid-phrase
   above it ("Where it / gives ground:"). The tap target was right; putting it
   in the middle of a sentence was not. As a block it keeps the full 44px and
   leaves the paragraph to set its own leading. */
.why-method {
  display: flex; width: fit-content; align-items: center;
  min-height: var(--tap);
  margin-top: var(--s-1);
  font: 600 var(--fs-xs)/1.2 var(--f-body);
  color: var(--ember-1); text-decoration: none;
}
.why-method span, .why-method { border-bottom: 0; }
.why-method::after { content: ""; }
/* Inside a ranked row the summary IS the reason, clamped to one line. It sits
   in the text column rather than on a row of its own. */
.cli-main .combo-why { margin-top: 3px; }
.cli-main .combo-why > summary {
  cursor: pointer; list-style: none;
  font: 400 var(--fs-xs)/1.4 var(--f-body); color: var(--text-3);
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cli-main .combo-why > summary::-webkit-details-marker { display: none; }
.cli-main .combo-why[open] > summary { white-space: normal; color: var(--text-2); margin-bottom: var(--s-2); }
.cli-main .combo-why p { color: var(--text-2); font-size: var(--fs-sm); margin: 0; }
/* A note that is a single sentence has nothing to disclose, so it renders as
   plain text rather than a disclosure that opens onto an empty box. Same size
   and colour as a closed summary, but it wraps instead of being clipped —
   there would be no way to reveal the rest of it. */
.cli-main .combo-why-flat {
  display: block; margin-top: 3px;
  font: 400 var(--fs-xs)/1.4 var(--f-body); color: var(--text-3);
}

/* Standalone (podium) disclosure keeps its own label and tap target. */
.podium-why { margin-top: var(--s-2); }
.combo-why > summary::marker { color: var(--ember-3); }

/* -- 404 ------------------------------------------------------------------ */
.nf-wrap { max-width: 760px; }
.nf-code {
  font: 700 clamp(4rem, 18vw, 8rem)/.9 var(--f-mono);
  letter-spacing: -.05em; margin: 0;
  background: var(--molten); -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.nf-lede { margin-bottom: var(--s-5); }
.nf-path {
  font-family: var(--f-mono); font-size: .9em;
  background: var(--plate-2); color: var(--ember-1);
  padding: 2px 6px; border-radius: var(--r-xs);
}
.nf-guess { margin: var(--s-5) 0; }
.nf-search {
  display: flex; gap: var(--s-2); align-items: center;
  background: var(--plate); border: 1px solid var(--hair);
  border-radius: var(--r-sm); padding: var(--s-2); margin: var(--s-5) 0;
}
.nf-search input {
  flex: 1; border: 0; background: transparent; color: var(--text);
  font-size: var(--fs-md); min-height: var(--tap); outline: none; padding: 0 var(--s-2);
}
.nf-links { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.nf-link {
  display: inline-flex; align-items: center; min-height: var(--tap);
  padding: 0 var(--s-4);
  background: var(--plate); border: 1px solid var(--hair); border-radius: var(--r-sm);
  color: var(--text-2); text-decoration: none; font: 600 var(--fs-sm)/1 var(--f-body);
}
@media (hover: hover) and (pointer: fine) { .nf-link:hover { color: var(--text); background: var(--plate-2); } }

/* ==========================================================================
   8d. THE RANKED LIST
   roc_combo_li() in app/view.php emits this markup on FIVE page types —
   /cavalry-combos and siblings, hero pages, /meta and /search. One component,
   five surfaces: it is the highest-leverage thing in the sheet.

   The rank numeral is the most important glyph on the row, so it is mono with
   tabular figures. In the old sheet it was set in Cinzel, which has no tabular
   figures — "7" and "188" were different widths and a column never aligned.
   ========================================================================== */
.rank-list { list-style: none; margin: 0; padding: 0; }
.rank-list > li {
  /* rank | faces | text | controls, with "why" spanning underneath. */
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  grid-template-areas: "rk faces main side" "why why why why";
  align-items: center;
  gap: var(--s-2) var(--s-3);
  padding: var(--s-3) var(--s-3) var(--s-3) var(--s-4);
  background: var(--plate);
  border-bottom: 1px solid var(--hair-soft);
  position: relative;
  /* A cavalry page carries 221 of these. content-visibility lets the browser
     skip layout and paint for the ones off screen while they stay in the DOM
     and the accessibility tree — so crawlers and Ctrl-F still see everything,
     but the browser stops doing 221 rows of work up front. contain-intrinsic-
     size keeps the scrollbar honest by reserving each row's height. */
  content-visibility: auto;
  contain-intrinsic-size: auto 128px;
}
.rank-list > li:first-child { border-radius: var(--r-md) var(--r-md) 0 0; }
.rank-list > li:last-child  { border-radius: 0 0 var(--r-md) var(--r-md); border-bottom: 0; }

/* The heat rail. Ranks 1-3 run hot, the rest cool — the ordinal cue you can
   read peripherally, without stopping to parse a number. */
.rank-list > li::before {
  content: ""; position: absolute; left: 0; top: var(--s-3); bottom: var(--s-3);
  width: 3px; border-radius: 0 2px 2px 0; background: var(--heat-cold);
}
.rank-list > li:nth-child(-n+3)::before { background: var(--heat-hot); }
.rank-list > li:nth-child(n+4):nth-child(-n+12)::before { background: var(--heat-mid); }

.rk {
  flex: 0 0 auto; min-width: 2.6ch; text-align: right;
  font: 700 var(--fs-lg)/1.35 var(--f-mono);
  font-variant-numeric: tabular-nums; letter-spacing: -.03em;
  color: var(--text-3);
}
.rank-list > li:nth-child(-n+3) .rk { color: var(--ember-1); }

.rank-list > li > .rk        { grid-area: rk; }
.rank-list > li > .faces     { grid-area: faces; }
.rank-list > li > .cli-main  { grid-area: main; min-width: 0; }
.rank-list > li > .cli-side  { grid-area: side; display: flex; align-items: center; gap: var(--s-2); }
.rank-list > li > .combo-why { grid-area: why; }
.cli-side .tier-chip { font-style: normal; white-space: nowrap; }
@media (max-width: 560px) { .cli-side .tier-chip { display: none; } }

/* -- score badge ----------------------------------------------------------
   The figure the note already states, pulled out where it can be scanned.
   Tabular numerals so a column of them lines up, and the same three heat
   bands as the tier rail so the colour means one consistent thing. */
.score {
  display: inline-flex; align-items: baseline; gap: 1px;
  padding: 5px 7px; border-radius: var(--r-sm);
  background: var(--plate-2); border: 1px solid var(--hair);
  font-family: var(--f-mono); font-variant-numeric: tabular-nums;
  line-height: 1; white-space: nowrap;
}
.score b     { font: 700 var(--fs-sm)/1 var(--f-mono); }
.score small { font-size: 9px; color: var(--text-3); }
.score.s-hi  b { color: var(--ember-1); }
.score.s-hi    { border-color: color-mix(in oklab, var(--ember-1) 40%, transparent); }
.score.s-mid b { color: var(--ember-2); }
.score.s-lo  b { color: var(--text-2); }

.combo-names { display: block; font: 600 var(--fs-md)/1.35 var(--f-body); color: var(--text); }
.combo-names a { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; }
@media (hover: hover) and (pointer: fine) { .combo-names a:hover { color: var(--ember-1); border-bottom-color: var(--ember-line); } }

/* Portrait thumbs inside a row. object-position crops the baked-in gold
   nameplate off the bottom of every portrait, so the row does not print the
   hero's name twice. */
.faces { display: flex; gap: var(--s-1); margin-top: var(--s-2); }
.face { position: relative; display: block; line-height: 0; }
.faces img {
  width: 40px; height: 31px; object-fit: cover; object-position: 50% 18%;
  border-radius: 24% 24% var(--r-xs) var(--r-xs);
  border: 1px solid var(--hair); background: var(--void);
}
/* Front / Middle / Back, on the portrait rather than on a line of its own.
   This replaced a whole text row that re-listed all three hero names to
   deliver three letters. Overlaid, it costs zero vertical space — which
   matters when the page carries 221 of these. */
.slot {
  position: absolute; left: 2px; bottom: 2px;
  font: 700 9px/1 var(--f-mono); font-style: normal; letter-spacing: .03em;
  color: var(--text); background: rgba(10, 10, 12, .78);
  padding: 2px 3px; border-radius: var(--r-xs);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}

.share-btn {
  flex: 0 0 auto; min-height: var(--tap); min-width: var(--tap);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  padding: 0 var(--s-3);
  background: transparent; color: var(--text-3);
  border: 1px solid var(--hair); border-radius: var(--r-sm);
  font: 600 var(--fs-xs)/1 var(--f-body); cursor: pointer;
}
.share-btn.copied { color: var(--ok); border-color: var(--ok); }
@media (hover: hover) and (pointer: fine) { .share-btn:hover { color: var(--text); background: var(--plate-2); } }

/* On a phone the row was ~200px tall: the names wrapped to three lines because
   the faces and share button took ~120px of a 358px column, and the position
   line wrapped under them. Shrinking the faces and clamping the position line
   to one line roughly halves the row height, which matters on a page that
   carries 221 of them. */
@media (max-width: 560px) {
  /* Faces move UNDER the text on a phone. Three 40px portraits, the rank and
     the share button together took ~190px of a 358px row, so the names wrapped
     to two lines and the reason line was clipped to about thirty characters.
     Dropping the faces to their own row gives the text the full width: the
     names fit on one line and the reason is actually readable. */
  .rank-list > li {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "rk main side" "rk faces faces";
    align-items: start;
    padding: var(--s-3) var(--s-2) var(--s-3) var(--s-3);
    gap: var(--s-2);
  }
  .faces img { width: 34px; height: 27px; }
  .slot { font-size: 8px; padding: 1px 3px; }
  .combo-names { font-size: var(--fs-sm); }
}

/* Tier separator — the one place the heat ramp is spelled out in words. */
.tier-break {
  display: block !important;
  background: var(--void) !important;
  border: 0 !important;
  padding: var(--s-6) 0 var(--s-2) !important;
}
.tier-break::before { display: none; }
.tier-break h3 {
  margin: 0; font-size: var(--fs-lg);
  display: flex; align-items: center; gap: var(--s-3);
}
.tier-break h3::before {
  content: ""; width: 3px; height: 1.1em; border-radius: 1px;
  background: var(--heat-mid); flex: 0 0 auto;
}
.tier-break p { margin: var(--s-2) 0 0; color: var(--text-3); font-size: var(--fs-sm); }

/* -- filter bar ---------------------------------------------------------- */
.combo-filter-bar {
  display: flex; align-items: center; gap: var(--s-3);
  margin: var(--s-4) 0 var(--s-3);
}
.combo-filter-bar input {
  flex: 1; min-height: var(--tap); padding: 0 var(--s-4);
  background: var(--plate); color: var(--text);
  border: 1px solid var(--hair); border-radius: var(--r-sm);
  font-size: var(--fs-md);            /* 16px floor — stops iOS zooming on focus */
}
.combo-filter-bar input::placeholder { color: var(--text-3); }
.filter-count {
  font: 500 var(--fs-xs)/1 var(--f-mono); color: var(--text-3);
  white-space: nowrap;
}

/* -- podium (top 3 on a category page) ----------------------------------- */
.podium { display: grid; gap: var(--s-4); grid-template-columns: 1fr; margin: var(--s-4) 0 var(--s-6); }
/* auto-fit, not a hard flip at 760px. The old rule took the podium portraits
   from 219px to 57px across a single pixel of viewport width — the worst image
   jump on the site. auto-fit keeps every card above a usable width instead. */
@media (min-width: 640px) { .podium { grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); } }
/* Each card sizes to its OWN content.
   A grid's default align-items is stretch, so all three cards were forced to
   the height of the tallest. Opening one card's "Why this lineup works" grew
   that card and the grid stretched the other two to match — which read exactly
   like all three had opened, with two of them showing nothing. They had not
   opened; they had been inflated. */
.podium { align-items: start; }

/* The rank badge and the score share a line above the portraits. */
.medal-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-2); margin-bottom: var(--s-3);
}
.medal-row .medal { margin-bottom: 0; }
.podium > article {
  background: var(--plate); border-radius: var(--r-md);
  box-shadow: var(--lift-2); padding: var(--s-4);
  position: relative; overflow: hidden;
}
.podium > article::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 3px; background: var(--heat-hot);
}
.podium > article:nth-child(2)::before { background: var(--heat-mid); }
.podium > article:nth-child(3)::before { background: var(--heat-cold); }
.medal {
  display: inline-block; margin-bottom: var(--s-3);
  font: 700 var(--fs-2xs)/1 var(--f-body);
  letter-spacing: .1em; text-transform: uppercase;
  padding: 6px 9px; border-radius: var(--r-xs);
  background: var(--molten); color: var(--ember-ink);
  clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 6px, 100% 100%, 0 100%);
}
.podium > article:nth-child(2) .medal { background: var(--heat-mid); }
.podium > article:nth-child(3) .medal { background: var(--plate-3); color: var(--heat-cold-text); }

.trio { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-2); }
.trio a {
  display: block; position: relative; overflow: hidden; aspect-ratio: 1;
  border-radius: 34% 34% var(--r-sm) var(--r-sm);
  border: 1px solid var(--hair); background: var(--void);
}
.trio img {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 126%; max-width: none; height: auto; display: block;
}
.podium h3 { font-size: var(--fs-md); margin: var(--s-3) 0 var(--s-2); text-align: center; }
.podium-why { margin-top: var(--s-2); }
.podium-why > summary {
  cursor: pointer; min-height: var(--tap); display: flex; align-items: center;
  font: 600 var(--fs-xs)/1 var(--f-body); color: var(--text-2);
}
.podium-why > summary::marker { color: var(--ember-3); }
.podium-why p { font-size: var(--fs-sm); color: var(--text-2); }

/* -- sibling-category nav ------------------------------------------------ */
.rank-cat-nav { display: flex; flex-wrap: wrap; gap: var(--s-2); margin: var(--s-4) 0; }
.rank-cat-nav a {
  display: inline-flex; align-items: center; min-height: var(--tap); padding: 0 var(--s-4);
  background: var(--plate); border: 1px solid var(--hair); border-radius: var(--r-sm);
  color: var(--text-2); text-decoration: none; font: 600 var(--fs-sm)/1 var(--f-body);
}
.rank-cat-nav a[aria-current="page"] { background: var(--molten); border-color: transparent; color: var(--ember-ink); font-weight: 700; }
@media (hover: hover) and (pointer: fine) { .rank-cat-nav a:hover { color: var(--text); background: var(--plate-2); } }

/* -- page-level action row ----------------------------------------------- */
.hero-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); margin: var(--s-4) 0; }

/* -- load more -----------------------------------------------------------
   Emitted by combos.php's inline script AND by app.js, so it appears in no
   template grep. */
.load-more-wrap { display: flex; justify-content: center; margin: var(--s-5) 0; }
.load-more-wrap button { min-width: 220px; }

/* ==========================================================================
   8e. TROOP TYPE — pigment, never rank
   These bare class names (.cavalry, .archer, .footmen, .mixed) are applied as
   modifiers by heroes.php and hero.php. Lightness is separated as well as hue,
   so the four survive colour-blindness without relying on hue alone.
   ========================================================================== */
.type-dot {
  width: 9px; height: 9px; border-radius: 50%;
  display: inline-block; flex: 0 0 auto;
  background: var(--text-3);
}
.type-dot.cavalry { background: var(--cav); }
.type-dot.archer  { background: var(--arc); }
.type-dot.footmen { background: var(--foo); }
.type-dot.mixed   { background: var(--mix); }

/* ==========================================================================
   8f. HERO DIRECTORY  (/heroes, /search)
   ========================================================================== */
.hx-toolbar {
  display: flex; flex-wrap: wrap; gap: var(--s-3);
  margin: var(--s-4) 0 var(--s-3);
}
.hx-search {
  flex: 1 1 260px; display: flex; align-items: center; gap: var(--s-2);
  background: var(--plate); border: 1px solid var(--hair); border-radius: var(--r-sm);
  padding: 0 var(--s-3); min-height: var(--tap);
}
.hx-search-ic { color: var(--text-3); }
.hx-search input {
  flex: 1; border: 0; background: transparent; color: var(--text);
  font-size: var(--fs-md);          /* 16px floor: below this iOS zooms the page */
  min-height: var(--tap); outline: none;
}
.hx-search input::placeholder { color: var(--text-3); }
.hx-sort select {
  min-height: var(--tap); padding: 0 var(--s-3);
  background: var(--plate); color: var(--text);
  border: 1px solid var(--hair); border-radius: var(--r-sm);
  font-size: var(--fs-md);          /* same floor — a <select> under 16px zooms too */
}
.hx-filters { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-bottom: var(--s-3); }
.hx-chip {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: var(--tap); padding: 0 var(--s-4);
  background: var(--plate); border: 1px solid var(--hair); border-radius: var(--r-sm);
  color: var(--text-2); font: 600 var(--fs-sm)/1 var(--f-body); cursor: pointer;
}
.hx-chip[aria-pressed="true"] {
  background: var(--molten); border-color: transparent;
  color: var(--ember-ink); font-weight: 700;
}
.hx-chip[aria-pressed="true"] .type-dot { background: var(--ember-ink); }
.hx-count { font: 500 var(--fs-xs)/1 var(--f-mono); color: var(--text-3); margin-bottom: var(--s-3); display: block; }
.hx-empty { padding: var(--s-7) var(--s-4); text-align: center; color: var(--text-3); }

.hero-index-grid {
  display: grid; gap: var(--s-3);
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
@media (max-width: 480px) { .hero-index-grid { grid-template-columns: repeat(2, 1fr); gap: var(--s-2); } }

.hero-index-card {
  display: flex; flex-direction: column;
  background: var(--plate); border-radius: var(--r-md);
  box-shadow: var(--lift-1);
  text-decoration: none; color: var(--text);
  overflow: hidden;
  transition: background var(--t-2) var(--ease);
}
/* Same square-source crop as the picker — see .hcard-img for why object-fit
   cannot do this. Square frame, image at 126% width pinned to the top. */
.hi-img { position: relative; background: var(--void); overflow: hidden; aspect-ratio: 1; }
.hi-img img {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 126%; max-width: none; height: auto; display: block;
  filter: saturate(.92);
}
/* Foot-fade: all 131 cards resolve to the same bottom edge, so the grid reads
   as one set instead of 131 competing rectangles. */
.hi-img::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 52%, rgba(10, 10, 12, .88));
}
.hi-body { padding: var(--s-3); display: flex; flex-direction: column; gap: var(--s-2); flex: 1; }
.hi-body h3 { margin: 0; font: 600 var(--fs-sm)/1.2 var(--f-body); font-family: var(--f-body); }
/* A three-type hero (Ragnar is Cavalry + Archer + Footmen) wrapped this list
   to two lines. Grid rows are equal height, so ONE such card made its whole
   row taller — across 66 rows that is most of why this page measured 20,500px
   on a phone. Tighter gaps fit all three on one line inside a 176px card. */
.hi-types { display: flex; flex-wrap: wrap; gap: 2px var(--s-2); min-width: 0; }
.hi-type {
  display: inline-flex; align-items: center; gap: 4px; min-width: 0;
  font: 600 var(--fs-2xs)/1.35 var(--f-body); color: var(--text-3);
  white-space: nowrap;
}
.hi-combos {
  font-family: var(--f-mono); font-variant-numeric: tabular-nums;
  font-size: var(--fs-2xs); color: var(--ember-1);
}
/* space-between with no shrink allowance pushed "Stats" past the card's
   padding, and .hero-index-card clips overflow — so on every multi-type card
   the badge was rendered cut in half. It shrinks now, and wraps before it
   truncates. */
.hi-foot {
  margin-top: auto; display: flex; flex-wrap: wrap;
  align-items: center; justify-content: space-between; gap: 2px var(--s-2);
  min-width: 0;
}
.hi-combos { min-width: 0; }
.hi-flag {
  font: 700 var(--fs-2xs)/1 var(--f-body); letter-spacing: .06em; text-transform: uppercase;
  color: var(--heat-cold-text);
  display: inline-flex; align-items: center; gap: 3px;
  min-width: 0; flex-shrink: 1;
}
.hi-flag svg { flex: 0 0 auto; }
@media (hover: hover) and (pointer: fine) {
  .hero-index-card:hover { background: var(--plate-2); }
  .hero-index-card:hover .hi-img img { filter: saturate(1); }
}
.hero-index-card:active { transform: translateY(1px); }

/* -- search page --------------------------------------------------------- */
.search-page-form { display: flex; gap: var(--s-2); margin: var(--s-4) 0; }
.search-page-form input {
  flex: 1; min-height: var(--tap); padding: 0 var(--s-4);
  background: var(--plate); color: var(--text);
  border: 1px solid var(--hair); border-radius: var(--r-sm);
  font-size: var(--fs-md);
}
.search-page-form input::placeholder { color: var(--text-3); }
.types { display: flex; flex-wrap: wrap; gap: var(--s-2); }

/* ==========================================================================
   8g. HERO PAGE
   ========================================================================== */
/* -- hero band -----------------------------------------------------------
   Portrait and copy as two balanced columns. The portrait column sets its own
   height, so there is no dead void under the copy the way there was when the
   aside carried both the portrait AND the best-combo card. */
.hero-band {
  display: grid; gap: var(--s-5);
  grid-template-columns: 1fr;
  max-width: var(--container); margin-inline: auto;
  padding: var(--s-5) var(--s-4) var(--s-6);
  align-items: center;
}
@media (min-width: 900px) {
  .hero-band { grid-template-columns: 380px 1fr; gap: var(--s-7); padding-block: var(--s-6) var(--s-7); }
}
.hero-band-copy { min-width: 0; order: 2; }
.hero-band-art  { order: 1; }
@media (min-width: 900px) { .hero-band-art { order: 1; } .hero-band-copy { order: 2; } }

.hero-band h1 { font-size: clamp(2rem, 5vw, 3.2rem); margin: var(--s-2) 0 var(--s-3); }
/* The " — Rise of Castles Combos" half is for search, not for the reader; it
   should not compete with the hero's name. */
.hero-band .h1-sub { display: block; font-size: .44em; color: var(--text-3); font-weight: 600; letter-spacing: 0; margin-top: 6px; }
.hero-band .hero-lede { margin-bottom: var(--s-4); }
/* The hero band carries THREE stats, so the homepage's 2-up rule stranded the
   third on a row of its own. Three short figures fit one row at 390px. */
.hero-band .top-stat-bar { max-width: 520px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 380px) { .hero-band .top-stat-bar span { font-size: 9.5px; letter-spacing: .06em; } }
.hero-band .hero-actions { margin-top: var(--s-4); }

.hero-band-art {
  position: relative; border-radius: var(--r-lg); overflow: hidden;
  aspect-ratio: 1 / 1.02;
  background: var(--plate); box-shadow: var(--lift-3);
  border: 1px solid rgba(255, 194, 77, .18);
  /* CAP THE WIDTH. Below 900px this is a single full-width column, so at 768 it
     stretched to 736px and the 131% crop asked a 512px source to paint 962px —
     a 1.88x upscale on the largest image of the page. 512 is the biggest source
     that exists, so the only fix is to stop the frame growing past it. */
  max-width: var(--art-max, 420px); margin-inline: auto; width: 100%;
}
/* --art-max is set per hero by hero.php from the portrait's TRUE width, so the
   frame never asks a 250px source to paint 380px. */
@media (min-width: 900px) { .hero-band-art { margin-inline: 0; } }
/* 1.02 / 0.78 = 1.31 — see .la-card for why this ratio has to track the frame. */
.hero-mug {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 131%; max-width: none; height: auto; display: block;
}
/* Ember rim-light plus a fade into the plate, so the portrait reads as part of
   the surface rather than a pasted square. */
.hero-band-art::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, transparent 52%, rgba(10, 10, 12, .92));
  box-shadow: inset 0 0 60px 14px rgba(255, 110, 50, .14);
}
.hero-type-badges { position: absolute; left: var(--s-3); bottom: var(--s-3); z-index: 2; display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* -- best combo, promoted out of the sidebar ------------------------------ */
.best-combo-section { padding-top: 0; }
.best-combo-section .showcase-combo { max-width: 560px; }
.hero-type-badge {
  display: inline-flex; align-items: center; gap: 6px;
  font: 700 var(--fs-2xs)/1 var(--f-body); letter-spacing: .1em; text-transform: uppercase;
  padding: 6px 9px; border-radius: var(--r-xs);
  background: rgba(10, 10, 12, .78); color: var(--text);
  border: 1px solid rgba(255, 255, 255, .12);
}

.showcase-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3);
  margin: var(--s-4) 0 var(--s-2);
}
.showcase-head strong { font: 600 var(--fs-lg)/1 var(--f-display); }
.showcase-live {
  font: 700 var(--fs-2xs)/1 var(--f-body); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ember-2);
}
.showcase-combo { background: var(--plate-2); border-radius: var(--r-md); padding: var(--s-3); box-shadow: var(--lift-1); }
.showcase-row { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); margin-bottom: var(--s-3); }
.showcase-row h2 { margin: 0; font-size: var(--fs-md); font-family: var(--f-body); font-weight: 600; }
.rank-badge {
  flex: 0 0 auto;
  font: 700 var(--fs-2xs)/1 var(--f-mono); letter-spacing: .04em;
  padding: 6px 8px; border-radius: var(--r-xs);
  background: var(--molten); color: var(--ember-ink);
  clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 6px, 100% 100%, 0 100%);
}
.showcase-slots { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-2); }
.showcase-slot { margin: 0; min-width: 0; }
.showcase-slot a {
  display: block; position: relative; overflow: hidden; aspect-ratio: 1;
  border-radius: 32% 32% var(--r-sm) var(--r-sm);
  border: 1px solid var(--hair); background: var(--void);
}
.showcase-slot img {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 126%; max-width: none; height: auto; display: block;
}
.showcase-slot span {
  display: block; margin-top: var(--s-1); text-align: center;
  font: 600 var(--fs-2xs)/1.2 var(--f-body); color: var(--text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.showcase-note { font-size: var(--fs-sm); color: var(--text-3); margin-top: var(--s-3); }

.hero-about { max-width: var(--container); margin-inline: auto; }
.hero-about-body p { color: var(--text-2); }
.hero-about-body a { color: var(--ember-1); }

/* ==========================================================================
   8h. CONTACT
   A progressive form: step 1 picks an intent, step 2 shows only the fields that
   intent needs. Every step stays in the DOM and is toggled with the `hidden`
   attribute, so the [hidden] rule at the end of this file is what keeps steps 2
   and 3 from rendering on top of each other.
   ========================================================================== */
.ct-hero { padding-bottom: var(--s-4); }
.ct-section { max-width: var(--container); margin-inline: auto; padding: var(--s-5) var(--s-4); }
.ct-layout { display: grid; gap: var(--s-5); grid-template-columns: 1fr; }
@media (min-width: 920px) { .ct-layout { grid-template-columns: 1fr 320px; align-items: start; } }
.ct-main { min-width: 0; }

.ct-step { margin-bottom: var(--s-6); }
.ct-step-head { display: flex; align-items: center; gap: var(--s-3); margin-bottom: var(--s-4); }
.ct-step-head h2 { margin: 0; font-size: var(--fs-xl); }
.ct-step-n {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--molten); color: var(--ember-ink);
  font: 700 var(--fs-sm)/1 var(--f-mono);
}

/* -- intent picker ------------------------------------------------------- */
.intent-grid { display: grid; gap: var(--s-3); grid-template-columns: 1fr; }
@media (min-width: 620px) { .intent-grid { grid-template-columns: 1fr 1fr; } }
.intent-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-2);
  text-align: left; cursor: pointer;
  padding: var(--s-4);
  min-height: var(--tap);
  background: var(--plate); color: var(--text);
  border: 1px solid var(--hair); border-radius: var(--r-md);
  border-left: 3px solid var(--hair);
  transition: border-color var(--t-2) var(--ease), background var(--t-2) var(--ease);
}
.intent-card[aria-pressed="true"] { border-left-color: var(--ember-3); background: var(--plate-2); }
.intent-card:active { transform: translateY(1px); }
@media (hover: hover) and (pointer: fine) { .intent-card:hover { background: var(--plate-2); } }
.ic-icon { color: var(--ember-2); }
.ic-icon .ic { width: 22px; height: 22px; }
.ic-title { font: 600 var(--fs-md)/1.25 var(--f-body); }
.ic-blurb { font: 400 var(--fs-sm)/1.45 var(--f-body); color: var(--text-3); }
/* Each intent keeps its own pigment so the chosen one is identifiable at a glance. */
.i-combo   .ic-icon { color: var(--cav); }
.i-ranking .ic-icon { color: var(--arc); }
.i-hero    .ic-icon { color: var(--foo); }
.i-other   .ic-icon { color: var(--mix); }

/* -- fields -------------------------------------------------------------- */
.fld-row { display: grid; gap: var(--s-3); grid-template-columns: 1fr; margin-bottom: var(--s-3); }
@media (min-width: 620px) { .fld-row { grid-template-columns: 1fr 1fr; } }
.fld { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
.fld label { font: 600 var(--fs-xs)/1 var(--f-body); color: var(--text-2); }
.fld input, .fld select, .fld textarea {
  width: 100%; min-height: var(--tap); padding: var(--s-2) var(--s-3);
  background: var(--plate); color: var(--text);
  border: 1px solid var(--hair); border-radius: var(--r-sm);
  font-size: var(--fs-md);          /* 16px floor on every control — no iOS zoom */
}
.fld textarea { min-height: 140px; resize: vertical; line-height: 1.6; }
.fld input::placeholder, .fld textarea::placeholder { color: var(--text-3); }
.fld input:focus, .fld select:focus, .fld textarea:focus { border-color: var(--ember-3); }
.fld-hint { font: 400 var(--fs-2xs)/1.4 var(--f-body); color: var(--text-3); }

.ct-consent {
  display: flex; align-items: flex-start; gap: var(--s-3);
  margin: var(--s-4) 0; min-height: var(--tap);
  font-size: var(--fs-sm); color: var(--text-2);
}
.ct-consent input { width: 20px; height: 20px; margin-top: 2px; accent-color: var(--ember-3); flex: 0 0 auto; }
.ct-verify { margin: var(--s-4) 0; }
.ct-actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-4); }
.ct-back { background: none; border: 0; color: var(--text-3); cursor: pointer;
  min-height: var(--tap); font: 600 var(--fs-sm)/1 var(--f-body); }
.ct-back:hover { color: var(--text); }
/* The two facts above the form: reply time, and the email address.
   The address is a standalone control, not a link inside a sentence, so the
   WCAG 2.5.8 inline-text exception does not cover it — it was a 178x16 target,
   about a third the height a thumb needs. Making the row a flex line lets the
   link carry a real 44px height without pushing the text off its baseline. */
.ct-meta {
  font-size: var(--fs-xs); color: var(--text-3); margin-top: var(--s-3);
  display: flex; flex-wrap: wrap; gap: 0 var(--s-4); align-items: center;
}
.ct-meta > span { display: inline-flex; align-items: center; gap: .4rem; min-height: var(--tap); }
.ct-meta a {
  display: inline-flex; align-items: center; min-height: var(--tap);
  padding: 0 .35rem; margin: 0 -.35rem; border-radius: var(--r-sm);
  color: var(--ember-2); text-decoration: none;
  border-bottom: 1px solid color-mix(in oklab, var(--ember-2) 45%, transparent);
}
@media (hover: hover) and (pointer: fine) {
  .ct-meta a:hover { color: var(--ember-1); border-bottom-color: currentColor; }
}

.contact-status { margin-top: var(--s-3); font-size: var(--fs-sm); min-height: 1.3em; }
.contact-status.ok  { color: var(--ok); }
.contact-status.bad { color: var(--bad); }

/* -- done ---------------------------------------------------------------- */
.ct-done { text-align: center; padding: var(--s-7) var(--s-4); background: var(--plate);
  border-radius: var(--r-lg); box-shadow: var(--lift-1); }
.ct-done h2 { margin-top: var(--s-3); }
.ct-done p { color: var(--text-2); }
.ct-done .ct-actions { justify-content: center; }
.ct-done-ic {
  display: inline-grid; place-items: center;
  width: 56px; height: 56px; border-radius: 50%;
  background: var(--bg-ok, rgba(108, 196, 143, .14)); color: var(--ok);
}
.ct-done-ic .ic { width: 28px; height: 28px; }

/* -- sidebar ------------------------------------------------------------- */
.ct-side { display: flex; flex-direction: column; gap: var(--s-3); }
.ct-tip {
  background: var(--plate); border-radius: var(--r-md);
  box-shadow: var(--lift-1); padding: var(--s-4);
  border-left: 3px solid var(--ember-line);
}
.ct-tip p { margin: var(--s-2) 0 0; font-size: var(--fs-sm); color: var(--text-2); }

/* ==========================================================================
   8i. THE GENERATOR (homepage)
   ========================================================================== */

/* -- lander --------------------------------------------------------------
   Two columns from 900px: the pitch and the primary action on the left, the
   current #1 lineup as overlapping portraits on the right. Before this the
   right half of the container was simply empty and the page had no visual
   anchor at all. */
.lander {
  max-width: var(--container); margin-inline: auto;
  padding: var(--s-6) var(--s-4) var(--s-6);
  display: grid; gap: var(--s-6); grid-template-columns: 1fr;
  align-items: center;
}
@media (min-width: 900px) { .lander { grid-template-columns: 1.05fr .95fr; gap: var(--s-7); padding-block: var(--s-8); } }
.lander-copy { min-width: 0; }
.lander h1 { margin: var(--s-2) 0 var(--s-3); }
.lander .hero-lede { margin-bottom: var(--s-5); }
.lander-cta { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-bottom: var(--s-5); }
.lander-cta .btn { flex: 1 1 auto; min-width: 160px; }
@media (min-width: 620px) { .lander-cta .btn { flex: 0 0 auto; } }

/* Inside the lander the column is ~470px, so four boxed stat cards squeezed to
   ~110px each and every label wrapped to two lines. A hairline spec strip fits
   the same four figures on one clean row. */
@media (min-width: 900px) {
  .lander .top-stat-bar {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0; background: none;
  }
  .lander .top-stat-bar > div {
    background: none; box-shadow: none; border-radius: 0;
    padding: 0 var(--s-4);
    border-left: 1px solid var(--hair);
  }
  .lander .top-stat-bar > div:first-child { padding-left: 0; border-left: 0; }
  .lander .top-stat-bar strong { font-size: var(--fs-2xl); }
  .lander .top-stat-bar span { white-space: nowrap; font-size: 10px; letter-spacing: .1em; }
}
/* "own?" was falling to a line of its own. */
@media (min-width: 900px) { .lander h1 { max-width: 15ch; } }

/* -- the lineup montage --------------------------------------------------- */
.lander-art { position: relative; display: none; min-height: 300px; }
@media (min-width: 900px) { .lander-art { display: block; } }

/* Heat bloom behind the stack — the only place on the site with a soft glow,
   because it is decoration rather than a surface. */
/* The montage is clipped so neither the bloom nor a rotated card can push the
   page wider than the viewport — at 1024 the glow's negative side inset was
   reaching 1035px and giving the whole document a horizontal scrollbar. */
.lander-art { overflow: hidden; }
.la-glow {
  position: absolute; inset: -12% 0 -18% 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(48% 46% at 50% 44%, rgba(255, 110, 50, .30), transparent 70%),
    radial-gradient(38% 40% at 68% 62%, rgba(255, 194, 77, .16), transparent 72%);
  filter: blur(6px);
}
.la-stack { position: relative; z-index: 1; display: block; height: 340px; }
/* Crop maths: the source is square and the gold nameplate occupies the bottom
   ~22%, so the visible fraction must be about 0.78. With a frame of aspect
   1/1.08, the image has to be scaled to 1.08 / 0.78 = 1.38 of the frame width.
   At 1/1.16 and 128% the visible fraction was 0.91 and the plate survived —
   which is exactly what the first pass got wrong. */
.la-card {
  position: absolute; top: 50%; width: 190px; aspect-ratio: 1 / 1.08;
  border-radius: 40% 40% var(--r-md) var(--r-md);
  overflow: hidden; background: var(--void);
  border: 1px solid rgba(255, 194, 77, .28);
  box-shadow: 0 18px 44px rgba(0, 0, 0, .66);
}
.la-card img {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 138%; max-width: none; height: auto; display: block;
}
/* Centre hero forward and upright; flankers tucked behind and tilted. */
.la-0 { left: 2%;  transform: translateY(-50%) rotate(-7deg) scale(.86); z-index: 1; filter: saturate(.85) brightness(.8); }
.la-1 { left: 30%; transform: translateY(-54%);                          z-index: 3; }
.la-2 { left: 58%; transform: translateY(-50%) rotate(7deg)  scale(.86); z-index: 2; filter: saturate(.85) brightness(.8); }

.la-tag {
  position: absolute; z-index: 4; left: 4%; bottom: 2%;
  display: flex; flex-direction: column; gap: 3px;
  font: 600 var(--fs-sm)/1.25 var(--f-body); color: var(--text);
  background: rgba(10, 10, 12, .82);
  border: 1px solid var(--hair); border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-3);
  backdrop-filter: none;
}
.la-tag i {
  font-style: normal; font: 700 var(--fs-2xs)/1 var(--f-body);
  letter-spacing: .12em; text-transform: uppercase; color: var(--ember-1);
}

/* The montage is decorative; never let it animate for someone who asked not to. */
@media (prefers-reduced-motion: no-preference) {
  .la-card { transition: transform var(--t-3) var(--ease); }
  .lander-art:hover .la-0 { transform: translateY(-50%) rotate(-10deg) scale(.88); }
  .lander-art:hover .la-2 { transform: translateY(-50%) rotate(10deg)  scale(.88); }
}

.tool-entry { padding-top: 0; }
.entry-card {
  background: var(--plate); border-radius: var(--r-lg);
  box-shadow: var(--lift-2); padding: var(--s-4);
}
.entry-label {
  font: 700 var(--fs-2xs)/1 var(--f-body); letter-spacing: .15em; text-transform: uppercase;
  color: var(--text-3); margin: 0 0 var(--s-3);
}
.preset-row { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-bottom: var(--s-4); }
.preset {
  min-height: var(--tap); padding: 0 var(--s-4);
  background: var(--plate-2); color: var(--text-2);
  border: 1px solid var(--hair); border-radius: var(--r-sm);
  font: 600 var(--fs-sm)/1 var(--f-body); cursor: pointer;
}
.preset:active { transform: translateY(1px); }
@media (hover: hover) and (pointer: fine) { .preset:hover { color: var(--text); border-color: var(--ember-line); } }
.entry-cta { width: 100%; font-size: var(--fs-md); }
.entry-help { margin: var(--s-3) 0 0; text-align: center; font-size: var(--fs-xs); color: var(--text-3); }

.roster-note {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3);
  background: var(--plate); border-left: 3px solid var(--ember-3);
  border-radius: var(--r-sm); padding: var(--s-3) var(--s-4); margin: 0 0 var(--s-4);
  font-size: var(--fs-sm); color: var(--text-2);
}
.roster-note.is-share { border-left-color: var(--foo); }

/* -- picker sheet -------------------------------------------------------- */
.picker { position: fixed; inset: 0; z-index: var(--z-sheet); }
.picker-scrim { position: absolute; inset: 0; background: rgba(6, 6, 8, .68); opacity: 0; transition: opacity var(--t-3) var(--ease); }
.picker.open .picker-scrim { opacity: 1; }
.picker-sheet {
  position: absolute; left: 0; right: 0; bottom: 0;
  /* dvh, not vh: on iOS the URL bar makes 100vh taller than the visible area,
     which would push the footer action off the bottom of the screen. */
  height: 92dvh; max-height: 92dvh;
  display: flex; flex-direction: column;
  background: var(--void);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  border-top: 1px solid var(--plate-3);
  box-shadow: 0 -18px 46px rgba(0, 0, 0, .6);
  transform: translateY(100%);
  transition: transform var(--t-3) var(--ease);
}
.picker.open .picker-sheet { transform: translateY(0); }
@supports not (height: 92dvh) { .picker-sheet { height: 92vh; max-height: 92vh; } }
@media (min-width: 860px) {
  .picker-sheet { left: 50%; transform: translate(-50%, 100%); width: min(880px, 94vw); height: 88dvh;
                  border-radius: var(--r-lg); bottom: 4dvh; border: 1px solid var(--plate-3); }
  .picker.open .picker-sheet { transform: translate(-50%, 0); }
}

.picker-head { flex: 0 0 auto; padding: 0 var(--s-4) var(--s-3); border-bottom: 1px solid var(--hair); }
.picker-grab { display: block; width: 36px; height: 4px; border-radius: 2px; background: var(--plate-3); margin: var(--s-2) auto var(--s-2); }
.picker-title-row { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); }
.picker-title-row h2 { margin: 0; font-size: var(--fs-lg); }
.picker-x {
  width: var(--tap); height: var(--tap); flex: 0 0 auto;
  display: grid; place-items: center;
  background: transparent; border: 0; color: var(--text-2);
  font-size: 1.6rem; line-height: 1; cursor: pointer; border-radius: var(--r-sm);
}
.picker-x:hover { color: var(--text); background: var(--plate); }

.picker-search {
  display: flex; align-items: center; gap: var(--s-2);
  background: var(--plate); border: 1px solid var(--hair); border-radius: var(--r-sm);
  padding: 0 var(--s-3); min-height: var(--tap); margin-top: var(--s-2);
}
.picker-search input {
  flex: 1; border: 0; background: transparent; color: var(--text);
  font-size: var(--fs-md); min-height: var(--tap); outline: none; min-width: 0;
}
.picker-search input::placeholder { color: var(--text-3); }
.picker-search button { background: none; border: 0; color: var(--text-3); font-size: 1.3rem; cursor: pointer;
  width: 32px; height: 32px; border-radius: var(--r-xs); }

/* Chips scroll sideways rather than wrapping and stealing grid height. */
.picker-chips {
  display: flex; gap: var(--s-2); margin-top: var(--s-2);
  overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.picker-chips::-webkit-scrollbar { display: none; }
.picker-chips .chip { white-space: nowrap; flex: 0 0 auto; }
.picker-chips .chip i { font-style: normal; font-family: var(--f-mono); font-size: var(--fs-2xs); opacity: .75; margin-left: 5px; }
.picker-chips .chip[aria-pressed="true"] { background: var(--molten); border-color: transparent; color: var(--ember-ink); }

.picker-bulk { display: flex; gap: var(--s-2); margin-top: var(--s-2); }

/* THE GRID. 4 across on a phone shows ~20 heroes per screen; the old
   single-column list showed 5, and ran 3,500-9,400px long. */
.hero-grid {
  flex: 1 1 auto; overflow-y: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  display: grid; gap: var(--s-2); align-content: start;
  /* minmax(0,1fr), NOT 1fr. A grid item's default min-width is auto — its
     min-content size — and .hcard-name is white-space:nowrap, so a long hero
     name sets a min-content floor that pushes the columns past the container
     and scrolls the whole sheet sideways. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  padding: var(--s-3) var(--s-4);
}
@media (min-width: 520px) { .hero-grid { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); } }

.hcard {
  display: flex; flex-direction: column; gap: 4px;
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--text-3); text-align: center; position: relative;
  min-width: 0;
}
/* PORTRAIT CROP.
   Every source portrait is SQUARE and has the hero's name burned into a gold
   nameplate across the bottom ~22%. object-fit:cover cannot remove it: with a
   square source in a taller-than-wide box, cover scales to fill the height and
   crops the SIDES, leaving the plate fully visible — which is why every card
   was printing the name twice.
   Cropping the bottom requires scaling the image beyond the frame and clipping.
   Square frame, image at 128% width, pinned to the top: shows the top 78% (the
   plate is gone) and trims 14% off each side, which portraits can afford
   because the subject is centred. */
.hcard-img {
  position: relative; display: block; overflow: hidden;
  aspect-ratio: 1; border-radius: 34% 34% var(--r-xs) var(--r-xs);
  border: 1px solid var(--hair); background: var(--plate);
}
.hcard-img img {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 128%; max-width: none; height: auto; display: block;
  filter: saturate(.4) brightness(.66);
  transition: filter var(--t-2) var(--ease);
}
.hcard-name { font: 600 var(--fs-2xs)/1.2 var(--f-body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.hcard-type { display: none; }
/* Selected heroes come to full colour. The user's own roster pops out of a
   131-card grid with no extra chrome at all. */
.hcard.on { color: var(--text); }
.hcard.on .hcard-img { border-color: var(--ember-3); box-shadow: 0 0 0 1px var(--ember-3), 0 3px 12px rgba(255, 77, 46, .28); }
.hcard.on .hcard-img img { filter: none; }
.hcard.on::after {
  content: ""; position: absolute; top: 5px; right: 5px; width: 15px; height: 15px;
  border-radius: 50%; background: var(--molten); border: 1.5px solid var(--void);
}
.hcard:active .hcard-img { transform: translateY(1px); }

.picker-foot {
  flex: 0 0 auto; display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-4) calc(var(--s-3) + env(safe-area-inset-bottom));
  border-top: 1px solid var(--hair); background: var(--void);
}
.picker-foot .btn { margin-left: auto; }

.dock-count strong { display: block; font: 700 var(--fs-lg)/1 var(--f-mono); font-variant-numeric: tabular-nums; color: var(--text); }
.dock-count span { display: block; font: 500 var(--fs-2xs)/1.3 var(--f-body); color: var(--text-3); margin-top: 2px; }

body.sheet-open { overflow: hidden; }

/* -- the dock ------------------------------------------------------------ */
.dock {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-dock);
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-4) calc(var(--s-3) + env(safe-area-inset-bottom));
  background: rgba(10, 10, 12, .96);
  border-top: 1px solid var(--hair);
}
.dock .btn { flex: 0 0 auto; }
.dock #shareBtn { margin-left: auto; }
body:has(.dock:not([hidden])) { padding-bottom: 84px; }
/* The subscribe sheet must never sit under the dock. */
.sub-pop { z-index: var(--z-subscribe); }

/* -- army ---------------------------------------------------------------- */
.army-shell { background: var(--plate); border-radius: var(--r-lg); box-shadow: var(--lift-2); padding: var(--s-4); margin-bottom: var(--s-5); }
/* The army plan picker sits with the Copy button, and wraps under the heading
   on a phone rather than squeezing the select to nothing. */
.army-actions { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
.army-plan-label {
  font-size: var(--fs-2xs); color: var(--text-3);
  text-transform: uppercase; letter-spacing: .1em;
}
.army-actions select {
  background: var(--plate-2); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: .45rem .6rem; font: inherit; font-size: var(--fs-xs);
  max-width: 100%;
}
@media (max-width: 560px) {
  .army-actions { width: 100%; }
  .army-actions select { flex: 1 1 auto; }
}

.army-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-3); margin-bottom: var(--s-3); }
.army-sub { margin: var(--s-1) 0 0; font-size: var(--fs-sm); color: var(--text-3); max-width: 60ch; }
.army-rows { display: flex; flex-direction: column; }

.arow {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) 0 var(--s-3) var(--s-3);
  border-bottom: 1px solid var(--hair-soft); position: relative;
  /* The one earned motion beat: rows seat in as the army resolves. */
  animation: arow-seat var(--t-3) var(--ease) both;
}
.arow:nth-child(1) { animation-delay: 0ms; }
.arow:nth-child(2) { animation-delay: 60ms; }
.arow:nth-child(3) { animation-delay: 120ms; }
.arow:nth-child(4) { animation-delay: 180ms; }
.arow:nth-child(5) { animation-delay: 240ms; }
@keyframes arow-seat { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }
.arow:last-of-type { border-bottom: 0; }
.arow > .rail { position: absolute; left: 0; top: var(--s-2); bottom: var(--s-2); }
.dev-chip {
  display: inline-block; font: 600 var(--fs-2xs)/1.5 var(--f-body); font-style: normal;
  letter-spacing: .02em; text-transform: capitalize; white-space: nowrap;
  color: var(--ember-ink); background: var(--molten);
  border-radius: var(--r-xs); padding: 1px var(--s-2);
}
.arow.is-empty { opacity: .62; }
/* The Defensive Queue is not chosen by the army plan -- it is whatever the four
   combat rows leave behind. The dashed rule says that before the copy has to. */
.arow.is-queue { border-top: 1px dashed var(--hair); margin-top: var(--s-1); }
.arow.is-queue .arow-label { color: var(--text-2); }
.arow-label { flex: 0 0 auto; width: 4.8rem; font: 500 var(--fs-2xs)/1.2 var(--f-mono); letter-spacing: .06em; text-transform: uppercase; color: var(--text-3); }
.arow-slots { display: flex; gap: var(--s-2); flex: 1 1 auto; min-width: 0; }
.aslot-fig { display: flex; flex-direction: column; align-items: center; gap: 3px; min-width: 0; }
.aslot-fig img {
  width: 44px; height: 34px; object-fit: cover; object-position: 50% 18%;
  border-radius: 26% 26% var(--r-xs) var(--r-xs); border: 1px solid var(--hair); background: var(--void);
}
.aslot-fig em { font: 600 8.5px/1.15 var(--f-body); font-style: normal; color: var(--text-3);
  max-width: 46px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.arow-rank { flex: 0 0 auto; text-align: right; font: 700 var(--fs-md)/1 var(--f-mono); font-variant-numeric: tabular-nums; color: var(--ember-1); }
.arow-rank i { display: block; font: 500 var(--fs-2xs)/1.3 var(--f-body); font-style: normal; color: var(--text-3); margin-top: 3px; }
.arow-empty { flex: 1; margin: 0; font-size: var(--fs-xs); color: var(--text-3); }
.arow-empty strong { color: var(--ember-1); }
.army-notice { margin-top: var(--s-3); padding: var(--s-3); background: var(--void); border-radius: var(--r-sm);
  font-size: var(--fs-xs); color: var(--text-2); }
.army-notice strong { color: var(--text); }
.army-summary { margin: var(--s-3) 0 0; font-size: var(--fs-xs); color: var(--text-3); }

/* -- tabs ---------------------------------------------------------------- */
.result-tabs { display: flex; gap: 3px; background: var(--plate); border-radius: var(--r-sm); padding: 3px; margin-bottom: var(--s-4); }
.rtab { flex: 1; min-height: 40px; border: 0; background: none; cursor: pointer;
  color: var(--text-3); font: 600 var(--fs-sm)/1 var(--f-body); border-radius: var(--r-xs); }
.rtab[aria-selected="true"] { background: var(--plate-2); color: var(--text); box-shadow: inset 0 1px 0 var(--edge-hi); }
.panel-lede { font-size: var(--fs-sm); color: var(--text-3); margin-bottom: var(--s-3); }

.results-controls { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-bottom: var(--s-3); align-items: center; }
.results-controls select {
  min-height: var(--tap); padding: 0 var(--s-3); font-size: var(--fs-md);
  background: var(--plate); color: var(--text); border: 1px solid var(--hair); border-radius: var(--r-sm);
}
.switch { display: inline-flex; align-items: center; gap: var(--s-2); min-height: var(--tap);
  font-size: var(--fs-sm); color: var(--text-2); cursor: pointer; }
.switch input { width: 20px; height: 20px; accent-color: var(--ember-3); }

/* -- combo rows ---------------------------------------------------------- */
.combo-results { display: flex; flex-direction: column; }
.result-count-note { font: 500 var(--fs-xs)/1 var(--f-mono); color: var(--text-3); margin: 0 0 var(--s-2); }
.crow {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-2) var(--s-3) var(--s-3);
  background: var(--plate); border-bottom: 1px solid var(--hair-soft); position: relative;
}
.crow:first-of-type { border-radius: var(--r-md) var(--r-md) 0 0; }
.crow:last-of-type { border-radius: 0 0 var(--r-md) var(--r-md); border-bottom: 0; }
.crow > .rail { position: absolute; left: 0; top: var(--s-2); bottom: var(--s-2); border-radius: 0 2px 2px 0; }
.crow-rank { flex: 0 0 auto; min-width: 2.4ch; text-align: right;
  font: 700 var(--fs-md)/1 var(--f-mono); font-variant-numeric: tabular-nums; color: var(--text-3); }
.crow.is-ready .crow-rank { color: var(--ember-1); }
.crow-faces { display: flex; gap: 3px; flex: 0 0 auto; }
.crow-faces img {
  width: 36px; height: 28px; object-fit: cover; object-position: 50% 18%;
  border-radius: 24% 24% var(--r-xs) var(--r-xs); border: 1px solid var(--hair); background: var(--void);
}
/* A hero you do NOT own is dimmed — you can see at a glance what is missing. */
.crow-faces img.dim { filter: saturate(.25) brightness(.5); }
.crow-body { flex: 1 1 auto; min-width: 0; }
.crow-names { display: block; font: 600 var(--fs-sm)/1.25 var(--f-body); color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.crow-why { display: block; margin-top: 2px; font: 400 var(--fs-2xs)/1.3 var(--f-body); color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.crow.is-ready .crow-why { color: var(--ok); }
.crow-meta { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.crow-meta b { font: 700 var(--fs-2xs)/1 var(--f-mono); color: var(--text-3); }
.crow-meta .tier-chip { font-style: normal; }
@media (max-width: 400px) {
  .arow-label { width: 4.3rem; font-size: .7rem; letter-spacing: .03em; }
  .aslot-fig img { width: 40px; }
  .aslot-fig em { max-width: 42px; }
}
@media (max-width: 420px) { .crow-meta .tier-chip { display: none; } }

/* -- stats / insights / wishlist ----------------------------------------- */
.stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); gap: var(--s-2); margin-bottom: var(--s-4); }
.stat { background: var(--plate); border: 0; border-radius: var(--r-sm); box-shadow: var(--lift-1);
  padding: var(--s-3); text-align: left; color: inherit; }
button.stat { cursor: pointer; min-height: var(--tap); }
button.stat[aria-pressed="true"] { box-shadow: var(--lift-1), inset 3px 0 0 var(--ember-3); }
.stat strong { display: block; font: 700 var(--fs-xl)/1 var(--f-mono); font-variant-numeric: tabular-nums; color: var(--ember-1); }
.stat span { display: block; margin-top: 4px; font: 600 var(--fs-2xs)/1.2 var(--f-body); letter-spacing: .08em; text-transform: uppercase; color: var(--text-3); }

.insight-grid { display: grid; gap: var(--s-3); grid-template-columns: 1fr; }
@media (min-width: 720px) { .insight-grid { grid-template-columns: 1fr 1fr; } }
.insight-card { background: var(--plate); border-radius: var(--r-md); box-shadow: var(--lift-1); padding: var(--s-4); }
.insight-card h3 { margin: var(--s-2) 0; font-size: var(--fs-md); font-family: var(--f-body); font-weight: 600; }
.insight-card p { margin: 0; font-size: var(--fs-sm); color: var(--text-3); }

.wishlist-grid { display: grid; gap: var(--s-2); grid-template-columns: 1fr; }
@media (min-width: 620px) { .wishlist-grid { grid-template-columns: 1fr 1fr; } }
.wish-card { display: flex; align-items: center; gap: var(--s-3); background: var(--plate);
  border-radius: var(--r-sm); box-shadow: var(--lift-1); padding: var(--s-2); }
.wish-card img { width: 50px; height: 39px; flex: 0 0 auto; object-fit: cover; object-position: 50% 18%;
  border-radius: 26% 26% var(--r-xs) var(--r-xs); border: 1px solid var(--hair); }
.wish-card h4 { margin: 0; font: 600 var(--fs-sm)/1.2 var(--f-body); }
.wish-card p { margin: 2px 0 0; font-size: var(--fs-2xs); color: var(--text-2); }
.wish-card small { font-size: var(--fs-2xs); color: var(--text-3); }

.empty-state { padding: var(--s-6) var(--s-4); text-align: center; color: var(--text-3);
  font-size: var(--fs-sm); background: var(--plate); border-radius: var(--r-md); }
.empty-state strong { color: var(--text); }

/* -- results section + tab panels ---------------------------------------- */
.results-section { padding-top: var(--s-4); }
.rpanel { min-height: 120px; }

/* -- SEO / FAQ blocks below the tool ------------------------------------- */
.seo-section, .faq-section { padding-top: var(--s-6); }
.seo-grid { display: grid; gap: var(--s-3); grid-template-columns: 1fr; }
@media (min-width: 620px) { .seo-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); } }
.seo-card {
  background: var(--plate); border-radius: var(--r-md); box-shadow: var(--lift-1);
  padding: var(--s-4); border-left: 3px solid transparent;
  transition: border-color var(--t-2) var(--ease);
}
.seo-card h3 { margin: 0 0 var(--s-2); font-size: var(--fs-md); }
/* These are the card's only control, so they carry the card's tap target —
   they were 20px tall standalone links. */
.seo-card h3 a { text-decoration: none; display: inline-flex; align-items: center; min-height: var(--tap); }
.seo-card p { margin: 0; font-size: var(--fs-sm); color: var(--text-3); }
@media (hover: hover) and (pointer: fine) { .seo-card:hover { border-left-color: var(--ember-3); } }
.faq-section details {
  background: var(--plate); border-radius: var(--r-md); box-shadow: var(--lift-1);
  padding: var(--s-3) var(--s-4); margin-bottom: var(--s-2);
}
.faq-section summary { cursor: pointer; min-height: var(--tap); display: flex; align-items: center;
  font: 600 var(--fs-md)/1.3 var(--f-body); }
.faq-section summary::marker { color: var(--ember-3); }
.faq-section details p { margin: var(--s-2) 0 0; color: var(--text-2); font-size: var(--fs-sm); }

/* -- homepage section order ----------------------------------------------
   The meta rail sits ABOVE the results for a visitor with no roster — they get
   something worth reading immediately instead of ~2,700px of empty states — and
   drops BELOW them once a roster exists, so a returning player gets their army
   first. Done with flex order rather than moving nodes, so #rankings keeps its
   DOM position and its anchor. */
/* NOTE: there is deliberately no CSS `order` swap here.
   The meta rail sits above the results in the MARKUP, and app.js physically
   moves the node below them once a roster exists. Doing it with `order` would
   have been fewer lines but would leave a keyboard user tabbing through the
   results before the rail they can see above it — WCAG 2.4.3 focus order and
   1.3.2 meaningful sequence. Visual order and DOM order stay equal. */

/* -- idle results --------------------------------------------------------- */
.results-idle { display: none; }
body.no-roster .results-idle {
  display: block; text-align: center;
  background: var(--plate); border-radius: var(--r-lg); box-shadow: var(--lift-1);
  padding: var(--s-6) var(--s-4);
}
.results-idle p { color: var(--text-2); max-width: 46ch; margin-inline: auto; }
.results-idle .btn { margin-top: var(--s-3); }
/* Everything the idle card stands in for. */
body.no-roster .army-shell,
body.no-roster .result-tabs,
body.no-roster .rpanel { display: none; }

/* -- the meta rail -------------------------------------------------------- */
.meta-rail-section { padding-top: var(--s-5); }
.rail-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--s-3); margin-bottom: var(--s-4); }
.rail-head h2 { margin: 0; font-size: var(--fs-xl); }
.rail-all {
  flex: 0 0 auto; font: 600 var(--fs-sm)/1 var(--f-body); color: var(--ember-1);
  text-decoration: none; min-height: var(--tap); display: inline-flex; align-items: center;
}

.meta-rail {
  display: grid; gap: var(--s-3);
  grid-template-columns: repeat(4, 78%);
  overflow-x: auto; scroll-snap-type: x mandatory;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  margin-inline: calc(var(--s-4) * -1); padding-inline: var(--s-4);
  padding-bottom: var(--s-2);
}
.meta-rail::-webkit-scrollbar { display: none; }
@media (min-width: 640px) { .meta-rail { grid-template-columns: repeat(2, 1fr); overflow: visible; margin-inline: 0; padding-inline: 0; } }
@media (min-width: 1000px) { .meta-rail { grid-template-columns: repeat(4, 1fr); } }

.meta-card {
  position: relative; scroll-snap-align: start;
  background: var(--plate); border-radius: var(--r-md);
  box-shadow: var(--lift-2); overflow: hidden;
}
.meta-card > .rail { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; }
.meta-card-link { display: block; padding: var(--s-3) var(--s-3) var(--s-3) var(--s-4); text-decoration: none; color: inherit; }
.meta-card-top { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); margin-bottom: var(--s-3); }
.meta-cat { display: inline-flex; align-items: center; gap: 6px; font: 700 var(--fs-2xs)/1 var(--f-body); letter-spacing: .11em; text-transform: uppercase; }
.meta-rank { font: 700 var(--fs-sm)/1 var(--f-mono); color: var(--ember-1); }

/* 56px, not 106px — a glance at the meta, not a gallery. */
.meta-faces { display: flex; gap: var(--s-2); }
.meta-face {
  position: relative; display: block; width: 56px; flex: 0 0 auto;
  aspect-ratio: 1; overflow: hidden;
  border-radius: 32% 32% var(--r-xs) var(--r-xs);
  border: 1px solid var(--hair); background: var(--void);
}
.meta-face img {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 126%; max-width: none; height: auto; display: block;
}
.meta-names { display: block; margin-top: var(--s-3); font: 600 var(--fs-sm)/1.3 var(--f-body); color: var(--text); }
.meta-foot { display: flex; align-items: center; gap: var(--s-2); margin-top: var(--s-2); }
.meta-foot em { font: 500 var(--fs-2xs)/1 var(--f-mono); font-style: normal; color: var(--text-3); margin-left: auto; }
.meta-foot .tier-chip { font-style: normal; }
@media (hover: hover) and (pointer: fine) { .meta-card:hover { background: var(--plate-2); } }
.meta-card:active { transform: translateY(1px); }

.showcase-note { font-size: var(--fs-sm); color: var(--text-3); margin-top: var(--s-4); }

/* ==========================================================================
   8j. HERO KIT — what the skills grant, and the eight skills
   --------------------------------------------------------------------------
   These two components carry the richest data on the site: a per-attribute
   breakdown totalled from the hero's own skill text, and all eight skill cards
   straight from the game card. The markup was already semantic and detailed —
   it simply had no rules in this sheet, so on all 95 heroes that HAVE skill
   data it rendered as bare text lines with emoji. That is the single biggest
   quality failure of the rollout and this section is the fix.
   ========================================================================== */
.kit-section { padding-top: var(--s-5); }
.kit-section > h2 { margin-bottom: var(--s-2); }
.kit-lede { font-size: var(--fs-sm); line-height: 1.6; color: var(--text-3); max-width: 68ch; margin: 0 0 var(--s-4); }
.kit-lede strong { color: var(--text-2); }

/* -- attribute cards ----------------------------------------------------- */
.attr-grid {
  display: grid; gap: var(--s-2);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  margin-bottom: var(--s-4);
}
.attr-card {
  background: var(--plate); border-radius: var(--r-md);
  box-shadow: var(--lift-1);
  padding: var(--s-3);
  display: flex; flex-direction: column; gap: var(--s-2);
  min-width: 0;
}
.ac-top { display: flex; align-items: center; gap: var(--s-2); }
.ac-icon { display: grid; place-items: center; width: 26px; height: 26px; flex: 0 0 auto;
  border-radius: var(--r-xs); background: var(--plate-2); color: var(--text-3); }
.ac-icon .ic { width: 15px; height: 15px; }
.ac-label {
  font: 700 var(--fs-2xs)/1.25 var(--f-body);
  letter-spacing: .09em; text-transform: uppercase; color: var(--text-2);
  min-width: 0; overflow-wrap: anywhere;
}

/* Three families, not seven colours. Offence runs ember, defence steel,
   utility moss — enough to scan by, few enough to stay disciplined. */
.a-might .ac-icon, .a-damage .ac-icon, .a-fatal_damage .ac-icon,
.a-fatal_rate .ac-icon, .a-destructive_rate .ac-icon, .a-siege_might .ac-icon { color: var(--ember-2); }
.a-resistance .ac-icon, .a-damage_taken .ac-icon, .a-hp .ac-icon,
.a-counter_taken .ac-icon { color: var(--foo); }
.a-recovery .ac-icon, .a-evasion .ac-icon, .a-attrition .ac-icon,
.a-counter .ac-icon { color: var(--arc); }

/* The number is the point, so it gets the mono face and the size. */
.bonus-line { display: flex; align-items: baseline; gap: var(--s-2); min-width: 0; }
.bonus-line strong {
  font: 700 var(--fs-lg)/1 var(--f-mono);
  font-variant-numeric: tabular-nums; letter-spacing: -.02em;
  color: var(--ember-1); flex: 0 0 auto;
}
.bonus-line span {
  font: 400 var(--fs-2xs)/1.3 var(--f-body); color: var(--text-3);
  min-width: 0; overflow-wrap: anywhere;
}
/* A minus here is a debuff the hero puts ON the enemy — good for the player,
   so it must not read as a penalty. Steel, not red. */
.bonus-line.neg strong { color: var(--foo); }

/* -- skill cards --------------------------------------------------------- */
.skill-cards { display: flex; flex-direction: column; gap: var(--s-2); }
.skill-card {
  background: var(--plate); border-radius: var(--r-md);
  box-shadow: var(--lift-1);
  padding: var(--s-3) var(--s-4) var(--s-4);
  position: relative;
}
/* The card is a <details>; the head is its <summary>. */
.skill-card > summary { list-style: none; cursor: pointer; min-height: var(--tap); }
.skill-card > summary::-webkit-details-marker { display: none; }
.skill-card > summary::after {
  content: ""; margin-left: auto; flex: 0 0 auto;
  width: 8px; height: 8px; border-right: 2px solid var(--text-3); border-bottom: 2px solid var(--text-3);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--t-2) var(--ease);
}
.skill-card[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.skill-card[open] > summary { margin-bottom: var(--s-2); }
.sc-head { display: flex; align-items: center; gap: var(--s-3); margin-bottom: var(--s-2); }
.sc-no {
  flex: 0 0 auto; width: 28px; height: 28px;
  display: grid; place-items: center;
  border-radius: var(--r-xs);
  background: var(--plate-2); box-shadow: inset 0 1px 0 var(--edge-hi);
  font: 700 var(--fs-sm)/1 var(--f-mono); color: var(--text-3);
}
/* Slots 2, 5 and 8 are the ones that decide a lineup, so they run hot. */
.skill-card:nth-child(2) .sc-no,
.skill-card:nth-child(5) .sc-no,
.skill-card:nth-child(8) .sc-no { background: var(--molten); color: var(--ember-ink); }
.sc-head h3 { margin: 0; font: 600 var(--fs-md)/1.25 var(--f-body); font-family: var(--f-body); min-width: 0; }
.sc-meta {
  margin: 0 0 var(--s-2); padding-left: calc(28px + var(--s-3));
  font: 600 var(--fs-2xs)/1.4 var(--f-body);
  letter-spacing: .06em; text-transform: uppercase; color: var(--ember-2);
}
.sc-desc {
  margin: 0; padding-left: calc(28px + var(--s-3));
  font: 400 var(--fs-sm)/1.6 var(--f-body); color: var(--text-2);
}
@media (max-width: 480px) {
  .sc-meta, .sc-desc { padding-left: 0; }
}

/* ==========================================================================
   9. DEFENSIVE + UTILITIES
   LOAD-BEARING. Ten components are toggled by the `hidden` attribute while
   also carrying their own display rule (flex/grid/block). Without this
   re-assertion the subscribe prompt, the contact form's later steps and the
   roster note all render permanently visible.
   ========================================================================== */
[hidden] { display: none !important; }

/* RUNTIME-TOGGLED CLASSES.
   These are added by JavaScript and therefore appear in NO server-rendered
   HTML — which is exactly why a coverage check that diffs rendered markup
   against this sheet cannot see them. Every one of these must be listed here
   explicitly.

   .is-hidden was the worst of them: heroes.php's filter script adds it to every
   non-matching card, so with no rule the troop-type filters on /heroes ran,
   updated their own count label to "Showing 53 cavalry heroes", and then hid
   nothing at all. The buttons looked broken because they were. */
.is-hidden { display: none !important; }

/* Subscribe / contact form error state (app/view.php sets .sub-msg.err). */
.sub-msg.err, .contact-status.err { color: var(--bad); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.center { text-align: center; }
.muted  { color: var(--text-3); }

/* ==========================================================================
   10. REDUCED MOTION — one kill switch for the whole sheet
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   11. PRINT
   ========================================================================== */
@media print {
  body::before, .site-footer, .nav-toggle, .sub-pop, .toast { display: none !important; }
  body { background: #fff; color: #000; }
}

/* -- copy deterrence -------------------------------------------------------
   Applied only where roc_head() was called with 'protect' => true.

   What this is honest about: it stops selecting and dragging text. It does not
   stop screenshots (no browser exposes that), View Source, curl or Reader Mode
   — the text ships as plain HTML because Google has to read it, and anything
   Google can read a person can read. See assets/js/protect.js.

   Scoped to content. Anything a reader has to type in, choose from, or select
   deliberately is exempted below — a protection that breaks the search box
   costs more than the copying it prevents. */
body.protect main,
body.protect .site-header,
body.protect .site-footer {
  -webkit-user-select: none;
     -moz-user-select: none;
       -ms-user-select: none;
          user-select: none;
  /* iOS: kills the long-press "Copy / Look Up / Share" callout over text. */
  -webkit-touch-callout: none;
}

/* Everything interactive keeps normal behaviour. */
body.protect input,
body.protect textarea,
body.protect select,
body.protect [contenteditable="true"] {
  -webkit-user-select: text;
     -moz-user-select: text;
          user-select: text;
  -webkit-touch-callout: default;
}

/* Portraits: no drag-to-desktop, no long-press "Save image". */
body.protect img {
  -webkit-user-drag: none;
     -khtml-user-drag: none;
          user-drag: none;
  -webkit-touch-callout: none;
  pointer-events: none;
}
/* ...except where the portrait IS the link. pointer-events:none above would
   otherwise make hero cards and combo rows unclickable. */
body.protect a img,
body.protect button img,
body.protect summary img { pointer-events: auto; }

/* -- print -----------------------------------------------------------------
   The one part of "stop screenshots" that is actually enforceable. Print and
   Save-as-PDF are a real bulk-extraction route and, unlike a screenshot, the
   page is asked first. The reader still gets the page identity and a pointer
   back; they do not get 900 words of analysis on a sheet of paper. */
@media print {
  body.protect main > section:not(.rank-page-hero),
  body.protect .rank-list,
  body.protect .skill-cards,
  body.protect .hero-about-body,
  body.protect .gloss { display: none !important; }

  body.protect main::after {
    content: "The rankings, hero analysis and combat data on this page are original research by ROCMeta.com and are not available in print. Read them at rocmeta.com — free, no account.";
    display: block; padding: 2rem 0; max-width: 60ch;
    font: 400 12pt/1.6 Georgia, "Times New Roman", serif; color: #000;
  }
}
