@font-face {
  font-family: "Anton";
  src: url("../assets/fonts/Anton-Regular.ttf") format("truetype");
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/Inter-Regular.ttf") format("truetype");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/Inter-Bold.ttf") format("truetype");
  font-weight: 700;
  font-display: swap;
}

:root {
  --bg: #0f0f0f; /* direct instruction, 2026-09-14 - was #0a0a0a */
  --panel: #161616;
  --panel-edge: #2a2a2a;
  --accent: #6ee7b7;
  --accent-dim: #2f6b53;
  --good: #6ee7b7;
  --bad: #ff4d5e;
  --text: #f4f4f4;
  --text-soft: #cfcfcf;
  --text-dim: #999999;
  color-scheme: dark;
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }

/* Hides the ENTIRE page instantly when arriving via a hub card's own
   ?autostart=1 link, so nothing flashes before quiz.js's async init() gets
   a chance to decide what to show itself (that has to wait on a data fetch
   first). visibility (not display) so .wrap keeps its normal layout box -
   no reflow/scroll jump when it reappears. The .autostarting class is set
   synchronously by a tiny inline <script> in each game's own <head>, before
   first paint, and removed by quiz.js's own init() in the same synchronous
   pass as the .hidden toggles that follow it (never across an await), so
   whatever becomes visible when this class is dropped is already the
   final, correct state - not just the intro screen specifically (an
   earlier version of this rule only targeted #start-panel, which still let
   the CARD's own narrower "intro" width rule apply during the gap, since
   that rule keys off the hidden ATTRIBUTE, not display - a visible width
   jump right as the intro was replaced; found via review, 2026-09-13).
   Same fix applied to the football site's own style.css. */
html.autostarting .wrap { visibility: hidden; }

body {
  margin: 0;
  background: #000000; /* pure black (direct instruction, 2026-09-14 - was #1a1a1a) */
  color: var(--text);
  font-family: "Inter", system-ui, sans-serif;
  min-height: 100vh; /* fallback for browsers without dvh */
  min-height: 100dvh; /* mobile address-bar-safe viewport height */
  display: flex;
  justify-content: center;
  padding: 40px 16px 32px;
}

.wrap { width: 100%; max-width: 480px; }

.site-header { text-align: center; margin-bottom: 29px; }
.site-logo-mark {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 auto 10px;
  text-decoration: none; /* it's an <a> on the hub pages (2026-09-15, links home) -
    no visible text inside to underline anyway, but explicit rather than relying
    on that - the portal's own logo stays a plain <div>, no link-to-self needed */
}
.site-logo-icon { display: block; width: 61.425px; height: auto; }
.kicker {
  display: inline-block;
  font: 700 12px/1 "Inter", sans-serif;
  letter-spacing: 0.28em;
  color: var(--accent);
  margin-bottom: 6px;
}
/* "See all games" - a way back to the portal (2026-09-15, direct instruction:
   the hub pages had NO link back to "/" at all before this - only the site
   logo above, now also made a link, see .site-logo-mark's own comment).
   Sits below the game grid, above the divider/intro paragraph below it -
   deliberately the CROSS-topic accent color (football's blue here, on the
   geography hub) rather than this page's own mint, as a small visual nod
   to "the other topic" you're one click away from. */
.hub-see-all {
  display: block;
  text-align: center;
  margin-top: 24px;
  color: #7ec6ff; /* football's own --accent - deliberately NOT this page's mint, see comment above */
  font: 700 13px/1 "Inter", sans-serif;
  text-decoration: none;
}
.hub-see-all:hover { text-decoration: underline; }

/* Hub-only short intro paragraph (2026-09-15, Phase 2 SEO/LLM work) - real
   written content BELOW the game grid (moved down from above it, direct
   instruction - the games stay the first thing seen, prose comes after),
   for a crawler/reader to get real sentences, not just card labels. */
.hub-intro {
  color: var(--text-soft);
  font-size: 13px;
  line-height: 1.5;
  text-align: left;
  margin-top: 20px;
  padding-top: 24px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

/* Site-wide footer (2026-09-15, Phase 2 SEO/trust work) - same markup on
   every page across both sites (root-relative links, so they resolve
   correctly whether the page lives at "/", "/geography/flags/", or proxied
   under "/football/"). Same divider treatment as .portal-about/.hub-intro
   above it, and the same de-emphasized 12px/11px size as .tagline-line -
   this is utility chrome, not primary content, on every page it appears on. */
.site-footer {
  margin-top: 40px;
  padding-top: 20px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  text-align: center;
  font: 400 12px/1.6 "Inter", sans-serif;
  color: var(--text-dim);
}
.site-footer a { color: var(--text-dim); text-decoration: none; }
.site-footer a:hover { color: var(--accent); text-decoration: underline; }
.site-footer .footer-sep { margin: 0 6px; color: var(--text-dim); }
.site-header h1 {
  font: 400 42.29px/1 "Anton", sans-serif; /* fitted to match .start-title's rendered width, same target as the kicker */
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin: 0;
  color: var(--text-soft);
}
/* Game-mode-only overrides (while #play-area is visible, i.e. after "Start
   Playing") - must not touch the intro's fitted GEOGRAPHY sizing/spacing. */
.wrap:has(#play-area:not([hidden])) .site-logo-icon { width: 43px; } /* 70% of 61.425px, top stays put since only height shrinks */
.wrap:has(#play-area:not([hidden])) .site-logo-mark { margin: -18px auto 4px; } /* title sits closer to the logo, whole thing shifted up */
.wrap:has(#play-area:not([hidden])) .site-header h1 { font-size: 32px; margin-top: 8px; } /* matches .result-title's size */
.wrap:has(#play-area:not([hidden])) .site-header { margin-bottom: 15px; } /* brings the card closer beneath the title */

.card {
  background: var(--panel);
  border: 1px solid var(--panel-edge);
  border-radius: 20px;
  padding: 10px 24px 24px;
  box-shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.6);
}
/* Narrower, content-hugging width only while the intro screen is showing - the
   play-area screens (flag, timer, options, results) need the full wrap width,
   so this must not apply once start-panel is hidden after "Start Playing". */
.card:has(#start-panel:not([hidden])) {
  width: 258px; /* 200px content column + 29px inset each side, matching the card-top-to-kicker-top distance */
  margin: 0 auto;
  padding-left: 29px;
  padding-right: 29px;
  padding-bottom: 29px;
  max-width: calc(100% - 20px); /* safety net so this doesn't overflow narrow phones */
}

/* Hub page only (quizebra.com/geography/) - one card per quiz, wrapping as more
   get added. Deliberately its own layout now rather than reusing the standalone
   intro card's exact numbers (258px/29px/200px) - equal padding on all 4 sides,
   kept as tight as still looks comfortable since this card has less content than
   the standalone intro (no kicker - that moved up into the page header instead). */
/* .site-header h1's shared color (--text-soft) was set for the standalone pages'
   summary-frame hierarchy, where it needs to recede below "ALL DONE!"/the
   percentile/leaderboard - the hub has no such competing hierarchy, and "GEOGRAPHY"
   should read the same as each card's own title below it. */
.hub-wrap .site-header h1 { color: var(--text); }
#hub-kicker {
  display: block; /* .kicker's default inline-block doesn't respond to margin-top the
    normal way (confirmed - even -20px had zero visual effect); block does */
  margin-top: 2px; /* was 6px before "move up 4px" (inline-block's own baseline gap) -
    switching to display:block above also silently removed its own baseline spacing,
    so this is 2px, not -4px, to still land on that same net 4px reduction */
}
/* .site-header's shared margin-bottom (29px, tuned for the standalone pages' own
   kicker-inside-the-card layout) becomes the gap from DAILY CHALLENGES down to the
   card row here - matched to .hub-grid's own 16px gap between the two cards instead,
   so vertical and horizontal spacing read as the same rhythm. */
.hub-wrap .site-header { margin-bottom: 16px; }
.hub-wrap { max-width: 480px; } /* exactly 2 cards (232px) + the gap between them
  (16px) - forces a strict 2-per-row grid at any viewport width, rather than
  however many happen to fit at 820px (3, once a 4th quiz was added) */
.hub-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 16px; /* 2 cards (232px) + this gap = 480px, exactly matching the actual
    in-round card's own width (.wrap's max-width, uncapped by the narrower
    258px-wide intro-only card variant) - at any viewport wide enough to show both
    side by side, this row's left/right edges land exactly on that same in-round
    card's own edges. Only holds for exactly 2 cards - revisit if a 3rd ships. */
}
.hub-card {
  display: block;
  width: 232px; /* content column (194px) = .start-title's own target width (191px,
    ~3px wider than the rules line by design - see .start-title's comment) plus a
    few px of slack - flush against the exact value wrapped the title in testing,
    sub-pixel font rendering apparently isn't quite as exact as getBoundingClientRect
    measurements suggested. */
  max-width: calc(100% - 20px);
  text-decoration: none;
  color: inherit;
  transition: transform 0.12s ease, border-color 0.12s ease;
  /* Hard-edged solid shadow (no blur, full opacity) instead of the standalone
     card's own soft dark one - a flat, crisp offset rather than a soft ambient
     lift. Same direction (bottom-right) on every card. Same color as the border
     below - both match "GEOGRAPHY"'s own color (.hub-wrap .site-header h1). */
  box-shadow: 3px 3px 0 0 var(--text);
  border-color: var(--text);
}
/* Two classes (not just .hub-card) so this reliably beats .card's own padding at
   every viewport width, including inside the narrow-screen media queries below -
   those target plain .card, same specificity as a lone .hub-card rule would have,
   and would otherwise win or lose based on source order alone (fragile - broke once
   already during this same round of edits). */
.card.hub-card {
  padding: 18px;
  background: #000000; /* pure black (direct instruction, 2026-09-14 - was
    var(--bg)/#0f0f0f) - only affects the REAL per-game hub-cards (e.g.
    quizebra.com/football/'s 4 game cards); the portal's own topic-cards
    still win out over this via their own higher-specificity
    .card.hub-card.topic-card rule, unaffected by this change */
}
/* One card only ("Rank the Country") - blends into the page instead of standing
   out like the other three. #000000 matches body's own background exactly (not
   --bg, a different dark token used for the other cards) - not a named palette
   variable since body's own background isn't one either (see body's own rule).
   Was #1a1a1a until body itself went pure black (direct instruction, 2026-09-14). */
.hub-card-page-bg { background: #000000; }
/* .start-panel's own top padding (10px, shared with the standalone pages, meant to
   sit under THEIR card's own different top inset) would break the "equal on all
   sides" above if left in here too - cancelled so .hub-card's padding is the only
   inset in play. */
.hub-card .start-panel { padding-top: 0; }
.hub-card .start-title { margin-top: 0; } /* .start-title's own 4px top margin would
  otherwise throw off "equal padding on all 4 sides" above by that same 4px */
/* One card only ("Find the color") - mint on the left fading to the title's normal
   color on the right, a small nod to what that quiz is actually about. */
.hub-title-gradient {
  background: linear-gradient(to right, var(--accent), var(--text));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Football's "Build the Team" on the portal's own football topic-card -
   .hub-title-gradient's own trick, football's accent hex (#7ec6ff)
   hardcoded instead of var(--accent) (resolves to this page's mint) - same
   "of course football's own color" principle as the other -fb classes in
   this file. Direct instruction, 2026-09-20. */
.hub-title-gradient-fb {
  background: linear-gradient(to right, #7ec6ff, var(--text));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Football's "Who Has More?" - accent color at the very bottom fading to the
   title's normal color at the very top (direct instruction, 2026-09-14) -
   same background-clip:text trick as .hub-title-gradient, just vertical
   instead of horizontal ("to top" starts the gradient's first color at the
   bottom edge, ending with the second color at the top edge). */
.hub-title-vertical-fade {
  background: linear-gradient(to top, var(--accent), var(--text));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Football's "Guess the Player" (used on the portal's own football topic-card
   too) - a filled circle behind the text, same trick as .hub-title-stlucia
   below but a plain radial-gradient circle instead of a country silhouette -
   direct instruction, 2026-09-14. */
.hub-title-circle {
  /* Stop was 55%/56% (~97px diameter for "Guess the Player"'s own title
     width) - shrunk to ~75% of that (direct instruction, 2026-09-20,
     "doesn't have to be exact") by scaling the stop itself: 55*0.75≈41. */
  background: radial-gradient(circle, var(--accent) 41%, transparent 42%) center / auto 400% no-repeat, var(--text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Football's "Guess the Club" (used on the portal's own football topic-card
   too) - .hub-title-circle's own trick inverted, direct instruction,
   2026-09-20: the circle itself in var(--text) (white), everything outside
   it in var(--accent) - opposite of "Guess the Player" above. Same 41%/42%
   shrink as that rule, to keep the two visually matched. */
.hub-title-circle-invert {
  background: radial-gradient(circle, var(--text) 41%, transparent 42%) center / auto 400% no-repeat, var(--accent);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Football's "Guess the Player"/"Guess the Club" on the portal's own
   football topic-card - same two rules as above, but with football's own
   accent hex (#7ec6ff) hardcoded instead of var(--accent), which resolves
   to this page's own mint. Direct instruction, 2026-09-20 (same "of
   course" football-accent-on-this-page principle as the arrow/stripe -fb
   classes elsewhere in this file). Plain radial-gradient functions, not a
   data-URI image, so a literal hex works fine here - no SVG encoding
   needed like the arrow classes required. */
.hub-title-circle-fb {
  background: radial-gradient(circle, #7ec6ff 41%, transparent 42%) center / auto 400% no-repeat, var(--text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.hub-title-circle-invert-fb {
  background: radial-gradient(circle, var(--text) 41%, transparent 42%) center / auto 400% no-repeat, #7ec6ff;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* One card only ("Guess the Country") - a real country's shape (mint) centered over
   the title text, rest of the letters stay the normal title color. Same
   background-clip:text trick as the gradient above, but with 2 layers instead of 1:
   the country-shape image on top (its transparent areas let the plain color layer
   underneath show through), the solid color as a fallback "fill" for the rest of
   the text. lc-mint.svg (St. Lucia) is a simplified/decimated cutout of the real
   rendered map from the map quiz's own asset pipeline (geoquiz/maps/assets/maps/
   lc.svg) - recognizable at this size, not simplified further than that. */
.hub-title-stlucia {
  background: url("/geography/shared/assets/lc-mint.svg") center / auto 750% no-repeat, var(--text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* "Find the Country" - one of the game's own clue icons (mint), centered
   behind just the word "Country" now (direct instruction, 2026-09-20 - was
   centered across the whole title). Applied to a <span> wrapping only that
   word (hub/index.html, portal/index.html), not the whole <h2>/<p>, so
   "center" here centers on "Country"'s own box, not the full title's -
   "Find the " stays the title's plain color. Which icon is still being
   iterated on ("a first try, will be rather iterative" - was direction/
   compass, then population, now continent/world) - same background-
   clip:text trick as the other titles above, icon sourced straight from
   that game's own assets (not a copy) so it can never drift out of sync
   with the in-game icon. Class name kept stable across icon swaps on
   purpose, so future iterations only need this one CSS edit. */
.hub-title-compass {
  background: url("/geography/findcountry/assets/icons/continent.png") center / auto 100% no-repeat, var(--text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* "Order the Country" - one up arrow (mint), same shape as .arrange-arrow's
   own "move up" button ("▲"), behind just the word "Country" (direct
   instruction, 2026-09-20). A hand-drawn inline SVG triangle, not that
   button's own text glyph - its tight viewBox (no padding around the
   triangle) is what makes "auto 100%" land the triangle's own top/bottom
   exactly on the text's own top/bottom, as asked ("bottom of text = bottom
   of arrow, top of text = top of arrow") - the source PNG icons used
   elsewhere in this title family have a little breathing room baked into
   their own canvas, which would throw that off here. Applied to a <span>
   wrapping only "Country" (hub/index.html, portal/index.html), same
   scoping reasoning as .hub-title-compass above. */
.hub-title-arrow-up {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 87'%3E%3Cpolygon points='50,0 100,87 0,87' fill='%236ee7b7'/%3E%3C/svg%3E") center / auto 100% no-repeat, var(--text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* "Order the Country" - the down-arrow twin of .hub-title-arrow-up above,
   behind just the word "Order" this time (direct instruction, 2026-09-20) -
   same shape family as .arrange-arrow's own "move down" button ("▼"), same
   tight-viewBox SVG technique/reasoning as its up-pointing sibling. */
.hub-title-arrow-down {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 87'%3E%3Cpolygon points='0,0 100,0 50,87' fill='%236ee7b7'/%3E%3C/svg%3E") center / auto 100% no-repeat, var(--text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Football's "Order the Player", shown on the portal's own football topic-
   card - same shapes as .hub-title-arrow-up/-down above, but baked with
   football's own accent hex (#7ec6ff) instead of this page's mint, since a
   data-URI SVG can't reference a CSS custom property and this page's own
   var(--accent) resolves to mint, not football's blue. Direct instruction,
   2026-09-20: "of course" football's own titles should use football's own
   color, even on this cross-topic page. */
.hub-title-arrow-up-fb {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 87'%3E%3Cpolygon points='50,0 100,87 0,87' fill='%237ec6ff'/%3E%3C/svg%3E") center / auto 100% no-repeat, var(--text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.hub-title-arrow-down-fb {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 87'%3E%3Cpolygon points='0,0 100,0 50,87' fill='%237ec6ff'/%3E%3C/svg%3E") center / auto 100% no-repeat, var(--text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Football's "Rank the Player", shown on the portal's own football topic-
   card - same px stops as .hub-title-stripes (Rank the Country), but
   var(--accent)/var(--text) swapped for football's own literal blue/white
   so it doesn't pick up this page's mint. Same reasoning as the arrow
   classes just above. */
.hub-title-stripes-fb {
  background: linear-gradient(
    to bottom,
    #7ec6ff 0, #7ec6ff 7.6375px,
    var(--text) 7.6375px, var(--text) 12.275px,
    #7ec6ff 12.275px, #7ec6ff 16.9125px,
    var(--text) 16.9125px, var(--text) 21.55px,
    #7ec6ff 21.55px, #7ec6ff 23.1875px
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* "Find the Country" - the population clue icon (mint), behind just the
   word "Find" (direct instruction, 2026-09-20) - .hub-title-compass above
   already covers "Country" with a different icon (continent), so this is
   a separate class/word pairing, same background-clip:text technique. */
.hub-title-population {
  background: url("/geography/findcountry/assets/icons/population.png") center / auto 100% no-repeat, var(--text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* One card only ("Name the Flag") - 3 equal horizontal bands (mint/normal/mint),
   a plain generic tricolor flag - hard color stops (each color repeated at the
   same %) rather than a smooth blend, so the bands read as stripes, not a gradient. */
.hub-title-flag {
  background: linear-gradient(
    to bottom,
    var(--accent) 0%, var(--accent) 33.33%,
    var(--text) 33.33%, var(--text) 66.66%,
    var(--accent) 66.66%, var(--accent) 100%
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* One card only ("Rank the Country") - thin mint gridlines over the normal title
   color, a nod to ranking/categories as a table. Horizontal lines only (vertical
   ones dropped per feedback). Same background-clip:text trick as the others above. */
.hub-title-grid {
  /* Longhand, not the shorthand - "background: layer1, var(--text)" parsed the
     trailing color as its own (invalid) 2nd image layer instead of a base color,
     silently resolving to "none" and leaving the whole thing invisible. */
  background-image: repeating-linear-gradient(to bottom, var(--accent) 0 0.35px, transparent 0.35px 0.8px);
  background-color: var(--text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* "Rank the Country" only (was .hub-title-grid, same as "Rank the Player" still
   uses - split into its own class since only this one's title changes here) -
   5 horizontal bands, mint/white/mint/white/mint, title height 23.1875px
   (measured, Playwright). History: started as 5 equal 1/5 bands; shrank each
   white band, growing top/bottom mint to absorb it; moved the middle mint
   band down 3px, growing white1/shrinking bottom-mint to compensate. Latest
   direct instruction, 2026-09-20: white1, middle mint, and white2 must all
   three be the SAME height. Kept the middle mint band's own position fixed
   (12.275px-16.9125px, i.e. still shifted down 3px per the previous
   instruction) and resized both white bands to match its height (4.6375px),
   extending outward from its own fixed edges - top mint (0-7.6375px) and
   bottom mint (21.55-23.1875px) simply take whatever's left on each side,
   not required to match each other. */
.hub-title-stripes {
  background: linear-gradient(
    to bottom,
    var(--accent) 0, var(--accent) 7.6375px,
    var(--text) 7.6375px, var(--text) 12.275px,
    var(--accent) 12.275px, var(--accent) 16.9125px,
    var(--text) 16.9125px, var(--text) 21.55px,
    var(--accent) 21.55px, var(--accent) 23.1875px
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Every vertical gap inside the card (rules->label, row->button) matches the
   card's own 19px padding too, not just its own separate value - rules->label
   overrides the standalone pages' shared .start-rules margin (22px, tuned for
   THEIR different layout), scoped to the hub only. white-space:nowrap - must never
   wrap to a second line, whatever the actual text; a real quiz's rules line is short
   enough to always fit already, this just guards a future quiz's from silently
   breaking the card's height rhythm if it ever runs long. */
.hub-card .start-rules { margin-bottom: 19px; white-space: nowrap; }
.hub-beat-label {
  font: 700 16px "Inter", sans-serif;
  letter-spacing: 2.4106px; /* widened to match .leaderboard-row's own width (188px) -
    the row it's introducing, directly below it - recompute if font-size changes again */
  color: var(--accent);
  margin: 0 0 4px; /* was 6px (~70% of the 8px used elsewhere) - moved up another 2px
    to pull the #1 row (and the button below it, following in normal flow) closer
    still to the question introducing it */
}
/* Explicit 188px (not 100%) on both - the card's own content column is 191px, a
   few px wider than this to fit .start-title, so these need their own narrower,
   centered width to land at the rules line's own width instead of stretching to
   fill the full (title-driven) column. */
.hub-card .leaderboard { width: 188px; margin: 0 auto 19px; }
/* No height override on .leaderboard-row here - its natural height (no CSS beyond
   the shared .leaderboard-row rule) is what the same row looks like on the actual
   results screen. Start Playing below matches THIS height (30px) instead - the
   other way around from an earlier pass, which forced the row up to the button's
   own height instead and made it a different size than it actually ever is. */
.hub-card .btn-primary {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 188px;
  margin: 0 auto;
  height: 30px;
  padding: 0 14px;
  border-radius: 8px; /* matches .leaderboard-row's own corner radius, not the shared
    .btn-primary default (12px) - same reasoning as the height match above */
}
@media (hover: hover) {
  a.hub-card:hover { border-color: var(--accent); transform: translateY(-2px); } /* only
    real links (shipped quizzes) - a not-yet-shipped card renders as a plain div, and
    shouldn't look interactive on hover if it isn't */
}

/* Force 2 cards per row down to 375px viewport width (see chat, 2026-09-10) instead of
   dropping to 1 per row below the ~512px this grid's own 232px cards + 16px gap need to
   fit naturally (480px content width + 16px/14px body padding each side). Below 375px,
   deliberately NOT covered - falls back to the existing 1-per-row stacking untouched,
   both as an accepted floor (very narrow phones are rare) and as a safety net for any
   even-smaller device neither of us thought of.
   zoom, not transform: scale() - zoom changes real layout size (so the flex item
   actually takes up less row-space, no extra DOM wrapper needed) and doesn't collide
   with a.hub-card:hover's own transform above, unlike scale() would (see chat for the
   full reasoning + independent second opinion that landed on zoom over scale()).
   0.7759 = 180/232 - the target card width (180px) expressed as a fraction of the
   existing 232px base, so it's derived from - and stays in sync with - that single
   source of truth rather than a second hardcoded width value that could drift from it.
   The -11px/+22px trick on .hub-wrap eats into body's own ambient horizontal padding
   to reach ~3px on each side, but that ambient value actually varies across THREE
   sub-ranges within this block's own 375-511px span, not the two this comment
   originally accounted for: @media (max-width:375px) gives 12px there (comes later in
   the cascade, wins over the 480px tier's own rule) -> 12-11=1px; 376-480px gets that
   480px tier's 14px -> 14-11=3px; 481-511px falls through to neither narrow-tier rule,
   back to the BASE 16px -> 16-11=5px. All three read as visually consistent (1-5px is
   a small enough range not to look inconsistent) and none of them overflow - worth
   knowing precisely if these constants ever get recalibrated, since the original
   version of this comment claimed only two tiers and got the fit-at-the-floor
   arithmetic slightly wrong (1px margin there is 371px total, not exactly 375 - just
   more slack than stated, not an overflow). */
@media (min-width: 375px) and (max-width: 511px) {
  .hub-wrap { margin: 0 -11px; width: calc(100% + 22px); }
  .hub-grid { gap: 9px; }
  .hub-card { zoom: 0.7759; }
  /* .start-rules (the "5 maps - 20s each..." subtitle) stays nowrap (the base rule's
     own default) - must always read as one line, never wrap, per explicit direction.
     zoom SHOULD preserve its one-line fit exactly (text and container shrink together),
     but confirmed on a real device (iPhone 14) that it doesn't always - fractional zoom
     factors don't guarantee pixel-perfect font metrics the math assumes, so a few px of
     overflow past the card's own border showed up in practice. Explicitly setting the
     font size here (rather than just whatever 9.8px zooms down to) keeps some safety
     margin against that same sub-pixel slop while still reading comfortably - 8.5px
     logical (~6.6px rendered after zoom), landed between an earlier too-small 8px pass
     and too-large 9px pass. */
  .hub-card .start-rules { font-size: 8.5px; }
}

/* quizebra.com root portal (geoquiz/portal/index.html) - the umbrella page
   above every topic hub (currently /football/, /geography/). Topic cards
   reuse .hub-card/.hub-grid/.hub-wrap as-is for the outer card/grid sizing
   (2 cards today, same 480px-fits-exactly assumption those already
   document) - a plain, non-linking .card now (the 4 GAMES inside are each
   their own clickable card instead, direct instruction 2026-09-14, a
   reversal of this page's first draft). */
.portal-logo-icon { width: 120px; } /* bigger than the standard 61.425px hub
  icon - the one page where the logo itself is the main character, not a
  small mark beside a topic title */
/* .portal-header (descendant selector, not just .portal-wordmark alone) -
   needed to actually beat .site-header h1's own (0,1,1) specificity, which
   otherwise silently wins over a bare 1-class .portal-wordmark rule and
   leaves this whole block dead (found via review, 2026-09-14: the page was
   rendering the shared 42.29px/0.02em sizing the whole time, not this). */
.portal-header .portal-wordmark {
  font-size: 40px; /* .site-header h1's shared 42.29px was fitted to a
    specific per-quiz kicker width, not relevant here - picked by eye instead
    for a page with no matching width to hit */
  letter-spacing: 0.06em; /* a bit looser than the shared 0.02em default - a
    single, short, all-caps brand word can afford (and reads better with)
    more breathing room than a longer quiz title needs */
}
/* Single-sentence tagline (2026-09-14, replaces an earlier 3-segment strip
   design that was tried, shipped, then rejected outright in favor of one
   flowing sentence), 2 lines spanning the full 480px header width, framed
   by the same top/bottom hairlines the earlier strip design used. */
.tagline-line {
  width: 100%;
  text-align: center;
  font: 400 12px "Inter", sans-serif;
  color: var(--text); /* same color as .topic-mini-title (e.g. "FOOTBALL") -
    was var(--text-dim), direct instruction 2026-09-14 to match line 1's own
    non-highlighted color (see .tagline-line1 below) on line 2 too */
  line-height: 1.5;
  margin: 16px 0 0; /* pushes the whole block (both hairlines + both text
    lines) down 16px - direct instruction 2026-09-14. Margin, not transform -
    the card grid below should track this shift too, same reasoning as
    .topic-mini-header's own margin-bottom elsewhere in this file. */
  padding: 6px 0; /* was 10px - hairlines sit closer to the text now, direct
    instruction 2026-09-14 */
  border-top: 1px solid var(--panel-edge);
  border-bottom: 1px solid var(--panel-edge);
}
/* 3 separate highlight colors (direct instruction, 2026-09-14), tying each
   phrase to what it's actually about: "New challenges" -> football's own
   accent, "new chances" -> geography's own accent (LITERAL #7ec6ff/#6ee7b7
   here, not var(--accent) - this header sits above both topic-cards, not
   inside either one's own --accent-scoped subtree, so the variable would
   just resolve to the geography default for both otherwise). "#1 spot" gets
   its own one-off pink (#ff5b80, not either topic's accent) since it isn't
   specific to either topic - every game on both sides has its own #1 spot. */
.tagline-accent-geo { color: #6ee7b7; font-weight: 700; }
.tagline-accent-football { color: #7ec6ff; font-weight: 700; }
.tagline-accent-spot { color: #ff5b80; font-weight: 700; }
/* First line only, 1px bigger than .tagline-line's shared 12px base - both
   sizes trimmed down 1px again (13px/12px -> 12px/11px, direct instruction
   2026-09-14) since the text overall read a bit too large. Both lines share
   the same var(--text) color (set on .tagline-line itself, above),
   redeclared here only for the font-size bump. */
.tagline-line1 { font-size: 13px; color: var(--text); }
/* Line 1 (.tagline-line1) must never wrap to 2 lines (direct instruction,
   2026-09-14) - at the narrow 375-511px tier's own worst case (375px
   viewport, ~369px actually available inside .wrap after that tier's own
   -11px/+22px trick, see the earlier @media block), the base 12px/13px pair
   overflows by ~17px and wraps. Shrunk here in 0.5px steps until it
   empirically fit with a real safety margin (12px/11px measured ~356px
   natural width, comfortably under 369px) - top line always exactly 1px
   bigger than the bottom line, same pairing as the base sizes. Placed AFTER
   the base rules above (not inside that earlier @media block near the top
   of this file) - same selector, same specificity, so source order is what
   decides the tie; put here, this wins at matching widths since it comes
   later. */
@media (min-width: 375px) and (max-width: 511px) {
  .tagline-line { font-size: 11px; }
  .tagline-line1 { font-size: 12px; }
  .portal-about p { font-size: 11px; }
  .site-footer { font-size: 11px; }
}
.topic-card { text-align: center; }
/* Background inversion (direct instruction, 2026-09-14): the outer topic
   card now blends into the PAGE background instead of standing out - #1a1a1a
   matches body's own background exactly (same token .hub-card-page-bg
   already uses for this one-off case elsewhere), overriding .card.hub-card's
   own var(--bg) default. The 4 game-cards inside take up that vacated
   var(--bg) instead (see .game-mini-card) - the visual "this is a card" cue
   moves from the topic level down to the game level, where the actual click
   targets are. 3 classes (not just .topic-card) to reliably beat
   .card.hub-card's own 2-class background rule - same reasoning as that
   rule's own comment on why it isn't just .hub-card. */
/* Top-to-bottom gradient, #1a1a1a to #0f0f0f (direct instruction,
   2026-09-14) - both lighter than the pure-black page behind, and #0f0f0f
   matches .card.hub-card's own var(--bg) exactly at the card's bottom edge
   (so it reads as converging back to the "standard card color" by the time
   the game-cards start, rather than an arbitrary unrelated endpoint). */
.card.hub-card.topic-card { background: linear-gradient(to bottom, #1a1a1a 0%, #0f0f0f 100%); }
/* Each topic card uses ITS OWN topic's real accent color, not the shared
   stylesheet's default mint (direct instruction, 2026-09-14) - overriding
   the --accent custom property here means every descendant that already
   reads var(--accent) (the mini-kicker text, .game-mini-card's own hover
   border) picks up the right color automatically, no need to touch those
   rules themselves. Geography needs no override - the shared stylesheet's
   own default (#6ee7b7) already IS geography's accent. */
.topic-card-football { --accent: #7ec6ff; } /* matches football/css/style.css's own --accent exactly */
/* Each topic card's own top section is a scaled-down replica of that topic's
   REAL page header (logo/name/kicker - same 3 elements, same relative
   proportions) rather than a bare title - the card doubles as a small
   preview of that topic's own branding, not just a label. Sized
   independently of .site-logo-mark/.site-header h1/.kicker (not a scaled
   clone via zoom/transform) since those are tuned for a wider column with
   nothing else sharing its vertical space - here it sits above 4 game
   cards, so it needs to be shorter, not just narrower. */
.topic-mini-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 8px; /* was 20px, then 4px (-16px), then 10px (+6px), now
    8px (-2px) - all direct instructions, 2026-09-14 - net -12px from the
    original 20px. Adjusting this gap (not a transform) so the whole card's
    height tracks the games' position, rather than leaving/losing empty
    space at the card's bottom. */
}
/* The title is the clickable unit (direct instruction, 2026-09-14) - the
   kicker stays plain text below, not part of the link. No logo here (removed
   per a later direct instruction, 2026-09-14 - the two subpages keep theirs,
   this card just didn't need a second copy of it above its own title). */
.topic-mini-link {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-decoration: none;
  color: inherit;
  transition: transform 0.12s ease, opacity 0.12s ease;
}
@media (hover: hover) {
  .topic-mini-link:hover { opacity: 0.85; transform: translateY(-2px); }
}
/* The title's own 34px and the kicker's font-size below are both derived from
   ONE scale factor (34/42.29 = 0.804, direct instruction 2026-09-14: "same
   proportions and spaces as the real page") against the REAL page header's
   own measured values (title-to-kicker gap 2px, kicker 12px - measured live
   off quizebra.com/geography/, not read off the CSS file, to be sure) -
   .topic-mini-title's own 34px (unscaled) is what that factor is computed
   FROM, not a value derived here. */
/* BIGGER than the game titles below (34px vs .start-title's 26px, direct
   instruction 2026-09-14 - was initially smaller, a real hierarchy miss:
   the topic name is the primary heading of this card, the 4 games are
   subordinate to it, so it needs to read as clearly the largest text in the
   card, not the smallest). Kept exactly 34px (not rescaled) - this value is
   the one fixed point the rest of the mini-header scales FROM. */
.topic-mini-title {
  font: 400 34px "Anton", sans-serif;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin: 0;
  color: var(--text);
}
.topic-mini-kicker {
  display: block;
  font: 700 9.65px "Inter", sans-serif; /* 12px * 0.804 */
  letter-spacing: 0.28em; /* .kicker's own default (em-based, so this already
    scales proportionally with the smaller font-size above with no extra
    tuning needed) */
  color: var(--accent);
  margin-top: 1.6px; /* 2px * 0.804 */
  /* Net -8px (moved up 12px, then back down 4px - both direct instructions,
     2026-09-14) via transform, not margin - transform doesn't affect
     document flow, so nothing below (the game cards) shifts; only this
     element's own painted position moves. */
  transform: translateY(-8px);
}
.topic-games { display: flex; flex-direction: column; gap: 10px; }
/* One real card per game (direct instruction, 2026-09-14) - takes over the
   var(--bg) background .topic-card itself used to have (see above), so
   these read as the clickable units, not the topic card around them. Flat
   pure black (was a 4-step black-to-#1a1a1a progression, one shade per
   card, retired along with the topic-card's own gradient once body itself
   went pure black - "pure black background on all pages including the
   portal") - still clearly distinct from the topic-card's own #0f0f0f
   background around them (this container itself has no background, so the
   gaps between cards show that #0f0f0f through, keeping the real
   separation a flat single color could otherwise have risked losing). A
   still-unshipped game (no href - "Guess the Player" as of 2026-09-14) is a
   plain div here, same "no link, not clickable-looking" convention the real
   hub pages already use for their own not-yet-shipped cards. */
.game-mini-card {
  display: block;
  background: #000000;
  border-radius: 10px;
  padding: 10px 12px;
  text-decoration: none;
  color: inherit;
  transition: transform 0.12s ease, border-color 0.12s ease;
  /* Thin border + small hard-edge shadow, same flat color for both
     (direct instruction, 2026-09-14 - #858585, a lighter grey than
     var(--panel-edge)/#2a2a2a, picked specifically for these cards rather
     than reusing that shared token). Hover still shifts border-color to the
     topic's own accent, unchanged - the shadow stays #858585 even on hover. */
  border: 1px solid #858585;
  box-shadow: 1.5px 1.5px 0 0 #858585;
}
@media (hover: hover) {
  a.game-mini-card:hover { border-color: var(--accent); transform: translateY(-2px); }
}
/* Game title + rules - same COLOR/weight/font-family as a real hub-card's
   own title/rules pair, but font-SIZE is uniform across all 8 rather than
   each fitted individually (direct instruction, 2026-09-14): measured every
   title's/every rules line's true natural single-line width (a temporary
   detached inline-block clone, so a short string's real width isn't masked
   by its container - naive scrollWidth on the actual element reports the
   CONTAINER's width whenever the text is narrower than it, found while
   doing this measurement) against the ~169px available inside a card
   (232px .hub-card - 2*18px .card.hub-card padding - 2*12px .game-mini-card
   padding). Longest title: "Guess the Country" at 199.1px/26px -> needs
   ~21.5px to fit; longest rules: "5 flags - 20s each - fastest wins" at
   188.1px/9.8px -> needs ~8.8px. Both rounded down a bit further (21px/
   8.5px) for a real safety margin against cross-device font-rendering
   variance, then applied to EVERY title/rules pair, not just the two that
   needed shrinking - so all 8 read as one consistent size, not a mix of
   full-size and shrunk-to-fit. .start-rules' own forced white-space:nowrap
   (from .hub-card .start-rules, tuned for a single-title real hub-card,
   where a rules line is guaranteed to fit) is turned back to normal
   wrapping here as a safety net, not the primary fix - this sizing should
   already keep every real string on one line at every supported width
   (verified down to 320px). */
.game-mini-card .start-title { margin: 0; font-size: 21px; }
.game-mini-card .start-rules { margin: 2px 0 0; white-space: normal; font-size: 8px; }

/* Portal-only long-form prose (2026-09-15, Phase 2 SEO/LLM work) - real written
   content below the game-picker cards, for crawlers and curious readers, not
   meant to compete with the cards for attention (hence living below them).
   Reuses .wrap's own 480px column - already a comfortable reading width, no
   separate container needed. */
.portal-about {
  margin-top: 40px;
  padding-top: 32px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  text-align: left;
}
/* Font-size matches .tagline-line's own (12px, 11px at the same narrow
   375-511px tier below) exactly - direct instruction, 2026-09-15: this
   whole section should read at the same subtle size as the portal's own
   "free · no signup · no app to download" line, not its own larger size. */
.portal-about p {
  color: var(--text-soft);
  font-size: 12px;
  line-height: 1.6;
  margin: 0 0 16px;
}
.portal-about h2 {
  color: var(--text);
  font-size: 15px;
  margin: 32px 0 12px;
}
.portal-about h3 {
  color: var(--text);
  font-size: 13px;
  margin: 20px 0 8px;
}
.portal-about a { color: var(--accent); text-decoration: none; }
.portal-about a:hover { text-decoration: underline; }
.portal-about .football-links { --accent: #7ec6ff; } /* matches .topic-card-football's own override exactly */

.start-panel {
  text-align: center;
  padding: 10px 0 0;
}
.start-panel .kicker { display: block; margin-top: 8px; margin-bottom: 2px; }
.start-title {
  font: 400 26px "Anton", sans-serif; /* shared/fixed across every quiz on purpose - titles
    must look the same size next to each other on the hub (quizebra.com/geography/) */
  letter-spacing: 0.02em; /* neutral default - every quiz page overrides this to its own
    fitted value (see e.g. maps/index.html) so THAT title's rendered width lands ~3px
    wider than the widest of the "TODAY'S CHALLENGE" kicker above it and the rules line
    below it (never narrower - looks like a mistake if the title reads as the odd one
    out, but a couple px wider reads as intentional emphasis), same target at every
    breakpoint since neither the kicker's nor .start-rules' font-size ever changes.
    Tracking, not size, is what's tuned per title now - a shorter/longer title needs
    looser/tighter letter-spacing to reach the same target width, rather than each
    rendering at its own different font-size (which looked inconsistent once multiple
    titles sit side by side on the hub). */
  text-transform: uppercase;
  line-height: 1.15;
  margin: 4px 0 2px;
}
.start-rules {
  font: 700 9.8px "Inter", sans-serif; /* fitted to the same kicker width as .start-title */
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: -1px 0 22px;
}
.start-hook {
  font: 700 13px "Inter", sans-serif;
  color: var(--accent);
  background: rgba(110, 231, 183, 0.08);
  border: 1px solid var(--accent-dim);
  border-radius: 10px;
  padding: 10px 14px;
  width: 200px; /* matches the card's content column */
  margin: 30px auto 22px;
}
.start-hook-time { font-size: 1.35em; }
.start-hook-line2 { display: inline-block; font-size: 19.3px; margin-top: 4px; }

.round-row {
  display: flex;
  justify-content: center;
  margin-bottom: 10px;
}
#round-label {
  font: 400 26px "Anton", sans-serif;
  line-height: var(--title-h); /* explicit, shared with .practice-badge's own
    height below - both reference the same variable so the badges' top AND
    bottom land exactly on this title's own top/bottom, by construction
    rather than by eyeballed pixel-tuning (see chat) */
  color: var(--text-soft);
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.flags-recap {
  display: flex;
  justify-content: center;
  align-items: flex-end;
  gap: 8px;
  margin-bottom: 18px;
}
.recap-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.recap-time {
  font: 700 11px "Inter", sans-serif;
  color: var(--text-dim);
}
.recap-time.fast { color: var(--good); }
.recap-time.slow { color: var(--bad); }
/* .recap-item svg - the map quiz's own recap only (see renderMapsRecap in
   geoquiz/maps/js/quiz.js), an inline <svg> instead of an <img> like every other
   quiz's recap - same box treatment either way. */
.recap-item img,
.recap-item svg {
  display: block;
  flex-shrink: 0;
}
/* Border only on the <img>-based recaps (flags/colors) - the map quiz's own <svg>
   recap deliberately has none (see chat), so this isn't in the shared rule above. */
.recap-item img {
  border: 1px solid var(--panel-edge);
}

.flag-viewport {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 2; /* the 1.5 width/height ratio ~43% of real flags already match exactly */
  overflow: hidden;
  background: var(--panel); /* same as the card itself - no separate letterbox band */
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Pre-round "3...2...1" gate (direct instruction, 2026-09-14, replacing an
   earlier low-opacity-flag + button design entirely - real device testing
   found that version had a real bug: the timer label's pixel position, only
   knowable once JS could measure the (still-loading) layout, visibly jumped
   on phones). Only for the very first round of the DAILY session, NOT
   extra/practice rounds (direct instruction) - see startRound/
   beginFirstRound's own comments. Takes over the exact spot the flag
   viewport/timer/options would occupy (all 3 hidden while this shows, see
   beginRound), rather than fading/dimming them - same aspect-ratio as
   .flag-viewport itself so it isn't an arbitrary size, just a solid var(--bg)
   panel (darker than the card's own var(--panel), reading as a distinct
   inset "screen") with one big number on it - no button, the panel itself is
   the tap target to skip ahead (see scheduleRoundStart in quiz.js). */
.gate-panel {
  width: 100%;
  aspect-ratio: 3 / 2;
  background: var(--bg);
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.gate-number {
  font: 400 120px "Anton", sans-serif;
  color: var(--accent);
  line-height: 1;
}
/* the flag frame: the flag exactly as it sits at zoom 1, contained in the viewport.
   tier crops are positioned inside this in percentages against its own untransformed
   box, so nothing needs recomputing on resize. no border here - a border on a scaling
   element renders as a scaled slab during zoom, and would misalign between tiers. */
/* width/height are set in JS (object-fit:contain math) - tier <img> children are
   position:absolute and contribute no intrinsic size, so CSS aspect-ratio alone
   can't contain-fit this against the box. */
.frame {
  position: relative;
  will-change: transform;
}
.frame img {
  position: absolute;
  display: block;
  backface-visibility: hidden;
}
.flash-overlay {
  position: absolute;
  inset: 0;
  background: var(--bad);
  opacity: 0;
  pointer-events: none;
}
.flash-overlay.hit { animation: flash-red 0.4s ease-out; }
@keyframes flash-red {
  0% { opacity: 0.55; }
  100% { opacity: 0; }
}

.penalty-popup {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font: 400 46px "Anton", sans-serif;
  letter-spacing: 0.02em;
  color: var(--bad);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.8), 0 0 22px rgba(255, 77, 94, 0.55);
  opacity: 0;
  pointer-events: none;
  z-index: 5;
}
.penalty-popup.show { animation: penalty-pop 0.9s ease-out; }
@keyframes penalty-pop {
  0%   { opacity: 0;   transform: translate(-50%, -50%) scale(0.6); }
  15%  { opacity: 1;   transform: translate(-50%, -50%) scale(1.15); }
  30%  { opacity: 1;   transform: translate(-50%, -50%) scale(1); }
  75%  { opacity: 1;   transform: translate(-50%, -62%) scale(1); }
  100% { opacity: 0;   transform: translate(-50%, -92%) scale(0.92); }
}

.timer-wrap {
  position: relative;
  margin-top: 44px; /* room above the track for the traveling time label */
}
.timer-track {
  height: 10px;
  border-radius: 6px;
  background: #050505;
  border: 1px solid var(--panel-edge);
  overflow: hidden;
}
.timer-fill {
  height: 100%;
  width: 100%;
  transform-origin: left;
  background: var(--accent);
  transition: background 0.3s linear;
}
.timer-fill.penalty-jump {
  transition: transform 0.4s cubic-bezier(0.22, 0.9, 0.4, 1), background 0.3s linear;
}
#timer-num {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 0;
  transform: translateX(-50%);
  font: 700 13px "Inter", sans-serif;
  color: var(--accent);
  white-space: nowrap;
}
#timer-num::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 100%;
  transform: translateX(-50%);
  width: 2px;
  height: 6px;
  background: currentColor;
}

.options {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: 16px;
}
.opt-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font: 400 20px "Anton", sans-serif;
  line-height: 1.2;
  letter-spacing: 0.01em;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--panel-edge);
  border-radius: 12px;
  padding: 18px 14px;
  min-height: 100px; /* fits two lines of the longest country names - used as the
    uniform default height for every option button, not just the long ones */
  cursor: pointer;
  transition: transform 0.12s ease, background 0.12s ease, border-color 0.12s ease;
}
/* hover:hover excludes touch - without it, tapping a button on mobile can leave its
   hover state visually "stuck" (a synthetic hover some mobile browsers apply on tap
   and only clear on the next tap elsewhere) instead of only lighting up for a mouse. */
@media (hover: hover) {
  .opt-btn:hover:not(:disabled) { border-color: var(--accent); transform: translateY(-1px); }
}
.opt-btn:active:not(:disabled) { transform: translateY(0); }
.opt-btn:disabled { cursor: default; opacity: 0.55; }
.opt-btn.correct { background: var(--good); border-color: var(--good); color: #06210f; }
.opt-btn.wrong { background: var(--bad); border-color: var(--bad); color: #2a0509; animation: shake 0.35s ease; }
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-6px); }
  75% { transform: translateX(6px); }
}

.result-panel { text-align: center; margin-top: 10px; }
.result-time-label {
  font: 700 12px "Inter", sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin: 0 0 -2px;
}
.result-title {
  font: 400 32px "Anton", sans-serif;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin: 0 0 6px;
  color: var(--accent);
}
.result-title.wrong { color: var(--bad); }
.result-sub {
  font: 400 15px "Inter", sans-serif;
  color: var(--text-soft);
  margin: -10px 0 16px; /* negative top margin (not result-title's own margin-bottom) so this
    only shifts the final daily-summary layout - result-title's spacing is shared with the
    hidden-result-sub per-round "Correct!"/"Time's Up" screen, which should stay unaffected */
}

.btn-primary {
  display: block; /* width:100% below needs a block box to mean anything - without this,
    an inline element (e.g. the hub's <span class="btn-primary">, not a real <button>)
    would silently ignore it and just shrink to fit its own text instead */
  font: 700 15px "Inter", sans-serif;
  color: #0a0f0c;
  background: var(--accent);
  border: none;
  border-radius: 12px;
  padding: 14px 22px;
  cursor: pointer;
  width: 100%;
  text-align: center;
}
#start-btn { width: 200px; margin-top: 14px; } /* matches .start-hook's width - the card directly above it */
@media (hover: hover) {
  .btn-primary:hover { filter: brightness(1.08); }
}

.btn-secondary {
  font: 700 15px "Inter", sans-serif;
  color: var(--text);
  background: #2a2a2a;
  border: none;
  border-radius: 12px;
  padding: 14px 22px;
  cursor: pointer;
  width: 100%;
  margin-bottom: 10px;
}
@media (hover: hover) {
  .btn-secondary:hover { background: #333333; }
}

/* On the final-summary screen specifically (share-btn visible there and only there),
   Share Results leads as the primary/green action and Keep Playing steps back to
   secondary/grey - derived purely from share-btn's own visibility, not a JS class
   toggle, so it can never drift out of sync across navigation paths. */
#share-btn:not([hidden]) {
  background: var(--accent);
  color: #0a0f0c;
}
@media (hover: hover) {
  #share-btn:not([hidden]):hover { filter: brightness(1.08); background: var(--accent); }
}
.result-panel:has(#share-btn:not([hidden])) #next-btn {
  background: #2a2a2a;
  color: var(--text);
}
@media (hover: hover) {
  .result-panel:has(#share-btn:not([hidden])) #next-btn:hover { background: #333333; filter: none; }
}

.result-actions {
  display: flex;
  gap: 10px;
}
.result-actions .btn-primary { width: auto; flex: 1; }
/* Share Results sharing this row with Keep Playing (final-summary screen only - see
   showFinalResults' own comment) is the same "split the row in half" treatment
   .btn-primary already gets above - width:100%/margin-bottom:10px are both only
   meaningful for .btn-secondary's other (stacked, standalone) uses elsewhere. */
.result-actions .btn-secondary { width: auto; flex: 1; margin-bottom: 0; }
.practice-back-link {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1;
  font: 700 14px "Inter", sans-serif;
  color: var(--text);
  text-decoration: none;
  text-align: center;
  white-space: nowrap;
  background: #2a2a2a;
  border-radius: 12px;
  padding: 14px 22px;
}
/* The in-round instance (#practice-back-link-playing) sits alone, full width - not
   sharing a row with "Play Another" the way the post-round .practice-back-link does -
   so it doesn't need that one's cramped fitted-to-half-width font-size. */
#practice-back-link-playing {
  margin-top: 8px;
  font-size: 15px;
}
/* Deliberately NOT .practice-back-link's own grey-pill button look - plain accent-
   colored text instead, no background/padding box, so it reads as a quieter "by the
   way, there's more" than a third button competing with Share Results/Keep Playing
   right above it. Only ever shown on the real daily-summary screen (see
   showFinalResults in quiz.js). white-space left at its normal default (unlike every
   .practice-back-link use, all deliberately nowrap) since this text is longer than
   any of those and should wrap to a second line on narrow screens rather than overflow. */
#hub-link {
  display: block;
  margin-top: 14px; /* same 14px on every device, same reasoning as .leaderboard's own
    margin-bottom above - fixed rather than varying by breakpoint */
  text-align: center;
  font: 700 13px "Inter", sans-serif;
  line-height: 1.3;
  color: var(--accent);
  text-decoration: none;
}
/* .card's own padding-bottom (24px base, still 24px at 480px, 10px at 375px/short
   screens - see those breakpoints below) would otherwise leave a DIFFERENT gap below
   this text depending on device, same problem .leaderboard's margin-bottom had above -
   fixed to 14px on every device instead, same :has()-scoped-override technique
   .card:has(#start-panel:not([hidden])) already uses elsewhere in this file. Only
   applies while hub-link is actually shown (the real daily-summary screen), so every
   other screen's card padding is untouched. */
.card:has(#hub-link:not([hidden])) { padding-bottom: 14px; }
@media (hover: hover) {
  #hub-link:hover { text-decoration: underline; }
}
@media (hover: hover) {
  .practice-back-link:hover { background: #333333; }
}

/* Anchors .practice-badge below - a plain block, no padding/margin of its own,
   so its content-box edges exactly match .play-area's, which (unlike .card)
   already IS the padded content column (round-row/options/result-actions all
   sit flush against it) - see .practice-badge's own comment for why that's
   what the badges should align to. */
.play-area {
  position: relative;
  --title-h: 30px; /* #round-label's own line-height, shared with .practice-badge's
    height below (see both their own comments) - single source of truth for the
    two-line badge's exact vertical match to the title, redefined per breakpoint
    alongside #round-label's own font-size changes. */
}

/* Persistent corner badges, shown only during a practice/extra episode (see
   quiz.js's showPracticeBadges) - left is this player's own best 5-round
   TOTAL today (daily's real completion counts too, even though the badge
   itself only ever appears during practice - see quiz.js), right is the
   genuine all-time best across every real daily completion ever OR any
   extra-episode total that beat it (see maybeUpdateSessionBest's own
   comment - "your best" can never show ahead of "all-time"), fetched once
   via api/all-time-best. Deliberately aligned to .play-area's own edges, not
   .card's outer padding - see chat: left matches the result screen's own
   "Play Another" button, right matches "Back to today's results" - i.e. the
   badges sit exactly within the same content column every other element in
   this card already uses, not a separate wider frame of their own. Top AND
   bottom match #round-label's own top/bottom exactly - both this height and
   that line-height reference the shared --title-h var (see chat), not a
   pixel-tuned guess, so a two-line label+value stack can be vertically
   centered inside without drifting off the title's own box. */
.practice-badge {
  position: absolute;
  top: 0;
  height: var(--title-h);
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center; /* horizontally centers both lines - see chat */
  justify-content: center;
  gap: 1px;
  padding: 0 7px;
  background: var(--bg);
  border: 1px solid var(--panel-edge);
  border-radius: 6px;
  white-space: nowrap;
  z-index: 2; /* sits above .round-row/.result-actions content, never behind it */
}
.practice-badge-left { left: 0; }
.practice-badge-right { right: 0; }
.practice-badge-label {
  font: 700 8px "Inter", sans-serif;
  letter-spacing: 0.02em;
  color: var(--text-dim);
  transform: translateY(1px); /* nudge down only - doesn't affect layout/the value line below */
}
.practice-badge-value {
  font: 700 12px "Inter", sans-serif;
  color: var(--accent);
}
@media (max-width: 375px), (max-height: 700px) {
  .practice-badge { padding: 0 6px; }
}

.leaderboard {
  list-style: none;
  margin: 18px 0 20px; /* bottom is fixed at 20px on every breakpoint (see the narrow-
    screen override below, which only still adjusts the top value) - the gap down to
    Share Results/Keep Playing should read the same on phone and desktop */
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  text-align: left;
}
.leaderboard-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  background: transparent;
  border: 1px solid var(--panel-edge);
  border-radius: 8px;
}
/* The #1 row always renders first (rows are already rank-sorted) - direct
   instruction, 2026-09-14, applies on every subpage that shows a real
   leaderboard, and also to a hub-card's own single-row "#1" preview (see
   .hub-card .leaderboard) since that's always rank 1 too. */
.leaderboard-row:first-child { background: #181818; }
.leaderboard-row.you { border-color: var(--accent-dim); }
.lb-rank { font: 700 11px "Inter", sans-serif; color: var(--text-soft); width: 20px; }
.leaderboard-row.you .lb-rank { color: var(--accent); }
/* Width-capped with a native ellipsis, not a character-count cap in JS (see chat,
   2026-09-10) - a character count has no reliable relationship to rendered width (an
   emoji-heavy name can be far WIDER than the same number of plain letters), so a count-
   based cap either truncates too early for narrow text or overflows for wide text.
   min-width:0 is required for this to work at all - a flex item's default min-width is
   auto (its own content's natural width), which would silently prevent this from ever
   shrinking below the untruncated text's full width no matter what overflow/ellipsis
   says. No explicit max-width here - the column's own natural flex-allocated width
   (different in the hub's 188px-wide preview row vs. the real results-screen
   leaderboard's wider one) IS the cap, so this applies correctly everywhere .lb-name is
   used without hardcoding a second width value that could drift out of sync with the
   row's own actual layout. */
.lb-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font: 400 12px "Inter", sans-serif;
  color: var(--text-soft);
}
/* Fixed width + right-aligned + never-shrinks - every row's time lands in the same
   column regardless of that row's own name length, INCLUDING the one row currently
   being edited (see .lb-name-edit below) - without this, the wider 16px+ name-edit
   input crowds .lb-time right up against - and in narrow cases past - the row's own
   border. 34px fits up to "999.9s"/"100.0%" with a little breathing room; text-align
   keeps shorter values (the common case) flush with the widest one instead of drifting
   left as the string gets shorter. */
.lb-time {
  flex-shrink: 0;
  width: 34px;
  text-align: right;
  font: 400 12px "Inter", sans-serif;
  color: var(--text-soft);
}

.lb-name-edit { display: flex; align-items: center; gap: 6px; }
/* 16px base (not the visually-matched 12px every other small label on this page uses) -
   below 16px, iOS Safari auto-zooms the whole page in on focus and (confirmed on a real
   device, see chat) there is no reliable way to programmatically zoom back out again
   afterward - not a bug in this site's own code, WebKit deliberately treats a focus-
   triggered zoom as user-initiated and only clears it on a full page reload or device
   rotation. Staying at/above 16px sidesteps the whole problem: nothing to undo. The
   :focus rule below recreates the "bigger while editing" feel this quiz wants anyway,
   just via ordinary CSS transitioning back on blur - reliable on every device, unlike
   the OS zoom. */
.lb-name-input {
  flex: 1; min-width: 0; font: 700 16px "Inter", sans-serif; color: var(--text);
  background: transparent; border: 1px solid var(--panel-edge); border-radius: 6px;
  padding: 3px 6px;
  transition: font-size 0.15s ease, padding 0.15s ease;
}
.lb-name-input:focus {
  outline: none;
  border-color: var(--accent);
  font-size: 20px;
  padding: 6px 8px;
}
.lb-name-save {
  flex: 0 0 auto; font: 700 11px "Inter", sans-serif; color: var(--accent);
  background: transparent; border: 1px solid var(--accent-dim); border-radius: 6px;
  padding: 3px 8px; cursor: pointer;
}
.lb-name-save:disabled { opacity: 0.6; cursor: default; }
@media (hover: hover) {
  .lb-name-save:hover:not(:disabled) { border-color: var(--accent); }
}

.your-rank-note {
  font: 700 12px "Inter", sans-serif;
  color: var(--text-dim);
  margin: 0 0 16px; /* bottom fixed at 16px on every device (no narrow-screen override
    below anymore) - same reasoning as .leaderboard's own margin-bottom above: this is
    the gap down to Share Results/Keep Playing on the (less common) screen where the
    player ranked outside the top 5, and it should read the same on phone and desktop */
}

/* Find the Color quiz - the flag itself is rendered to a <canvas> (see quiz.js's
   buildColorRound/repaintCanvas), not the flag-zoom quiz's <img>/tiered-crop setup,
   since the target region needs live per-pixel recoloring as the hue slider moves -
   there's no equivalent "just swap a CSS variable" trick for an arbitrary SVG region. */
.color-flag-wrap {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--panel);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
}
.color-flag-wrap canvas { display: block; max-width: 100%; max-height: 100%; }

/* One shared font-size for every flag's caption (not auto-fit per name) -
   calibrated ONCE against the longest real country name ("Saint Vincent and
   the Grenadines", the actual longest by measured render width in this
   font/weight, not just character count - "Central African Republic" and
   "Bosnia and Herzegovina" are the next-longest and noticeably shorter) so
   that name's own rendered width equals .color-flag-wrap's rendered width at
   a plain 3:2 flag (the most common real-world flag ratio - see the ratio
   field in colors.json), rounded DOWN to a whole px for a small safety
   margin. Every other (shorter) name just renders narrower than the flag at
   this same size, which is intentional - a per-name auto-fit would make
   "Chad"'s caption enormous next to a small "Saint Vincent..." on another
   round, inconsistent in a way that reads as a bug, not a feature. Flags
   whose OWN ratio isn't 3:2 render narrower than the box (see
   .color-flag-wrap's centering) - the caption still sizes off the box, not
   that particular flag's own rendered width, so it can end up visibly wider
   than a squarer flag directly above it. Accepted as-is (see chat) rather
   than adding per-round dynamic sizing for that case.
   26px here / 22px at 480px / 19px at 375px are each independently
   calibrated the same way against .color-flag-wrap's own actual rendered
   width at that tier (430px / 368px / 310px) - not a single value scaled by
   guesswork, so the "longest name = flag width" relationship holds at every
   breakpoint, not just the primary one. */
.color-flag-caption {
  font: 700 26px "Inter", sans-serif;
  /* No letter-spacing - the calibration above measures plain canvas
     measureText width with none, found the hard way: adding even a small
     0.02em tracking (reflexively copied from other captions on this page)
     made the longest name overflow by 10px, since that extra per-character
     gap was never part of the measurement it was calibrated against. */
  color: var(--text-soft);
  text-align: center;
  white-space: nowrap;
  margin: 10px 0 0;
}

.hue-slider-wrap { margin-top: 16px; }
/* A 7-stop hsl() gradient (0/60/.../360°) approximates a true continuous hue sweep
   closely enough at this size - linear-gradient interpolates each adjacent pair in
   RGB space, not around the hue circle, but with stops every 60° the visual
   difference from a "true" spectrum is negligible. */
.hue-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 14px;
  border-radius: 7px;
  /* Rotated so both ends land on ~290° (violet/purple), not both on pure red -
     see quiz.js's SLIDER_HUE_OFFSET comment for the data behind that specific
     value. Stops are hue = (290 + t*360) mod 360 at t = 0, 1/6, ..., 1 - same
     7-stop approximation as before, just phase-shifted, not a different
     gradient technique. */
  background: linear-gradient(to right,
    hsl(290 100% 50%), hsl(350 100% 50%), hsl(50 100% 50%), hsl(110 100% 50%),
    hsl(170 100% 50%), hsl(230 100% 50%), hsl(290 100% 50%));
  border: 1px solid var(--panel-edge);
  cursor: pointer;
}
.hue-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--text);
  border: 3px solid var(--bg);
  box-shadow: 0 0 0 1.5px var(--text);
  cursor: pointer;
}
.hue-slider::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--text);
  border: 3px solid var(--bg);
  box-shadow: 0 0 0 1.5px var(--text);
  cursor: pointer;
}
.hue-slider:disabled { opacity: 0.5; cursor: default; }

#lock-in-btn { margin-top: 16px; }

/* Per-round reveal (inside the shared .result-panel, alongside "Correct!"/"Time's
   Up" for the other quizzes - this quiz's equivalent is the match% itself, see
   quiz.js's showRoundResult) - hidden again on the final daily-summary screen. */
.color-result-flags {
  display: flex;
  justify-content: center;
  gap: 14px;
  margin: 14px 0;
}
.color-result-flag-col { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.color-result-flag-col canvas,
.color-result-flag-col img {
  display: block;
  width: 130px;
  height: auto;
  border: 1px solid var(--panel-edge);
  border-radius: 4px;
}
.color-result-caption {
  font: 700 10px "Inter", sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
}
/* Mobile: the flag/timer/options stay visible underneath the result panel between
   rounds (so the correct-answer highlight and the "which one did I pick" moment are
   still visible - they're not hidden until the next round actually starts), which is
   the whole reason this screen is tall. Desktop has room for that; a short phone
   screen doesn't. Compact spacing/type across the board rather than hiding content. */
@media (max-width: 480px) {
  body { padding: 40px 14px 32px; }
  .kicker { margin-bottom: 7px; }
  .card { padding: 14px 16px 24px; }
  .wrap:has(#play-area:not([hidden])) .site-header h1 { font-size: 24px; } /* matches .result-title's 480px-tier size */
  .start-rules { margin-bottom: 18px; }
  .start-hook { margin: 0 auto 18px; }
  .round-row { margin-bottom: 14px; }
  .play-area { --title-h: 31px; } /* keeps pace with #round-label's own font-size bump below */
  #round-label { font-size: 27px; }
  .flags-recap { margin-bottom: 11px; }
  .gate-number { font-size: 90px; }
  .timer-wrap { margin-top: 64px; }
  .options { margin-top: 23px; gap: 16px; }
  .opt-btn { padding: 16px 10px; font-size: 20px; min-height: 100px; }
  .result-panel { margin-top: 6px; }
  .result-title { font-size: 24px; margin-bottom: 4px; }
  /* Only the final-summary total time, not the per-round "Correct!"/"Time's Up"
     text which reuses .result-title - #result-time-label only shows on the
     final screen, so it's the marker for "this is the summary page". */
  .result-panel:has(#result-time-label:not([hidden])) .result-title { margin-top: -4px; }
  .result-sub { font-size: 13px; margin-bottom: 8px; }
  .btn-primary { padding: 8px 18px; }
  .btn-secondary { padding: 8px 18px; margin-bottom: 8px; }
  /* only shrink when the row genuinely has two visible buttons (Play Another +
     Back to today's results) - "Keep Playing" alone shouldn't be affected */
  .result-actions:has(#practice-back-link:not([hidden])) .btn-primary { font-size: 13.4px; }
  /* Same reasoning, same value - the OTHER two-visible-buttons pairing this row can
     hold (Share Results + Keep Playing, the real daily-summary screen - see
     showFinalResults). Both buttons need it here, not just .btn-primary, since
     .btn-secondary is the one newly sharing this row rather than sitting full-width
     alone above it. */
  .result-actions:has(#share-btn:not([hidden])) .btn-primary,
  .result-actions:has(#share-btn:not([hidden])) .btn-secondary { font-size: 13.4px; }
  .practice-back-link { padding: 8px 18px; font-size: 11.5px; }
  #practice-back-link-playing { margin-top: 8px; padding: 8px 18px; font-size: 15px; }
  .leaderboard { margin-top: 10px; } /* bottom stays the base rule's 20px - same on every device, see its own comment */
  .color-flag-caption { font-size: 22px; } /* recalibrated at this tier's own .color-flag-wrap width (368px) - see the base rule's comment */
}

/* iPhone 13 mini and similarly short/narrow phones: this is the one tier allowed
   to scroll - shrink toward the pre-480px numbers instead of fighting for a no-
   scroll fit that would cramp every larger phone in the tier above. */
@media (max-width: 375px), (max-height: 700px) {
  body { padding: 22px 12px 18px; }
  .card { padding: 10px 12px; }
  .start-rules { margin-bottom: 14px; }
  .start-hook { padding: 8px 12px; margin-bottom: 14px; }
  .round-row { margin-bottom: 10px; }
  .gate-number { font-size: 72px; }
  .timer-wrap { margin-top: 36px; }
  .options { margin-top: 22px; gap: 8px; }
  .opt-btn { padding: 10px 8px; font-size: 15px; min-height: 80px; }
  #practice-back-link-playing { margin-top: 8px; }
  .color-flag-caption { font-size: 19px; } /* recalibrated at this tier's own .color-flag-wrap width (310px) - see the base rule's comment */
}

/* ---------------------------------------------------------------------
   Rank the Country - 8-country/8-category ranking-assignment puzzle
   (sibling of the flag/map/color quizzes above, but no timer, no
   multiple-choice, one round per day instead of five). Card/flag/name/
   bucket rules below are LOCKED exactly as iterated live in that game's
   design mockup - see the geoquiz-fourth-game-superlatives-design memory
   for the full spec (concept, scoring, country-selection algorithm) this
   implements.
   --------------------------------------------------------------------- */
/* .rank-goal (goal + how scoring works) sits above .rank-instructions (the
   bare tap-to-place mechanic) - two separate lines/purposes, not merged
   into one paragraph, so either can be re-worded independently.

   36px top PADDING on .rank-goal, not margin - the shared .play-area has no
   border/padding of its own, so a margin-top here would collapse straight
   through it (confirmed via getBoundingClientRect: with margin, this
   element's own top, the badges' top, AND .play-area's top were all the
   exact same pixel - the margin was pushing .play-area's whole box down the
   page instead of creating space inside it, so the badges - position:
   absolute, top:0 relative to .play-area - moved down right along with it
   and stayed exactly as overlapped as before). Padding never collapses, so
   it reliably clears the practice-badge corner boxes (~30-31px tall)
   sitting at .play-area's own top:0 - this line runs the full card width,
   so its start/end sat directly under them otherwise. */
.rank-goal {
  font: 400 12px "Inter", sans-serif;
  line-height: 1.4;
  color: var(--text-dim);
  text-align: center;
  margin: 0 0 8px;
  padding-top: 36px;
}
.rank-instructions {
  font: 700 12.5px "Inter", sans-serif;
  color: var(--text-soft);
  text-align: center;
  margin: 0 0 16px;
}

/* The 8 draggable/tappable country cards - tap to select (highlights it,
   dims the rest, see .ccard.selected/.dimmed), then tap an empty category
   to place it there (removed from this pool once placed). Grid, not flex-
   wrap - flex-wrap's line-breaking depends on available width, which at
   this card's own content width let 5 fit on the first row and only 3 on
   the second (found as a real bug on wider phones/desktop) instead of a
   steady 4x2. grid-template-columns: repeat(4, auto) sizes each column to
   its card's own natural width (74px, see .ccard) rather than stretching
   to fill the row, so the grid stays a tight 4-wide block, centered via
   justify-content instead of stretching edge to edge. */
.rank-pool {
  display: grid;
  grid-template-columns: repeat(4, auto);
  justify-content: center;
  gap: 9px;
  margin-bottom: 22px;
  min-height: 76px; /* keeps .rank-grid below from jumping up as the pool empties */
}
.ccard {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  background: #1e1e1e;
  border: 1px solid var(--panel-edge);
  border-radius: 12px;
  padding: 10px 6px 8px;
  width: 74px;
  cursor: pointer;
  transition: border-color 0.12s ease, opacity 0.12s ease, background 0.12s ease;
}
.ccard.selected { border-color: var(--accent); }
/* A category was tapped first (see quiz.js's onBucketClick) - every pool
   country becomes a valid tap target for it, same visual language as a
   targetable bucket below. */
.ccard.targetable { border-color: var(--accent); background: rgba(110, 231, 183, 0.08); }
.ccard.dimmed { opacity: 0.35; }
.flag-wrap {
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ccard-flag { border-radius: 3px; flex-shrink: 0; display: block; }
/* Fixed-height zone, bottom-aligned - every card's name sits on the same
   baseline regardless of its own auto-fit font-size (see quiz.js's
   fitName), rather than a long name pushing the whole card taller. */
.ccard-name-zone {
  width: 100%;
  height: 13px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
.ccard-name {
  font-size: 10.5px; /* auto-shrunk down to a 4px floor by quiz.js's fitName -
    a real JS measurement, not a hand-tuned per-name value */
  font-weight: 600;
  text-align: center;
  line-height: 1;
  white-space: nowrap;
  display: inline-block;
  color: var(--text);
}

/* The 8 category buckets - 4 pairs (Population/Area/Coastline/Peak), each
   pair its own row, lowest-value bucket on the left, highest on the
   right - two separate bordered cards, not one divided module. Only the
   live interactive placement grid uses this now - the reveal/comparison
   screen builds its own compact list instead (see .comparison-* below and
   buildComparisonList in quiz.js), not these bucket cards. */
.pair-stack { display: flex; flex-direction: column; gap: 10px; }
.pair-module { display: flex; flex-direction: row; align-items: stretch; gap: 8px; }
.pair-bucket {
  flex: 1;
  border: 1.5px solid var(--panel-edge);
  border-radius: 12px;
  padding: 10px;
  font: 600 11.5px "Inter", sans-serif;
  color: var(--text-dim);
  display: flex;
  align-items: center;
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease, opacity 0.12s ease;
}
/* All empty buckets highlight together while a country is selected in the
   pool above - "the 8 buckets are also highlighted in a way that makes
   clear one has to be selected now" (chat). */
.pair-bucket.targetable { border-color: var(--accent); background: rgba(110, 231, 183, 0.08); }
/* The reverse flow: a category was tapped FIRST (see quiz.js's onBucketClick) -
   this is the one awaiting a country (same look as .targetable, it IS one),
   every other empty bucket dims to make clear it's not the active target. */
.pair-bucket.selected { border-color: var(--accent); background: rgba(110, 231, 183, 0.08); }
.pair-bucket.dimmed { opacity: 0.35; }
.pair-bucket.filled { cursor: default; flex-direction: column; align-items: stretch; gap: 6px; }
.bucket-label {
  font: 600 10px "Inter", sans-serif;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.bucket-country { display: flex; align-items: center; gap: 6px; }
.bucket-flag { height: 18px; width: auto; border-radius: 2px; display: block; flex-shrink: 0; }
.bucket-country-name {
  flex: 1;
  min-width: 0;
  font: 700 12px "Inter", sans-serif;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* "Reverse this move" control - play screen only, the only screen this bucket
   markup renders on now. */
.bucket-undo {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 1px solid var(--panel-edge);
  background: transparent;
  color: var(--text-dim);
  font: 700 13px "Inter", sans-serif;
  line-height: 1;
  cursor: pointer;
}
@media (hover: hover) {
  .bucket-undo:hover { border-color: var(--bad); color: var(--bad); }
}

/* Stays a real, clickable button even while incomplete (not a `disabled`
   attribute) - quiz.js's click handler shows #submit-hint instead of
   submitting in that case, which a genuinely disabled button couldn't do
   (a disabled element never fires click at all). The muted grey (matching
   .btn-secondary's own "not the primary action right now" color, not a
   new palette color) is the "can't be pushed yet" signal; .not-ready is
   removed the moment the last country is placed. */
#submit-btn { margin-top: 18px; }
#submit-btn.not-ready {
  background: #2a2a2a;
  color: var(--text-dim);
}
@media (hover: hover) {
  #submit-btn.not-ready:hover { background: #2a2a2a; filter: none; }
}
.submit-hint {
  font: 700 12px "Inter", sans-serif;
  color: var(--bad);
  text-align: center;
  margin: 10px 0 0;
}

/* Order revised 2026-09-11 (again): back to "Your score: X%" first (shared
   .result-time-label/.result-title, same ids/classes the other 3 games use, so this
   picks up their CSS unmodified - direct instruction to match them exactly), then
   "You beat X%..."/the ranking, then the buttons/hub-link, and now the comparison
   table LAST, below everything else, as its own de-emphasized detail section.
   .result-time-label is the true first visible child of #result-panel in both the
   daily and practice reveal here (pool/grid/goal/instructions are all hidden by then)
   - unlike the other 3 games, which always keep an always-visible #round-label row
   above their own result-panel (sized to already clear the practice badges), this
   game hides everything above result-panel on reveal, so result-time-label needs its
   OWN practice-badge clearance. Scoped via :has(#rank-goal) - an id that only exists
   on this page - so this doesn't add unwanted padding to the other games, which
   share this same stylesheet and already handle their own clearance via round-label. */
.play-area:has(#rank-goal):has(#session-badge:not([hidden])) .result-time-label { padding-top: 36px; }
/* Marks where the card's own bottom edge normally sits in the other 3 games -
   hub-link is their last child, with .card's own 14px padding-bottom (see
   .card:has(#hub-link:not([hidden])) below) landing right under it. This game's
   comparison table now trails past that same point instead, so a real divider line
   stands in for that visual "end of card" boundary - full width, edge to edge like
   the card's own border, via negative margins that cancel .card's own left/right
   padding (24px base; -16px/-12px overrides below match .card's own padding at
   those breakpoints, see .card's own rule for each value). 14px above (matching
   hub-link's distance from that normal card-bottom padding), 20px below (measured
   live: divider's own bottom edge to the "54.7%"/"100%" value text's own top -
   direct instruction). */
#comparison-divider {
  border: none;
  border-top: 1px solid var(--panel-edge);
  margin: 14px -24px 20px;
}
@media (max-width: 480px) {
  #comparison-divider { margin-left: -16px; margin-right: -16px; }
}
@media (max-width: 375px), (max-height: 700px) {
  #comparison-divider { margin-left: -12px; margin-right: -12px; }
}

/* Compact Your-Assignment-vs-Optimal-Assignment comparison (see chat, 2026-09-11) -
   replaces the old two full-width bucket-card sections (no flags, deliberately
   smaller text throughout - the whole point was taking up less space) with one list:
   a header row and every category row share the exact same 3-column grid, so "Your
   Assignment"/"Optimal Assignment" land at the same left edge as the country columns
   below them (see buildComparisonList's own comment in quiz.js for why the header
   needs a leading spacer to make that true, not just matching column widths). */
/* .comparison-header.comparison-row (compound, both classes required), NOT the two
   separate selectors this used to be split across - found a real bug (see chat,
   2026-09-11): the header element carries BOTH classes, and plain .comparison-row's
   own margin-bottom:2px (declared later in this file) was silently overriding
   whatever margin-bottom got set here, since equal-specificity rules resolve by
   source order, not by which one "sounds" more specific to a person reading them.
   The compound selector's higher specificity is what makes this actually win,
   regardless of source order.
   No margin-top override anymore - that was only ever there to pull the header up
   into #reveal-wrap's own padding-top when the table was the panel's first child
   (see #comparison-divider's own comment); now that the table sits at the very
   bottom of the panel instead, #comparison-divider's own margin above already does
   all the positioning, and this header just needs normal spacing below it.
   10.7px - measured live (direct instruction: 16px from the bottom of the "YOUR
   ASSIGNMENT"/"OPTIMAL ASSIGNMENT" text to the top of the first row's country name
   text) - smaller than that 16px target since the two elements' different font
   sizes/line-heights add their own leading on top of this margin; re-measure and
   adjust again if either element's font-size ever changes. */
.comparison-header.comparison-row { margin-bottom: 10.7px; }
/* Both are <p> now (was one <span> with the percentage inline after it) - stacked on
   separate lines instead, percentage first (revised 2026-09-11 - was title first).
   margin:0 resets the browser's own default <p> margin, which would otherwise add
   unwanted extra vertical gap between the two on top of the explicit spacing below. */
.comparison-col-title {
  margin: 0;
  min-width: 0;
  font: 700 9px "Inter", sans-serif;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-soft);
}
/* Centered under the WHOLE column (not just under the title text below it, which
   stays left-aligned to match the country column below IT) - explicit direction.
   13px (was 9px, +4px per direct instruction) - noticeably larger than the title
   below it now that it leads the column instead of following it. margin-bottom (not
   -top, following from now coming first) is what spaces it from the title beneath. */
.comparison-col-pct {
  margin: 0 0 2px;
  text-align: center;
  font: 400 17px "Inter", sans-serif;
  color: var(--accent);
}
/* Optimal Assignment's own value only (see buildComparisonColTitle's muted param in
   quiz.js) - grey like its own .comparison-col-title text right below it, not the
   mint accent color "Your Assignment" keeps (direct instruction: it's always exactly
   100%, so mint there read as a second "your own result" highlight where there
   wasn't one). */
.comparison-col-pct-muted { color: var(--text-soft); }
/* 95px category column, fits every one of the 8 real labels on one line with a little
   spare room (the widest, "Shortest Coastline", measures ~91px at .comparison-
   category's current 8px font - see chat; re-measure and adjust this width again if
   that font size ever changes, it's sized specifically for it, not a fixed constant)
   - explicit direction that these must never wrap, unlike an earlier pass that let
   the longer ones break to 2 lines to save width for the country columns instead.
   align-items:end (explicit direction, was start) lines this column's own single-line
   text up with the BOTTOM of the country entries beside it, not the top - matters
   because the category text is smaller than the country/rank text, so top-aligning
   them left a visible gap under the shorter category text that bottom-aligning
   closes. Only visibly differs from top-aligning in the common (all single-line)
   case; the rare 2-line country-name exception still reads fine, just with the
   category label sitting a little lower relative to that entry's own first line. */
.comparison-row {
  display: grid;
  grid-template-columns: 95px 1fr 1fr;
  align-items: end;
  gap: 6px;
  margin-bottom: -2px; /* negative, not just 0 - explicit direction (2px, then -4px more) */
}
.comparison-row:last-child { margin-bottom: 0; }
/* Caps + grey, same weight/color as the live grid's own .bucket-label - just smaller.
   nowrap - the column above is sized so this never needs to, but explicit rather than
   relying on that alone, since forcing it onto one line is the actual requirement. */
.comparison-category {
  text-align: right;
  /* align-items:end on .comparison-row above aligns this element's own BOX to the
     country entries' box, but the smaller 8px font's actual glyph bottom still sits
     ~2px above the 10px country/rank text's own glyph bottom (different font-size ->
     different line-box metrics, not something align-items accounts for) - this nudge
     is a direct pixel correction for that visual gap, confirmed on a real device. */
  transform: translateY(-2px);
  font: 600 8px "Inter", sans-serif;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--text-dim);
}
/* Plain inline flow (not flex) - a flex row with the name set to grow would push the
   rank out to the column's own far edge regardless of how short the name actually is,
   reading as detached from its own country instead of sitting right next to it. */
.comparison-entry { min-width: 0; }
/* No ellipsis/nowrap/wrap here (unlike the shared .lb-name elsewhere on this site) -
   a rare very-long name gets its OWN font-size shrunk down to fit one line instead
   (revised 2026-09-11, was: let it wrap to a second line - see buildComparisonEntry's
   own shrinkToFit call in quiz.js), so this element's base font-size below is really
   just the ceiling that call starts from, not a fixed value. margin-right (not a flex
   gap, following from .comparison-entry no longer being a flex row above) is what
   keeps the rank close to the name rather than pushed away from it - and is exactly
   what shrinkToFit's own budget calculation subtracts back out, see quiz.js. */
.comparison-country {
  /* Required for shrinkToFit's own scrollWidth check in quiz.js to work at all - a
     plain inline element (a <span>'s own default) doesn't reliably report a real
     scrollWidth in every engine, same reason .ccard-name (fitName's other caller)
     already needs this same override. Confirmed as a real bug, not just theory - the
     long-name shrink silently never triggered without it (scrollWidth read as 0). */
  display: inline-block;
  margin-right: 4px;
  font: 700 10px "Inter", sans-serif;
  color: var(--text);
}
/* De-emphasized vs. the live grid's own accent-colored .bucket-rank - this screen
   shows 16 of them at once, same reasoning the old .pair-bucket.reveal had. */
.comparison-rank { font: 700 10px "Inter", sans-serif; color: var(--text-soft); }
/* #rank-percentile carries the shared .result-sub class (see index.html) and no
   longer needs its own override here (revised 2026-09-11, again): "Your score: X%"
   is back (shared #result-time-label/#result-title, same as every other game), so
   this line follows .result-title directly just like the other 3 games' own
   .result-sub does - the shared class's own -10px/0/16px margin (tuned for exactly
   that position) applies unmodified. */
/* Scoped to the adjacent-sibling case (only ever true on this page - no other game
   has a #rank-percentile element), overriding .leaderboard's own shared 18px
   margin-top just here rather than touching that shared value for every game.
   16px, matching #rank-percentile's own margin-bottom above, so the collapsed gap
   lands at exactly 16px (see chat, 2026-09-10) instead of the 18px the shared
   value would otherwise force via collapse (collapse takes the LARGER margin, and
   18 > 16, so leaving the shared rule alone couldn't reach 16 no matter what
   #rank-percentile's own margin said). */
#rank-percentile + .leaderboard { margin-top: 16px; }

@media (max-width: 480px) {
  .rank-pool { gap: 7px; margin-bottom: 18px; }
  .ccard { width: 66px; padding: 8px 5px 7px; }
  .pair-stack { gap: 8px; }
  .pair-module { gap: 6px; }
  .pair-bucket { padding: 8px; font-size: 11px; }
  .bucket-country-name { font-size: 11px; }
}
@media (max-width: 375px), (max-height: 700px) {
  .ccard { width: 60px; }
  .pair-bucket { padding: 7px; }
}

/* --- Order the Country (2026-09-17, renamed from "Arrange the Countries"
   2026-09-19) --------------------------------------------------------
   Real visual design, locked in after iterating on two published mockups
   (play-mode cards, then the reveal screen) - see the design memory for
   the full back-and-forth. Reuses shared tokens (--accent/--text-dim/
   --panel-edge/--text-soft) throughout, same as every other game's CSS
   here; the reveal screen's own TOP section (score/leaderboard/share/
   Keep Playing/hub-link) needs zero new rules at all - it's the exact
   same #result-panel/.leaderboard/etc. markup and CSS every other game on
   this site already shares, confirmed pixel-for-pixel against the live
   Rank the Country page during the mockup pass. Only the two genuinely
   new pieces - the ordering list itself, and the your-order/correct-order
   comparison grid below the divider - need rules of their own. */
/* Same text size as Guess the Club's own .gt-goal ("Name the CLUB these
   players once played for", football site) - including its "only the
   emphasized word is accent-colored, rest stays dim" treatment. */
.arrange-metric-label {
  font: 700 19px "Inter", sans-serif;
  color: var(--text-dim);
  text-align: center;
  margin: 4px 0 14px;
}
.arrange-metric-label em { color: var(--accent); font-style: normal; }
.arrange-endpoint-label {
  font: 400 12px "Inter", sans-serif;
  color: var(--text-dim);
  text-align: center;
  margin: 0;
}
.arrange-endpoint-label.top { margin-bottom: 4px; }
.arrange-endpoint-label.bottom { margin-top: 4px; margin-bottom: 18px; }

.arrange-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* stretch (not center) - lets the two arrow buttons stretch to exactly
   match .arrange-card's own height (see .arrange-arrow's aspect-ratio:1
   below), rather than sitting at a fixed smaller size. No rank number
   anymore (removed, 2026-09-17 - "the arrows are hard to aim at"): down
   on the LEFT, up on the RIGHT, both now big/square instead of a small
   stacked pair, directly addressing that. */
.arrange-row {
  display: flex;
  align-items: stretch;
  gap: 8px;
  position: relative; /* anchors the dragged row's own position:relative+z-index while dragging */
  /* Animates the OTHER rows' live "make way" shift while a drag is in
     progress - the actively-dragged row overrides this to transition:none
     inline (quiz.js's own onPointerDown) so it keeps following the
     pointer with zero lag; this transition only ever plays on the rows
     that aren't currently being held. */
  transition: transform 0.15s ease;
}
.arrange-card {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  background: #1e1e1e;
  border: 1.5px solid var(--panel-edge);
  border-radius: 14px;
  padding: 10px;
  cursor: grab;
  touch-action: none; /* this element handles its own vertical drag - don't let the page scroll fight it */
}
.arrange-row.dragging .arrange-card {
  cursor: grabbing;
  border-color: var(--accent);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45);
}
/* A flag on EACH side, natural aspect ratio, never cropped - the two
   flags are the SAME image (just mirrored placement), so they're always
   equal width and the centered name between them is automatically
   centered on the card too, not just on the leftover space. Height
   always matches .arrange-name's own line-height (22px), set inline by
   quiz.js per-flag since width depends on that flag's own real ratio. */
.arrange-flag {
  height: 22px;
  display: block;
  flex-shrink: 0;
}
.arrange-name {
  font: 700 18px "Inter", sans-serif; /* matches Who Has More?'s own .duel-card .name size */
  line-height: 22px;
  color: var(--text);
  text-align: center;
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
}
/* Same height as .arrange-card via align-items:stretch on .arrange-row
   (a flex item's stretched cross-size is a real definite height, so this
   needs no explicit height rule here). Width is explicit rather than
   aspect-ratio-derived - a stretched cross-size is "indefinite" for the
   aspect-ratio algorithm, so it silently fell back to a small content-only
   width instead of matching the intended size (found via independent
   review, 2026-09-17). Half-width narrowed further per user feedback
   (2026-09-17): 22.5px = half of the original 45px square width. */
.arrange-arrow {
  width: 22.5px;
  border: 1px solid var(--panel-edge);
  border-radius: 12px;
  background: #1e1e1e;
  color: var(--text);
  font-size: 16px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.arrange-arrow:disabled { opacity: 0.25; cursor: default; }
@media (hover: hover) {
  .arrange-arrow:not(:disabled):hover { border-color: var(--accent); }
}
.arrange-arrow:not(:disabled):active { border-color: var(--accent); }

/* --- reveal: your order vs. correct order, plus the value ------------- */
.reveal-grid {
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  column-gap: 8px;
  row-gap: 6px;
}
.reveal-head {
  text-align: left;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--panel-edge);
  margin-bottom: 4px;
}
.reveal-head-pct {
  margin: 0 0 2px;
  font: 400 15px "Inter", sans-serif;
  color: var(--accent);
}
.reveal-head-pct.muted { color: var(--text-soft); }
.reveal-head-title {
  margin: 0;
  font: 700 9px "Inter", sans-serif;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.reveal-head-value {
  margin: 0;
  font: 700 9px "Inter", sans-serif;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-dim);
  align-self: end;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--panel-edge);
  margin-bottom: 4px;
  text-align: right; /* matches .reveal-value's own right alignment below it */
}

.reveal-row {
  display: flex;
  align-items: baseline;
  gap: 4px;
  min-width: 0;
}
.reveal-rank { font: 700 10px "Inter", sans-serif; color: var(--text-dim); flex-shrink: 0; }
/* Matches Rank the Country's own .comparison-country - same reveal-list
   context (name + rank per row), reusing an existing size rather than
   inventing a new one. */
.reveal-name {
  font: 600 10px "Inter", sans-serif;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* font-size here (unused for the text itself - both child spans set their
   own) exists only to match the invisible line-height "strut" this block
   establishes to its actual 11px content. Without it the strut inherits a
   much larger ancestor font-size, and since .reveal-row (the sibling name
   column) is flex/baseline-aligned instead - no strut there at all - the
   two ended up ~5px out of vertical sync despite sharing a grid row
   (confirmed via direct measurement, 2026-09-17: name text flush with the
   row top, value text sitting 5px lower). The font-size fix above closed
   most of that gap but left a small remainder (still visibly a touch
   lower per user feedback) - this translateY is a direct pixel nudge for
   the rest (started at -2px, dialed back to -1px per follow-up feedback),
   same technique .comparison-category uses elsewhere in this file for an
   analogous font-size-driven metrics mismatch. */
.reveal-value {
  text-align: right;
  white-space: nowrap;
  font-size: 11px;
  transform: translateY(-1px);
}
/* Peak name + elevation sit on one line now (highestPointM only - every
   other metric's row has no .reveal-value-peak at all, just the plain
   formatted value) - each keeps its own font/color, just not stacked. */
.reveal-value-peak {
  font: 400 10px "Inter", sans-serif;
  color: var(--text-soft);
}
.reveal-value-elev {
  font: 700 11px "Inter", sans-serif;
  color: var(--text-dim);
  margin-left: 5px;
}

/* Short/narrow phones only (same tier .card itself drops to 10px/12px at,
   see that rule's own media query) - .card's shared left/right (12px) and
   bottom (10px) padding diverge slightly at this one tier; on THIS game
   specifically, match left/right down to the same 10px so the side
   gutters read the same width as the space below Submit (per user
   feedback, 2026-09-17). #game is NOT unique to this page (every geography
   game's main card uses the same id) - :has(.arrange-list) scopes this to
   only the page that actually has an arrange-list, instead of touching
   the shared .card rule every other game also uses at this breakpoint. */
@media (max-width: 375px), (max-height: 700px) {
  #game:has(.arrange-list) { padding-left: 10px; padding-right: 10px; }
}

/* --- Find the Country (6th geoquiz game, 2026-09-20) ----------------------
   Countryle-style: type a guess, get 4 clues back (continent/population/
   area/direction), keep guessing until you land the secret country. No
   timer, no fixed round count - the shared #result-panel/.leaderboard/etc.
   below the divider needs no new rules at all (same pattern the Order the
   Country CSS section above documents), only the guess input/autocomplete
   and the guess-row list below it are new. */
.fc-guess-form {
  display: flex;
  gap: 8px;
  align-items: stretch; /* Guess button matches the input's own height exactly,
    whatever that ends up being, instead of shrinking to its own text height
    (found as a real mismatch, 2026-09-20) - see .fc-guess-btn's own comment */
  /* Direct instruction, 2026-09-20: the guess input's own top gap (to the card's
     top edge) should equal its left gap (to the card's left edge). .card's base
     padding is 10px 24px 24px (top/left/bottom) - asymmetric by design elsewhere
     on the site - so this makes up the 14px difference itself; the two mobile
     breakpoints below re-cover the same gap at their own smaller .card padding. */
  margin-top: 14px; /* 24px (.card's left padding) - 10px (.card's top padding) */
}
/* Re-covers the same top=left gap at .card's own smaller breakpoint paddings (see
   that rule's own media queries) - placed here, AFTER the base .fc-guess-form rule,
   not alongside .card's own overrides up there: a media-scoped rule only beats an
   unconditional one later in the SAME cascade position if it also comes later in
   the file, regardless of specificity, so these have to live below the base rule
   to actually apply (real bug caught via direct measurement - the shared 480px
   block's own attempt at this silently lost to the unconditional rule below it). */
@media (max-width: 480px) {
  .fc-guess-form { margin-top: 2px; } /* 16px left padding - 14px top padding */
}
@media (max-width: 375px), (max-height: 700px) {
  .fc-guess-form { margin-top: 2px; } /* 12px left padding - 10px top padding */
}
.fc-input-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
}
/* font-size stays >=16px even on :focus - unlike .lb-name-input's own
   deliberate focus-grow (that input starts smaller since it sits inline in
   a leaderboard row), this one is already full-size, so no iOS zoom-on-
   focus risk to begin with (see .lb-name-input's own comment for the
   underlying WebKit behavior this avoids). */
.fc-guess-input {
  width: 100%;
  box-sizing: border-box;
  font: 700 16px "Inter", sans-serif;
  color: var(--text);
  background: #1e1e1e;
  border: 1.5px solid var(--panel-edge);
  border-radius: 10px;
  padding: 10px 12px;
}
.fc-guess-input:focus { outline: none; border-color: var(--accent); }
/* Only the placeholder shrinks (quiz.js's fitPlaceholder sets --fc-ph-size, and
   only when the text wouldn't fit) - the field itself stays 16px+ so iOS never
   zooms the page on focus. */
.fc-guess-input::placeholder { font-size: var(--fc-ph-size, 16px); }
.fc-autocomplete {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 5;
  max-height: 220px;
  overflow-y: auto;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: #1e1e1e;
  border: 1.5px solid var(--panel-edge);
  border-radius: 10px;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.45);
}
.fc-autocomplete-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px;
  border-radius: 8px;
  cursor: pointer;
  font: 600 13px "Inter", sans-serif;
  color: var(--text);
}
.fc-autocomplete-item.active { background: #2a2a2a; }
.fc-autocomplete-flag { height: 14px; display: block; flex-shrink: 0; }
/* No vertical padding (unlike .btn-primary's own 14px) - height now comes
   entirely from .fc-guess-form's own align-items:stretch, matching
   .fc-guess-input's real rendered height exactly rather than a separately
   guessed padding value that would drift out of sync if the input's own
   font-size/padding/border ever changed. display:flex centers the label
   inside whatever that stretched height turns out to be. */
.fc-guess-btn {
  width: auto;
  flex: 0 0 auto;
  padding: 0 18px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.fc-guess-btn:disabled { opacity: 0.4; cursor: default; }
/* "?" how-to-play button: same height as Guess (.fc-guess-form's
   align-items:stretch), but grey (.btn-secondary, not the mint highlight
   color) and narrower - direct instruction, 2026-09-21: it was the mint
   .btn-primary look at 44px wide, which drew more attention than Guess
   itself while Guess is dimmed. margin:0 cancels .btn-secondary's own
   margin-bottom, which would otherwise push it out of line in this row. */
.fc-info-btn {
  width: 30px; /* was 44px, then 36px - narrowed twice on request, 2026-09-21 */
  margin: 0;
  flex: 0 0 auto;
  padding: 0 0 0 2px; /* 2px left padding shifts the centered "?" 1px right - direct instruction, 2026-09-21 */
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
}
/* Unfolding explanation panel (first-visit auto-open, see quiz.js's setInfoOpen).
   Real .fc-clue chips as samples, so what's explained is literally what the
   player will see in the list. */
.fc-info {
  margin-top: 10px;
  padding: 12px;
  background: #1e1e1e;
  border: 1.5px solid var(--panel-edge);
  border-radius: 12px;
  font: 400 12px/1.4 "Inter", sans-serif;
  color: var(--text-soft);
}
.fc-info-intro { margin: 0 0 10px; }
.fc-info-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.fc-info-list li {
  display: grid;
  grid-template-columns: 16px 100px 1fr;
  column-gap: 8px;
  align-items: center;
}
.fc-info-icon { height: 16px; width: auto; display: block; justify-self: center; }
.fc-info-samples { display: flex; gap: 6px; }
.fc-info-samples .fc-clue { width: 46px; min-width: 0; padding: 0; flex: none; }
.fc-info-samples .fc-clue-continent { font-size: 9px; }
.fc-info-text b { color: var(--text); }
.fc-info-foot { margin: 10px 0; }
.fc-info-close { width: auto; padding: 8px 20px; display: block; margin: 0 auto; font-size: 13px; }
.fc-guess-hint {
  font: 700 12px "Inter", sans-serif;
  color: var(--bad);
  text-align: center;
  margin: 8px 0 0;
}

/* World map (2026-09-20 addition) - one shared SVG (findcountry/assets/
   world.svg, built by pipeline/build_world_map.py), every guessed country
   highlighted and kept lit as more guesses come in, camera pans/zooms to
   the latest one. preserveAspectRatio="xMidYMid slice" is set on the <svg>
   itself (index.html, not here) - now a no-op at the box's own square
   aspect ratio (matches the viewBox's own 2000x2000 square exactly, so
   there's no cropping/letterboxing to speak of at this outer level; left
   in place since it's harmless and this box's aspect ratio could change
   again), kept for the same "vector content, inlined not <img>-sourced,
   non-scaling-stroke" reasons as Guess the Country's own map reveal
   (maps/js/quiz.js) - a transform-scaled <img> SVG rasterizes and blurs. */
.fc-map {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1; /* was 3/2 - square, direct instruction 2026-09-20 ("current width is also the new height") */
  overflow: hidden;
  border-radius: 14px;
  border: 1.5px solid var(--panel-edge);
  /* NOT a negative top margin: an earlier attempt (-42px) to make the map's
     top gap match its left gap pulled the map up over the guess field that
     sits directly above it (real regression, reported 2026-09-20, caught
     by the user - I had only verified it numerically, not visually). The
     guess form occupies that space, so the map's gap to the card's top edge
     can't equal its left gap. */
  margin: 14px 0;
  /* Pure black = .fc-map-ocean's own fill. Was var(--bg) (#0f0f0f grey): before
     the first guess the world sits shifted down (DEFAULT_MAP_CENTER_Y), leaving
     an empty strip at the top that isn't covered by the ocean rect (that rect
     moves WITH the world), so the card's own background showed through as a
     grey band. Direct report, 2026-09-21. */
  background: #000000;
}
/* Summary screen only (correct country guessed) - 75% of the square's height:
   4/3 = width : 0.75 x width. Added/removed by quiz.js (showReveal/beginRound). */
.fc-map.fc-map-summary { aspect-ratio: 4 / 3; }
/* On the summary screen the map is the card's first visible child (the guess
   form is hidden), so its top margin stacks on .card's own top padding and
   made the top gap larger than the left gap on phones (29px vs 17px; direct
   report 2026-09-20). Top gap should equal left gap: .card padding-left minus
   padding-top = 2px at both mobile tiers (16-14, 12-10); desktop (24-10=14)
   already matches the base margin. Media blocks sit AFTER the rule above so
   they win on source order too - a media query alone doesn't beat a later
   unconditional rule (that bit an earlier fix in this file). */
@media (max-width: 480px) {
  .fc-map.fc-map-summary { margin-top: 2px; }
}
@media (max-width: 375px), (max-height: 700px) {
  .fc-map.fc-map-summary { margin-top: 2px; }
}
.fc-map-svg { position: absolute; inset: 0; }
.fc-map-svg svg { display: block; width: 100%; height: 100%; }
.fc-map-ocean { fill: #000000; } /* was var(--bg) (#0f0f0f) - pure black, direct instruction 2026-09-20 */
.fc-map-antarctica { fill: #575757; stroke: #4a4a4a; stroke-width: 0.6; } /* fill was #696969 - same landmass/border colors as every other country, direct instruction 2026-09-20 ("include antarctica, all landmasses") */
/* Floating card showing the LATEST guess's own 4 clue values, overlaid on
   the map itself (2026-09-20 addition) - lets a player read the result
   without looking away from the country the camera just zoomed to. Only
   ever shows one guess at a time (unlike the guess-row list below, which
   keeps every guess) - updateMapOverlay() in quiz.js replaces its content
   wholesale on every new guess, it never accumulates. Semi-transparent so
   the map is still visible through it, per direct instruction. */
/* Flush against the map's own top/left/right edges (direct instruction,
   2026-09-20) - .fc-map itself clips to its own rounded corners via
   overflow:hidden, so this still reads as a rounded card despite having
   square top corners of its own. Background fades from top to bottom
   (was a flat 70% + a border-bottom hairline - direct instruction,
   2026-09-20: no more hard edge, the map should show through more and more
   toward the bottom of the card) - see the gradient's own comment below
   for its exact stops. */
.fc-map-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 120px; /* was 80px - grown so the gradient's own 80px->120px (40%->0%) stretch is
    actually visible instead of being clipped off below the box, direct instruction 2026-09-20:
    "from these 40% there should also be a transition to the 0% below it". The metric row's own
    content (icons/labels/values) is untouched - it still only occupies the top ~80px via its own
    padding/flow, same as before; the extra 40px is empty space carrying only the fading
    background, a soft trailing fade below the metrics rather than a second content row. */
  z-index: 3;
  display: flex;
  justify-content: space-around;
  gap: 4px;
  /* Three-stop gradient (direct instruction, 2026-09-20): 95% opacity at
     the top, 50% at 10px below the continent value pill's own bottom edge
     (measured live: that pill's background bottom sits at 70px from the
     card's own top, so this stop is at 80px), then down to 0% at 120px. */
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.95) 0px, rgba(0, 0, 0, 0.5) 80px, rgba(0, 0, 0, 0) 120px); /* middle stop was 0.4 */
  padding: 8px 10px 6px; /* left/right was 4px - widened so Continent's/Direction's own centers sit
    further from the map's edges on every device (direct instruction, 2026-09-20) - this is a
    proportional layout (4 equal flex:1 columns), so a plain px change here scales with it at
    every width, unlike a one-off px offset on the items themselves. */
}
.fc-map-overlay-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  flex: 1;
  min-width: 0;
}
/* All four icons keep the SAME 14px layout box regardless of visual size -
   the per-category scale() below only resizes the rendered pixels, never
   the space reserved for it in this flex column, so the label/value below
   every icon starts at the identical y-offset no matter which icon kind it
   follows (direct instruction, 2026-09-20: "the other text of the other 3
   metrics are higher [than continent's], move them down, same y position
   as CONTINENT" - root cause was the OLD approach here, which changed
   `height` per-category and therefore changed the layout box itself).
   transform-origin: bottom keeps each icon's bottom edge fixed in place
   while scaling, same visual effect the old height+translateY combo
   achieved, without the layout-height side effect. */
/* Icon/label +4px, value +8px (direct instruction, 2026-09-20, to fill the
   card's new fixed 80px height) - all via translateY, a purely visual shift
   that doesn't change any element's own layout box, so it can't disturb the
   alignment work above/below (icons still share one layout height, labels
   still share one font/line-height) - it just moves the rendered pixels. */
.fc-map-overlay-icon { height: 14px; width: auto; display: block; transform-origin: bottom center; transform: translateY(4px); } /* base/default - direction keeps this exact size */
.fc-map-overlay-icon-population,
.fc-map-overlay-icon-area {
  transform: translateY(3px) scale(0.9); /* was translateY(-1px) - +4px card-height nudge, net -1+4=3 - direct instruction; translate listed first so it isn't itself scaled down */
}
.fc-map-overlay-icon-continent {
  transform: translateY(4px) scale(1.1); /* 110% size, +4px card-height nudge, direct instruction */
}
.fc-map-overlay-label {
  font: 700 10px "Inter", sans-serif; /* was 9px, direct instruction 2026-09-20 - top position unaffected
    (label sits after the icon's own fixed-height box in this flex column, see
    .fc-map-overlay-icon's own comment), the extra line-height only pushes the
    label's OWN bottom + everything below it (the value, and the card's own
    auto height) down - all 4 labels share this exact font/line-height and an
    identical icon box height above them, so their bottoms stay aligned. */
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-soft);
  white-space: nowrap;
  transform: translateY(4px); /* direct instruction 2026-09-20 */
}
/* Reuses .fc-clue as-is (the exact element renderClueCell() already builds
   for the guess-row list) for the value itself - height/padding/font-size/
   colors now all live in the shared base rules (see the guess-row CSS
   section below), since the guess-row list's own values were made to match
   this card exactly (direct instruction, 2026-09-20). Only this card's own
   extra vertical-position quirk stays here. */
.fc-map-overlay-item .fc-clue {
  margin-top: 1px;
  transform: translateY(8px); /* direct instruction 2026-09-20 */
}
/* Continent is the one value whose sizing still differs from the shared base
   rule: this card's own continent column is proportional to viewport width
   (unlike the guess-row list's fixed 70px column), so it keeps its own
   responsive clamp() instead of the shared rule's flat font-size. white-
   space: nowrap so it never wraps to 2 lines (real bug, direct instruction
   2026-09-20: "that should never happen") - the clamp's own minimum (6.5px)
   was picked empirically (Playwright, see chat) as the smallest size that
   still fits "N.-America" in this column's own available width at the
   narrowest realistic viewport (~320px). */
.fc-map-overlay-item .fc-clue-continent {
  font-size: clamp(6.5px, 2.5vw, 11px); /* was a flat 11px */
}
#fc-map-countries path {
  fill: #575757; /* was #696969, direct instruction 2026-09-20 */
  stroke: #4a4a4a; /* was #919191, direct instruction 2026-09-20 */
  stroke-width: 0.8;
  transition: fill 0.25s ease, stroke 0.25s ease;
}
/* .fc-map-current is always paired with either .fc-map-guessed or
   .fc-map-correct (see quiz.js's focusMapOnCountry) - never applies alone -
   so it's listed FIRST here and the other two after it, letting equal-
   specificity source order make right/wrong color always win over it
   (direct instruction, 2026-09-20: wrong guesses must show red even as the
   just-guessed/"current" one, not the old bright-green "current" tint). */
#fc-map-countries path.fc-map-current { fill: var(--accent); stroke: var(--accent); }
/* Wrong guesses that are no longer the most recent one fade to this darker
   red (direct instruction, 2026-09-20) - the more specific 2-class rule
   below (.fc-map-guessed.fc-map-current, always the LATEST wrong guess)
   keeps the brighter red regardless of source order, since it's more
   specific than either single-class rule around it. */
#fc-map-countries path.fc-map-guessed { fill: #69303a; stroke: #69303a; }
#fc-map-countries path.fc-map-guessed.fc-map-current { fill: #ff496e; stroke: #ff496e; }
#fc-map-countries path.fc-map-correct { fill: var(--good); stroke: var(--good); }
/* Camera pan/zoom itself - see quiz.js's own worldZoomTransform() - animates
   via a plain CSS transition on this one <g>, not the WAAPI keyframe-curve
   machinery Flags/Maps need for their own continuous 20s reveal; a guess-
   triggered camera move is just a single eased jump between two fixed
   states, no per-frame curve to drive. */
#fc-map-world {
  transition: transform 0.7s cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* Small-country marker - added/removed dynamically per guess when a
   country's own rendered size at the fixed zoom falls below the legible
   threshold (see quiz.js's SMALL_COUNTRY_VB_THRESHOLD). Sized in raw
   viewBox units, same coordinate space as the map paths themselves, so it
   scales correctly under the shared <g>'s own zoom transform instead of
   needing separate screen-space math. */
.fc-map-marker {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5; /* was 2, direct instruction 2026-09-20 */
  vector-effect: non-scaling-stroke;
}
.fc-map-marker-wrong { stroke: #ff496e; } /* direct instruction 2026-09-20, matches the wrong-guess country fill */

/* Legend + every guess row share the same 5-column template (name column
   flexible, 4 equal-width clue columns) so the clue cells line up exactly
   underneath their own header, guess after guess. */
.fc-clue-legend,
.fc-guess-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) repeat(4, 36px); /* was 70px - about half, direct instruction 2026-09-20 */
  column-gap: 6px;
  align-items: center;
}
/* Icon only per column now (direct instruction, 2026-09-20 - the labels
   that used to sit under each icon here are gone). align-items:start (not
   the shared rule's own center) so the icon sits flush with the top of its
   row, same as it did back when a label sat below it - kept as-is rather
   than switching to center, since nothing asked for that to change. */
.fc-clue-legend {
  margin: 14px 0 2px; /* bottom was 8px - list sat too far from the icons above it, direct instruction 2026-09-20 */
  align-items: start;
  /* Same padding + border-width as .fc-guess-row below it, just invisible
     (transparent border, no background) - found as a real misalignment,
     2026-09-20: with these at 0 here but 8px/1.5px on the guess rows, the
     two grids' own CONTENT-BOX widths differed, so the flexible first
     (spacer) column resolved to a different absolute width in each,
     shifting the 4 fixed clue columns out of alignment with the legend
     above them despite sharing the identical grid-template-columns value -
     confirmed by direct measurement (9px consistent offset on every
     column). Matching the box model exactly is the robust fix, not a
     hand-tuned offset that could drift again if either rule ever changes. */
  padding: 8px;
  border: 1.5px solid transparent;
  box-sizing: border-box;
}
.fc-clue-legend-item {
  display: flex;
  justify-content: center;
}
/* Icon only, no label underneath any more (direct instruction, 2026-09-20) -
   sized to EXACTLY match .fc-map-overlay-icon's own sizing (14px base height,
   same 90%/110% per-category scale for population/size/continent) so this
   list's icons read as the same icons at the same size as the map card's own
   4-metric card above it. transform-origin: bottom center for the same
   reason .fc-map-overlay-icon has it - keeps each icon's bottom edge fixed
   while scaling. Deliberately WITHOUT that card's own extra +4px translateY -
   that was a positioning nudge specific to filling ITS OWN taller box, not
   part of the icon's actual size. */
.fc-clue-legend-icon { height: 14px; width: auto; display: block; transform-origin: bottom center; }
.fc-clue-legend-icon-population,
.fc-clue-legend-icon-area {
  transform: translateY(-1px) scale(0.9);
}
.fc-clue-legend-icon-continent {
  transform: scale(1.1);
}

.fc-guesses {
  display: flex;
  flex-direction: column-reverse; /* newest guess appended last in the DOM, but shown on top */
  gap: 6px;
}
.fc-guess-row {
  background: #1e1e1e;
  border: 1.5px solid var(--panel-edge);
  border-radius: 12px;
  padding: 6px 8px; /* top/bottom was 8px - shorter rows, direct instruction 2026-09-20 */
}
.fc-guess-row.correct { border-color: var(--good); }
.fc-guess-name-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.fc-guess-flag { height: 15px; display: block; flex-shrink: 0; } /* was 18px, direct instruction 2026-09-20 */
.fc-guess-name {
  font: 700 11.5px "Inter", sans-serif; /* was 13px, direct instruction 2026-09-20 */
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
/* Same size as the map overlay card's own values (direct instruction,
   2026-09-20: "for the values, use the same size (and also color etc.) as
   in the map metric card") - was height:34px/font-size:12px. The overlay's
   own .fc-clue rule (below, in the map section) now only adds its own
   extra vertical-position quirks on top of this. */
.fc-clue {
  height: 22px;
  min-width: 30px;
  padding: 0 4px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 10px "Inter", sans-serif;
  background: #262626;
  color: var(--text-soft);
}
/* Wrong/right continent - same red/green as the map overlay's own continent
   value (direct instruction, 2026-09-20). white-space:nowrap so it never
   wraps to 2 lines (same real bug as the overlay's own, see that rule's
   comment) - this column is a fixed 36px (not proportional to viewport
   like the overlay's own), so a single flat font-size that's confirmed via
   direct measurement to fit "N.-America"/"S.-America" here is enough, no
   clamp() needed. Tighter padding than the map card's own continent value
   (2px not 8px) since this column is now much narrower - direct instruction
   2026-09-20 ("reduce this horizontal space to about half"). */
.fc-clue-continent { font-size: 7px; white-space: nowrap; color: #ff496e; padding: 0 2px; }
.fc-clue-continent.match { color: var(--good); }
.fc-clue-arrow { font-size: 12px; }
.fc-clue-direction { font-size: 15px; } /* color falls through to .fc-clue's own text-soft - same as the overlay's direction value resolves to */
.fc-clue-moderate { color: #ffe900; } /* was #e7d066, direct instruction 2026-09-20 */
.fc-clue-severe { color: #ff496e; }
.fc-clue.correct { background: rgba(110, 231, 183, 0.16); color: var(--good); font-size: 12px; }

/* --- reveal: read-only recap of every guess made, reusing .fc-guess-row/
   .fc-clue as-is (same markup quiz.js builds during live play) - buildReveal()
   now also clones the legend (icon row) above this, direct instruction
   2026-09-20. */
.fc-reveal-guesses {
  display: flex;
  flex-direction: column-reverse; /* was column - most recent guess (the
    correct one) on top, matching live play's own #fc-guesses; direct
    instruction 2026-09-20 */
  gap: 6px;
  margin-top: 4px;
}

/* ---------------------------------------------------------------------
   Your Name As A Flag (quizebra.com/name, 2026-09-22) - standalone tool,
   not a daily game: no start/play split, no leaderboard, purely client-
   side (see name/js/flagmaker.js). Deliberately its own page, never
   linked from either overview (quizebra.com or quizebra.com/geography) -
   see the plan in chat. Reuses .kicker/.start-rules/.fc-guess-form/
   .fc-input-wrap/.fc-guess-input/.fc-guess-btn/.btn-primary/.round-error/
   .color-flag-wrap/.color-flag-caption as-is (all generic, none of them
   gated on the #start-panel id that would otherwise force the narrow
   258px intro-card width - see the comment on that rule further up). */
.fm-hero { text-align: center; padding-top: 10px; }
/* Direct instruction, 2026-09-23: title moved closer to the kicker above
   it - .kicker's own base margin-bottom (6px) trimmed down just for this
   page, plus .fm-title's own top margin removed entirely (was 4px). */
.fm-hero .kicker { margin-bottom: 1px; }
/* Its own class rather than reusing .start-title directly - same font/
   transform, but .start-title's own letter-spacing is individually
   fitted per quiz to a ~191px NARROW column width (see .start-title's
   own comment); this page has the full ~430px card width to work with,
   so the default 0.02em tracking already sits comfortably inside it. */
.fm-title {
  font: 400 26px "Anton", sans-serif;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin: 0 0 2px;
  color: var(--text-soft);
}
.fm-form { margin-top: 18px; }
.fm-flag-wrap { margin-top: 14px; }
.fm-flag-wrap canvas { display: block; max-width: 100%; max-height: 100%; }
.fm-caption { margin-top: 10px; }
.fm-actions { margin-top: 14px; }
.fm-actions a.btn-primary { text-decoration: none; } /* anchor, not a <button> - .btn-primary itself never needs this reset elsewhere */
