html, body, button, input, textarea, select {
    font-family: 'Nunito', 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
    color: var(--mud-palette-text-primary, #2C2C2E);
    font-feature-settings: "ss01", "cv11";
}

html, body {
    font-weight: 500;
    letter-spacing: -0.005em;
}

/* Set by the first script in App.razor's <head> and removed two frames after
   DOMContentLoaded. Without it, component <style> blocks that parse after their markup fire
   every transition on page load (the closed mobile menu and drawers slide away). Keyframe
   animations are not affected. */
html.preload *,
html.preload *::before,
html.preload *::after {
    transition: none !important;
}

h1, h2, h3, h4, h5, h6 {
    font-family: 'Nunito', 'Inter', system-ui, sans-serif;
    font-weight: 800;
    letter-spacing: -0.02em;
}

/* ──────────────────────────────────────────────────────────────
   Duolingo-style 3D pressable buttons / tiles
   --duo-depth: the bottom "shelf" thickness
   --duo-fg / --duo-shadow: surface + the darker shelf tone
   ────────────────────────────────────────────────────────────── */
:root {
    --duo-depth: 4px;
    --duo-radius: 16px;

    /* Shared quiz-map palette - the single source of truth for every map implementation
       (SVG world map, MapLibre "Top 10" + "Place It", and anything added later).
       These MUST be concrete colours, not var()/color-mix indirection: the geojson maps
       resolve them to RGB by probing a throwaway element (see map-quiz.js), which can't
       follow theme-var references - so a color-mix() ocean came out black.
       Light + dark are SEPARATE variables (not a .mud-theme-dark override) because the
       maps read the palette at init via getComputedStyle, before the theme class is on the
       DOM - they pick the right one from localStorage (isDarkTheme()) instead. Values mirror
       the SVG quiz stage: ocean = SVG light stage (--mud-palette-background-gray, #f5f5f5) /
       SVG dark stage (#1a1a2e); land = the SVG world map's baked fill (#ececec, kept light
       in both themes so countries read against the dark ocean). */
    /* Quiz map palette - kept identical to the SVG world map / arena so every map mode
       (Top 10 .mq-map, Place It .pq-map, SVG quizzes, arena) looks the same: countries
       #e3e5ec with a #c4c8d2 hairline border on the #f3f4f7 background. Maps read these at
       init via getComputedStyle. Dark mode keeps its own ocean value (countries stay light,
       mirroring the SVG world map's baked fill, so they read against the dark water). */
    --quiz-map-ocean: #f3f4f7;
    --quiz-map-ocean-dark: #1a1a2e;
    --quiz-map-land: #e3e5ec;
    --quiz-map-border: #c4c8d2;
    /* Boards where the internal borders ARE the game (GameUsa state lines) use this
       clearly darker stroke instead of the hairline. Land stays light in both themes,
       so one value works for light and dark. */
    --quiz-map-border-strong: #8b93a5;
}

/* ──────────────────────────────────────────────────────────────
   Theme palette - self-defined (replaces MudThemeProvider's generated
   --mud-palette-* variables). Light is the default on :root; dark is
   activated by the `mud-theme-dark` class on <html> (toggled from
   localStorage by the inline script in App.razor and the theme switch).
   ────────────────────────────────────────────────────────────── */
:root {
    /* The app-wide page background (body, app bar, page backdrops). Change it HERE and
       every non-quiz page + the app bar follow. Quiz gameplay surfaces (SVG stage, map
       ocean, arena panels) intentionally do NOT use this - they keep their own colors. */
    --app-page-bg: #ffffff;

    --mud-palette-primary: #58CC02;
    --mud-palette-primary-darken: #46A302;
    --mud-palette-primary-hover: rgba(88, 204, 2, 0.10);
    --mud-palette-primary-rgb: 88, 204, 2;
    --mud-palette-secondary: #1CB0F6;
    --mud-palette-tertiary: #FF9600;
    --mud-palette-info: #1CB0F6;
    --mud-palette-success: #58CC02;
    --mud-palette-warning: #FF9600;
    --mud-palette-error: #FF4B4B;
    --mud-palette-error-lighten: #FF7575;
    --mud-palette-error-rgb: 255, 75, 75;
    --mud-palette-background: #f3f4f7;
    --mud-palette-background-gray: #F0F0F0;
    --mud-palette-surface: #FFFFFF;
    --mud-palette-text-primary: #3C3C3C;
    --mud-palette-text-secondary: #777777;
    --mud-palette-text-disabled: rgba(0, 0, 0, 0.38);
    --mud-palette-divider: #E5E5E5;
    --mud-palette-lines-default: #E5E5E5;
    --mud-palette-action-default-hover: rgba(0, 0, 0, 0.04);
    --mud-palette-dark: #2B2B35;
}

/* Dark default = "Midnight" (navy slate), chosen 2026-08-30. */
.mud-theme-dark {
    --app-page-bg: #0F172A;

    --mud-palette-primary: #58CC02;
    --mud-palette-primary-darken: #46A302;
    --mud-palette-primary-hover: rgba(88, 204, 2, 0.15);
    --mud-palette-primary-rgb: 88, 204, 2;
    --mud-palette-secondary: #1CB0F6;
    --mud-palette-tertiary: #FF9600;
    --mud-palette-info: #1CB0F6;
    --mud-palette-success: #58CC02;
    --mud-palette-warning: #FFC800;
    --mud-palette-error: #FF4B4B;
    --mud-palette-error-lighten: #FF7575;
    --mud-palette-error-rgb: 255, 75, 75;
    --mud-palette-background: #0F172A;
    --mud-palette-background-gray: #162032;
    --mud-palette-surface: #1E293B;
    --mud-palette-text-primary: #F1F5F9;
    --mud-palette-text-secondary: #97A3B6;
    --mud-palette-text-disabled: rgba(255, 255, 255, 0.30);
    --mud-palette-divider: #334155;
    --mud-palette-lines-default: #334155;
    --mud-palette-action-default-hover: rgba(255, 255, 255, 0.06);
    --mud-palette-dark: #2B2B35;
}

/* "Classic" - the pre-2026-08-30 dark palette (Duolingo-style teal), kept so we
   can switch back. Activate with window.triviTheme.setPalette('classic') in the
   browser console (persists in localStorage "darkPalette"; '' returns to Midnight).
   The toolbar tints in App.razor's triviTheme.darkTints must match the
   backgrounds here. Accents are shared with the base block above. */
.mud-theme-dark[data-dark-palette="classic"] {
    --app-page-bg: #131F24;
    --mud-palette-background: #131F24;
    --mud-palette-background-gray: #1A282F;
    --mud-palette-surface: #202F36;
    --mud-palette-text-primary: #F7F7F7;
    --mud-palette-text-secondary: #AFAFAF;
    --mud-palette-divider: #37464F;
    --mud-palette-lines-default: #37464F;
}

/* Base reset (previously provided by MudBlazor's stylesheet). */
*, *::before, *::after { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--app-page-bg);
    color: var(--mud-palette-text-primary);
}

button { font-family: inherit; }

/* Nothing on the page gets the browser's tap highlight. Individual controls used to opt out one
   by one (.duo-btn, .qil-card), so every plain <button> still flashed grey on Android. */
* { -webkit-tap-highlight-color: transparent; }

/* Two fast taps on one control must not zoom the page. iOS Safari reads a quick second tap as
   double-tap-to-zoom, so a player who tapped Delete twice in Daily Letters & Numbers got a
   zoomed page (reported 2026-11-08). The viewport stays zoomable on purpose, for accessibility,
   so the fix is per control: manipulation keeps panning and pinch zoom, and drops only the
   double-tap gesture. It also drops iOS's ~300ms wait for that second tap, so every button
   answers at once.

   Every control gets it, the way the tap-highlight rule above does, because the components
   opted in one by one and a new button was always born with the bug.

   :where() holds the specificity at zero, so any rule that sets touch-action itself still
   wins: the drag, pan and pinch surfaces (.rl-hero, .pq-token, .hrm, .bg-arena, .mz-key,
   .atree-stage, .svg-stage, .pmc) keep their own value. Those surfaces are safe anyway,
   because touch-action values intersect down the ancestor chain: a `none` parent still blocks
   the gesture on the button inside it.

   Text inputs are left out. A double tap in a text field selects a word, which is editing, not
   zooming. */
:where(button, a, label, summary, select, [role="button"],
       input[type="checkbox"], input[type="radio"], input[type="submit"], input[type="button"]) {
    touch-action: manipulation;
}

/* Touch devices: the app is not a document. A tap or a long press must not select text or
   show the copy / share callout. Only the typed inputs and elements marked .selectable keep
   selection. Desktop is untouched, so mouse users can still copy text. */
@media (hover: none) and (pointer: coarse) {
    body {
        -webkit-user-select: none;
        user-select: none;
        -webkit-touch-callout: none;
    }

    input, textarea, select, [contenteditable="true"] {
        -webkit-user-select: text;
        user-select: text;
        -webkit-touch-callout: default;
    }
}

/* Blazor's unhandled-error banner - hidden by default; the framework reveals it by
   setting an inline `display: block` when a real error occurs. (Previously hidden by
   MudBlazor's stylesheet + a scoped rule that wasn't being loaded; defined globally here
   so it works on every page/layout.) */
#blazor-error-ui {
    display: none;
    position: fixed;
    /* Above Mediavine's bottom ad bar (mvAdhesionInset.js), so "Reload" stays tappable. */
    bottom: var(--ad-bottom, 0px);
    left: 0;
    width: 100%;
    z-index: 1000;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    background: lightyellow;
    /* A fixed dark text colour: the inherited page colour is light in the dark theme, and
       light text on lightyellow cannot be read. */
    color: #3a3000;
    font-weight: 600;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
}
#blazor-error-ui .reload {
    color: #0b57d0;
    font-weight: 800;
    text-decoration: underline;
}
#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

/* MainLayout: the main area is a flex column, so the footer (margin-top: auto) sits at the
   bottom of the viewport on short pages. These rules are here and not in MainLayout's own
   <style> block: that block comes after the page body, so it parses too late for the first
   paint. */
.site-main {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    background: var(--app-page-bg);
}

.site-body {
    flex: 1 0 auto;
}

.duo-btn {
    --duo-fg: #58cc02;
    --duo-shadow: #58a700;
    --duo-text: #ffffff;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 14px 22px;
    border: none;
    border-radius: var(--duo-radius);
    background: var(--duo-fg);
    color: var(--duo-text);
    font-family: 'Nunito', 'Inter', sans-serif;
    font-weight: 800;
    font-size: 15px;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    text-decoration: none;
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    box-shadow: 0 var(--duo-depth) 0 0 var(--duo-shadow);
    transition: transform 0.05s ease, box-shadow 0.05s ease, filter 0.15s ease;
    line-height: 1.1;
    white-space: nowrap;
}

.duo-btn:hover {
    filter: brightness(1.04);
}

.duo-btn:active {
    transform: translateY(var(--duo-depth));
    box-shadow: 0 0 0 0 var(--duo-shadow);
}

.duo-btn:focus-visible {
    outline: 3px solid rgba(88, 204, 2, 0.4);
    outline-offset: 3px;
}

.duo-btn[disabled] {
    cursor: not-allowed;
    opacity: 0.55;
    filter: grayscale(0.2);
}

/* Neutral / outlined variant */
.duo-btn.duo-btn-ghost {
    --duo-fg: #ffffff;
    --duo-shadow: #e5e5e5;
    --duo-text: #4b4b4b;
    border: 2px solid #e5e5e5;
}

/* Brand variants */
.duo-btn.duo-btn-blue {
    --duo-fg: #1cb0f6;
    --duo-shadow: #1899d6;
}

.duo-btn.duo-btn-purple {
    --duo-fg: #ce82ff;
    --duo-shadow: #a560cc;
}

.duo-btn.duo-btn-pink {
    --duo-fg: #ff4b8b;
    --duo-shadow: #d63a72;
}

.duo-btn.duo-btn-orange {
    --duo-fg: #ff9600;
    --duo-shadow: #e08600;
}

.duo-btn.duo-btn-red {
    --duo-fg: #ff4b4b;
    --duo-shadow: #d63a3a;
}

.duo-btn.duo-btn-yellow {
    --duo-fg: #ffc800;
    --duo-shadow: #e0af00;
    --duo-text: #4b3a00;
}

/* Dark theme: ghost variant needs darker neutral so it's still readable */
.mud-theme-dark .duo-btn.duo-btn-ghost {
    --duo-fg: #2b2b35;
    --duo-shadow: #1a1a22;
    --duo-text: #f3f4f6;
    border-color: #3a3a47;
}

/* Full-width button (e.g. form submit). */
.duo-btn-block {
    display: flex;
    width: 100%;
}

/* Compact size for in-quiz chrome (the Skip / View buttons in a floating mode panel).
   Same shelf, same uppercase voice - just less padding and a shallower shelf. */
.duo-btn.duo-btn-sm {
    --duo-depth: 3px;
    gap: 6px;
    padding: 8px 14px;
    border-radius: 12px;
    font-size: 12px;
}

/* ──────────────────────────────────────────────────────────────
   Duolingo-style surface card.
   A rounded panel with a subtle 3D "shelf" underneath, matching the
   quiz menu tiles/cards. Use for sections, panels and list groups.
   --duo-card-accent (optional) tints the shelf + hover border.
   ────────────────────────────────────────────────────────────── */
.duo-card {
    --duo-card-accent: var(--mud-palette-lines-default);
    border-radius: 20px;
    background: var(--mud-palette-surface);
    border: 2px solid var(--mud-palette-lines-default);
    box-shadow: 0 4px 0 0 var(--mud-palette-lines-default);
}

/* ──────────────────────────────────────────────────────────────
   "NEW" badge for freshly added content (quizzes, categories, modes).
   Rendered when NewContent.IsNew(AddedOn) - shows for NewContent.WindowDays days from the
   definition's AddedOn date. Base class is inline (sits next to a
   title); add .duo-new-badge-abs to pin it to a card's top-right corner
   (the parent needs position: relative / absolute / fixed).
   ────────────────────────────────────────────────────────────── */
.duo-new-badge {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    /* Asymmetric on purpose: Nunito caps sit ~0.5px above optical centre. */
    padding: 2.5px 7px 1.5px;
    border-radius: 999px;
    background: #FF4B4B;
    box-shadow: 0 2px 0 0 #D63A3A;
    color: #fff;
    font-family: 'Nunito', sans-serif;
    font-size: 9.5px;
    font-weight: 900;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    line-height: 1.5;
    white-space: nowrap;
    pointer-events: none;
}

.duo-new-badge-abs {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 2;
}

a {
    color: #1cb0f6;
}

h1:focus {
    outline: none;
}

/* ── Difficulty label (see <DifficultyLabel>) ─────────────────────────────────
   One-word difficulty chip for the 1–5 scale (Easy … Expert). The component
   sets --dl-color per level. Lives here (not a <style> block) because the chip
   renders many times per dialog. */
.diff-label {
    display: inline-flex;
    align-items: center;
    line-height: 1;
    font-family: 'Nunito', sans-serif;
    font-size: 10.5px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    /* Symmetric top/bottom: at line-height 1 the Nunito ExtraBold caps sit
       ~centred in the line box, so even padding centres the word. (An earlier
       asymmetric 4/2 was meant to nudge it but pushed the word visibly low.) */
    padding: 3px 9px;
    border-radius: 999px;
    white-space: nowrap;
    color: var(--dl-color);
    background: color-mix(in srgb, var(--dl-color) 13%, transparent);
    border: 1px solid color-mix(in srgb, var(--dl-color) 40%, transparent);
}

.diff-label-sm {
    font-size: 9.5px;
    padding: 2px 7px;
}


/* ── Custom mask icons ────────────────────────────────────────────────────────
   Icons Font Awesome free doesn't cover well. Each behaves like a FA <i>:
   sized by font-size (1em box), tinted by currentColor - so they can be used
   anywhere an icon class string is rendered (MainQuizCatalog.Icon, dialogs). */
.icon-football {
    display: inline-block;
    width: 1em;
    height: 1em;
    vertical-align: -0.125em; /* match Font Awesome's baseline offset */
    background-color: currentColor;
    -webkit-mask: url('/SVGS/football-icon.svg') no-repeat center / contain;
    mask: url('/SVGS/football-icon.svg') no-repeat center / contain;
}

/* ── Daily quiz tiles ──────────────────────────────────────────────────────
   Shared chrome for the per-guess feedback tiles in Daily Country and Daily Animal.
   Both boards render the same visual language - a bordered tile with an uppercase label
   over a bold value, popping in on a stagger, tinted green on a match and yellow on a
   partial. It lives here rather than being copy-pasted per board so a fix to one is a fix
   to both; each quiz still owns its own tile CONTENT and any hint colours unique to it
   (Country's higher/lower arrows, Animal's five-tier taxonomy ladder). */

/* Guess pips - the chunky progress segments at the top of a daily board, one per guess
   slot, filled (accent) as guesses are spent. Shared by Daily Country and Daily Animal;
   the accent comes from the quiz root's --accent. */
.dq-pips {
    display: flex;
    gap: 5px;
    justify-content: center;
    padding: 0 0 2px;
}
.dq-pips span {
    width: 30px;
    height: 7px;
    border-radius: 999px;
    background: var(--mud-palette-lines-default);
}
.dq-pips span.on { background: var(--accent); }

/* Column count is a modifier, not a constant: Daily Country and Daily Animal both run
   four columns (two on narrow screens). Everything else about a tile is identical. */
.dq-tiles {
    display: grid;
    gap: 6px;
    grid-template-columns: repeat(2, 1fr);
}

.dq-tiles-4 { grid-template-columns: repeat(4, 1fr); }

@media (max-width: 640px) {
    .dq-tiles-4 { grid-template-columns: repeat(2, 1fr); }
}

/* A tile spanning the full row - used for chip rows and the strongest single hint. */
.dq-tile-wide { grid-column: 1 / -1; }

.dq-tile {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 8px 10px;
    border-radius: 11px;
    border: 1.5px solid var(--mud-palette-lines-default);
    background: var(--mud-palette-background);
    min-width: 0;
    animation: dqTilePop 0.3s ease both;
}

/* Wordle-ish stagger: tiles of a freshly-added guess pop in sequence. @key on the guess row
   keeps old rows' DOM, so only new tiles animate. */
.dq-tile.s0 { animation-delay: 0.00s; }
.dq-tile.s1 { animation-delay: 0.07s; }
.dq-tile.s2 { animation-delay: 0.14s; }
.dq-tile.s3 { animation-delay: 0.21s; }
.dq-tile.s4 { animation-delay: 0.28s; }
.dq-tile.s5 { animation-delay: 0.35s; }
.dq-tile.s6 { animation-delay: 0.42s; }
.dq-tile.s7 { animation-delay: 0.49s; }
.dq-tile.s8 { animation-delay: 0.56s; }
.dq-tile.s9 { animation-delay: 0.63s; }
.dq-tile.s10 { animation-delay: 0.70s; }
/* s11–s12 exist for Daily Animal's class-specific tiles: birds add three of them to the ten
   the board already draws. */
.dq-tile.s11 { animation-delay: 0.77s; }
.dq-tile.s12 { animation-delay: 0.84s; }

@keyframes dqTilePop {
    from { opacity: 0; transform: scale(0.85); }
    to { opacity: 1; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
    .dq-tile { animation: none; }
}

/* m = exact match, p = partial. x (no match) deliberately has no rule: it is the base tile. */
/* A match carries its green in the FILL only. The 1.5px green outline around a green fill drew
   a second, darker edge on every matched tile, and a board of ten guesses was mostly outline.
   The border stays in place as a transparent one so the tile keeps the same size as its
   neighbours. */
.dq-tile.m {
    border-color: transparent;
    background: color-mix(in srgb, #58CC02 20%, var(--mud-palette-background));
}
.dq-tile.p {
    border-color: #FFC800;
    background: color-mix(in srgb, #FFC800 14%, var(--mud-palette-background));
}
/* u = unknown: the dataset has no value for this field, so the tile is visibly inert rather
   than showing a match or a miss it cannot justify.

   The fade needs its own keyframe, not just `opacity: 0.5`: .dq-tile's pop runs with fill-mode
   `both`, and an animated property beats a declared one, so the tile's final frame (opacity: 1)
   won and these landed dashed but at full strength - while the rules legend promises "faded and
   dashed". The reduced-motion block below drops the animation entirely, at which point the
   declared 0.5 applies on its own. */
.dq-tile.u {
    opacity: 0.5;
    border-style: dashed;
    animation-name: dqTilePopFaded;
}

@keyframes dqTilePopFaded {
    from { opacity: 0; transform: scale(0.85); }
    to { opacity: 0.5; transform: scale(1); }
}

.dq-tile-label {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 0.64rem;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--mud-palette-text-secondary);
    white-space: nowrap;
    overflow: hidden;
}
.dq-tile-label i { font-size: 0.7rem; }

.dq-tile-value {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-weight: 900;
    font-size: 0.92rem;
    color: var(--mud-palette-text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

@media (max-width: 600px) {
    .dq-tile-value { font-size: 0.85rem; }
}

/* ── Hover hint ────────────────────────────────────────────────────────────
   The styled replacement for the native `title` tooltip, driven by
   wwwroot/scripts/hoverHint.js. Mark up the anchor and the script does the rest:

       <li data-hint-title="Order" data-hint="A large group in a class. …">

   Use it where the hint is part of the UI and must be read (a term the player
   does not know yet). A plain `title` is still correct for a small label that
   only repeats what is already on screen.

   One element is shared by every anchor, positioned with fixed coordinates, so
   a scroll container never clips it. Give the anchor `cursor: help` and a
   dotted underline so players know a hint is there. */
.hint-pop {
    position: fixed;
    z-index: 3000;
    left: 0;
    top: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
    max-width: min(300px, calc(100vw - 24px));
    padding: 9px 12px 10px;
    border: 2px solid var(--mud-palette-lines-default);
    border-radius: 14px;
    background: var(--mud-palette-surface);
    /* The design-system shelf, plus a soft drop so the hint reads as a layer
       above the board rather than as another tile on it. */
    box-shadow: 0 4px 0 0 var(--mud-palette-lines-default), 0 12px 28px rgba(0, 0, 0, 0.18);
    pointer-events: none;   /* a hint is never a hit target */
    opacity: 0;
    transform: translateY(4px);
    transition: opacity .12s ease, transform .12s ease;
}

.hint-pop.open {
    opacity: 1;
    transform: translateY(0);
}

.hint-pop-title {
    font-size: 0.6rem;
    font-weight: 900;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--hint-accent, var(--mud-palette-primary));
}

.hint-pop-body {
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1.45;
    color: var(--mud-palette-text-primary);
}

/* A rotated square showing two of its borders - the same trick as a speech
   bubble tail, so the tail keeps the popup's border and background. */
.hint-pop-arrow {
    position: absolute;
    width: 10px;
    height: 10px;
    margin-left: -5px;
    background: var(--mud-palette-surface);
    border: 2px solid var(--mud-palette-lines-default);
    transform: rotate(45deg);
}

.hint-pop-arrow { bottom: -7px; border-top: none; border-left: none; }
.hint-pop.below .hint-pop-arrow { bottom: auto; top: -7px; border: 2px solid var(--mud-palette-lines-default); border-bottom: none; border-right: none; }

@media (prefers-reduced-motion: reduce) {
    .hint-pop { transition: opacity .12s ease; transform: none; }
    .hint-pop.open { transform: none; }
}

/* Off screen but still read aloud - the text a hint popup shows visually, held
   in the accessibility tree for aria-describedby to point at. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ── Quiz card (see <QuizInfoLabelComponent>) ─────────────────────────────────
   Lives here, not in the component's own <style> block: the home page renders this
   card ~138 times, and a component <style> is emitted once per instance - which put
   ~340 KB of duplicated CSS in the prerendered HTML and made every card's stylesheet
   a sibling element in the grid, so :nth-child() counted styles as cards. */
/* One tap target, one job: open the quiz. The card carries no icon, no category chip and
   no Play button, so the title is the first thing read and the whole surface is clickable. */
.qil-card {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: 6px;
	padding: 14px 16px;
	border-radius: 16px;
	background: var(--mud-palette-surface);
	border: 2px solid var(--mud-palette-lines-default);
	color: var(--mud-palette-text-primary);
	text-align: left;
	text-decoration: none;
	font-family: 'Nunito', sans-serif;
	/* The design system's drop edge. It stays grey until the pointer is on the card: the
	   category colour arrives with the hover, and the badge carries it the rest of the time. */
	box-shadow: 0 3px 0 0 var(--mud-palette-lines-default);
	transition: transform 0.08s ease, box-shadow 0.08s ease, border-color 0.15s ease;
	min-height: 104px;
	overflow: hidden;
	cursor: pointer;
	-webkit-tap-highlight-color: transparent;
}

.qil-card:hover {
	border-color: var(--accent);
	box-shadow: 0 5px 0 0 var(--accent-shadow);
	transform: translateY(-2px);
}

/* Presses like the other duo buttons: the card sinks onto its own drop edge. */
.qil-card:active {
	transform: translateY(3px);
	box-shadow: 0 0 0 0 var(--accent-shadow);
}

.qil-card:focus-visible {
	outline: 3px solid color-mix(in srgb, var(--accent) 50%, transparent);
	outline-offset: 2px;
}

/* Full score already reached. The card keeps its normal ground: the green border, the
   green drop edge and the check mark in the meta row carry the state on their own. */
.qil-card.is-done {
	border-color: color-mix(in srgb, #58CC02 45%, var(--mud-palette-lines-default));
	box-shadow: 0 3px 0 0 #58A700;
}

.qil-card.is-done:hover {
	border-color: #58CC02;
	box-shadow: 0 5px 0 0 #58A700;
}

.qil-card.is-done:active {
	box-shadow: 0 0 0 0 #58A700;
}

/* Title row. The badge floats right, so the title's first line wraps around it and any
   second line runs the full width of the card - a flex row would narrow both lines and
   cut the title instead. The row itself clamps to two lines. */
.qil-head {
	display: block;
	max-height: 2.45em;
	overflow: hidden;
}

.qil-title {
	display: block;
	font-weight: 900;
	font-size: 1.02rem;
	line-height: 1.2;
	letter-spacing: -0.015em;
	color: var(--mud-palette-text-primary);
}

.qil-card:hover .qil-title { color: var(--accent); }

/* Four lines. Measured 2026-09-28 at three cards per row (1280px viewport): every home
   page description but one fits in four lines, and five of them needed the fourth. A
   grid row stretches to its tallest card, so the clamp only bounds the worst case. */
.qil-desc {
	font-size: 0.8rem;
	line-height: 1.4;
	color: var(--mud-palette-text-secondary);
	font-weight: 500;
	display: -webkit-box;
	-webkit-line-clamp: 4;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

/* Phone: no clamp at all. Cards stand one per row there, so a taller card costs nothing
   in row alignment, and a clipped third line was the only way to lose the end of a
   description (2026-09-28: the longest ones ran to four lines at 360px). */
@media (max-width: 600px) {
	.qil-desc {
		display: block;
		-webkit-line-clamp: none;
		overflow: visible;
	}
}

/* Pinned to the bottom so cards in a row line their play counts up, whatever the title
   and description above them cost. */
.qil-meta {
	display: flex;
	align-items: center;
	gap: 8px;
	margin-top: auto;
	padding-top: 8px;
}

/* The category badge beside the title: the subject the quiz belongs to, in that subject
   tile's colour. This is where the card carries its category while the drop edge stays grey. */
.qil-cat {
	float: right;
	max-width: 60%;
	margin: 1px 0 0 8px;
	padding: 2px 6px;
	border-radius: 6px;
	background: color-mix(in srgb, var(--accent) 15%, transparent);
	color: color-mix(in srgb, var(--accent) 72%, var(--mud-palette-text-primary));
	font-size: 0.55rem;
	font-weight: 900;
	line-height: 1.5;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* Dark theme: several tile colours are dark navies and browns, which disappear against
   the dark card. Take more of the light text colour into the mix so the badge stays
   readable, and lift the tint behind it. */
.mud-theme-dark .qil-cat {
	background: color-mix(in srgb, var(--accent) 26%, transparent);
	color: color-mix(in srgb, var(--accent) 40%, var(--mud-palette-text-primary));
}

.qil-done-mark {
	width: 18px;
	height: 18px;
	flex-shrink: 0;
	border-radius: 50%;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	font-size: 9px;
	background: #58CC02;
	color: #fff;
}

.qil-plays {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	font-weight: 700;
	font-size: 0.72rem;
	color: var(--mud-palette-text-secondary);
}

/* ── Pin control on a quiz card ───────────────────────────────────────────────
   The card is one anchor, so the pin is a sibling inside a wrapper cell, not a child.
   It sits bottom-right: the category badge holds the top right, the play count the
   bottom left. */
.qil-wrap {
	position: relative;
	display: flex;
}

.qil-wrap > .qil-card {
	flex: 1;
	width: 100%;
}

.qil-pin {
	position: absolute;
	right: 10px;
	bottom: 10px;
	z-index: 2;
	width: 25px;
	height: 25px;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 0;
	border: 2px solid var(--mud-palette-lines-default);
	border-radius: 50%;
	background: var(--mud-palette-surface);
	color: var(--mud-palette-text-secondary);
	font-size: 0.62rem;
	cursor: pointer;
	opacity: 0.65;
	-webkit-tap-highlight-color: transparent;
	transition: opacity 0.12s ease, background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

.qil-wrap:hover .qil-pin,
.qil-pin:focus-visible {
	opacity: 1;
}

.qil-pin:focus-visible {
	outline: 3px solid color-mix(in srgb, var(--accent) 50%, transparent);
	outline-offset: 2px;
}

/* Keep the card's own content clear of the pin: the play-count row always, and the
   description only on a card that has no play-count row to end on. */
.qil-wrap .qil-meta { padding-right: 29px; }

.qil-card:not(:has(.qil-meta)) .qil-desc { padding-right: 29px; }

/* A pinned card keeps the pin at full strength, so the state reads without a hover. The
   pin itself carries the category colour on a white face, which stays legible on a card of
   any colour and in both themes. On a pointer device it hides with the rest (see the hover
   block below), because a pinned quiz already sits in the pinned row. */
.qil-pin.is-pinned {
	opacity: 1;
	background: #fff;
	border-color: var(--accent);
	color: var(--accent);
}

/* Dark theme: a white disc is a bright hole in a dark card. The pin keeps the card's own
   surface instead. Several tile colours are dark navies and browns, so the glyph takes the
   same mix the category badge takes above, and the accent border still says pinned. */
.mud-theme-dark .qil-pin.is-pinned {
	background: var(--mud-palette-surface);
	color: color-mix(in srgb, var(--accent) 40%, var(--mud-palette-text-primary));
}

/* On a pointer device the pin stays hidden until the card is hovered, so a grid of cards
   reads as cards only. A touch device has no hover, so the pin stays visible there. */
@media (hover: hover) and (pointer: fine) {
	.qil-pin,
	.qil-pin.is-pinned {
		opacity: 0;
	}

	.qil-wrap:hover .qil-pin,
	.qil-wrap:focus-within .qil-pin,
	.qil-pin:focus-visible {
		opacity: 1;
	}
}

@media (prefers-reduced-motion: reduce) {
	.qil-card,
	.qil-card:hover,
	.qil-card:active {
		transition: none;
		transform: none;
	}
}

/* ── Mediavine (Journey) chrome ─────────────────────────────
   Their wrapper injects these elements at the end of <body>, outside Blazor. Only the look
   is changed here; the units and the privacy button stay theirs.

   Adhesion: on desktop and tablet the bar is full width around a 728px creative, and the
   backdrop is rgba(0,0,0,.9) unless the account has adhesion_light. They set the colour as
   a :root variable, so :root:root outranks their rule and the bar takes the page colour. */
:root:root {
	--adhesion-background-color: var(--app-page-bg);
}

.adhesion_wrapper {
	border-top: 1px solid var(--mud-palette-divider);
}

/* Their floating video player (Universal Player Plus, the videoV10 chunk) mounts VISIBLE in
   the bottom corner while its video ad auction still runs, and removes itself when no video
   bid comes (their debug log: abandon reason "noFill"). On most page views that is a black
   227x128 box that flashes for about 0.3s and vanishes right as the bottom bar fills.
   Verified 2026-11-05 against wrapper 3.89.8 on the live site with ?debugVideo=1; reported
   to Mediavine. Until they mount it hidden, this holds the player invisible while its ad
   container still carries the "loading" state class. The moment an ad or their content
   plays, their setState swaps the class (playing / content / ctp), the rule stops matching
   and the player shows at once. Fail open: if they rename a class, the rule stops matching
   and the flash returns, nothing worse. No impression is hidden: a "loading" player has no
   ad in it yet, and their own classic player (#universalPlayer_wrapper) also stays hidden
   while empty.
   The transition:none is required: their adopted stylesheet gives every child of
   #fixed_container_bottom "transition: all .3s ease-in" with a 500ms delay, which held the
   computed opacity at 1 for the whole life of a noFill mount. The container is a zero-size
   shell (the player inside animates itself), so no visible animation is lost. */
#fixed_container_bottom > .mv-floating-video {
	transition: none !important;
}

.mv-floating-video:has(.mv-ad-container.loading) {
	opacity: 0 !important;
	pointer-events: none !important;
}

/* Phone keyboard open (keyboardViewport.js puts kbd-open on <html>): every bottom ad goes
   away. With the keyboard up a phone keeps about 300px of page, and the bar took 50px to
   100px of that above the text field. The rule is CSS only, so the ads return by themselves
   when the field loses focus or the keyboard closes. mvAdhesionInset.js skips a display:none
   bar, so --ad-bottom drops to 0 and our fixed layers take the space back at once. Desktop
   never has kbd-open (the detector needs a coarse pointer), so the desktop bar is untouched.
   * #fixed_container_bottom: Journey's fixed box that holds the bar and the floating video.
   * body > .ym: a "high-impact" bottom unit from Yieldmo (seen 2026-11-01, a 320x100
     Disney+ ad). It is not inside the bar: Yieldmo adds its own position:fixed element to
     <body>, over the bar, so hiding the bar alone left it on screen. It then also covered
     the bottom of a quiz, because --ad-bottom had dropped to 0.
   * [data-kbd-ad]: any other ad unit of that kind. mvAdhesionInset.js marks it (a fixed
     child of <body> on the bottom edge, at an ad z-index).
   !important outranks the inline styles the units set; none of them sets display. */
html.kbd-open .adhesion_wrapper,
html.kbd-open #fixed_container_bottom,
html.kbd-open body > .ym,
html.kbd-open [data-kbd-ad] {
	display: none !important;
}

/* Privacy button: appended after the footer because the account has no footer_selector.
   Styled as a second footer row. Do not hide it or replace it: in the US the same button
   reads "DO NOT SELL OR SHARE MY PERSONAL INFORMATION", which is the legally required
   wording, and their CMP picks the label by region. */
#consumer-privacy-footer-wrapper {
	background: var(--app-page-bg);
	text-align: center;
	padding: 0 1.5rem 1rem;
}

#consumer-privacy-footer-wrapper .consumer-privacy-footer-btn {
	margin: 0 auto;
	padding: 0;
	font-family: inherit;
	font-size: 0.8rem;
	font-weight: 400;
	color: var(--mud-palette-text-secondary);
	cursor: pointer;
}

#consumer-privacy-footer-wrapper .consumer-privacy-footer-btn:hover,
#consumer-privacy-footer-wrapper .consumer-privacy-footer-btn:focus-visible {
	color: var(--mud-palette-text-primary);
	text-decoration: underline;
}

/* Desktop side column for Journey's sidebar units (App.razor, ad pages only). It sits in the
   LEFT gutter because the achievements tab is fixed to the right edge. It is position:fixed,
   so the unit stays in view while the page scrolls. Their sticky sidebar_btf fills it
   (account sidebar selector "#trivi-ad-rail", Slidebar toggle off). Journey places no sidebar
   unit on the home page "/", so the column is empty there.
   Two breakpoints, because the two kinds of page absorb the column differently:
   * A quiz page (html.quiz-ads on top of has-ad-rail) absorbs it from 1280px: its play area
     pads by --ad-rail-inset on BOTH sides, so it gets narrower and stays centred - nothing
     is pushed.
   * An ordinary page (.page-root) can only move RIGHT by 332px (16px + 300px + 16px), and
     below 1700px that leaves it hugging the right screen edge: the widest containers
     (1280px + padding, leaderboard / quiz menu) fill every pixel of what remains up to
     1612px, and at 1660px only 24px of air is left (measured 2026-11-08, Isak's 1536px
     laptop was the complaint). So the column shows on ordinary pages from 1700px only,
     where the worst case keeps ~80px of right margin.
   * 1880px and up: the gutter beside the centred 1280px content already fits 300px + 24px,
     so nothing moves at all.
   Below its breakpoint the column is display:none, and their wrapper skips a sidebar with no
   width. It is display:none too without html.has-ad-rail: in SpaMode the element is on every
   document, and the page script in App.razor sets the class only on a page that has the
   column (not "/", not a page without ads, not a category page - AdPolicy.RailFreePages). */
#trivi-ad-rail {
	display: none;
}

@media (min-width: 1280px) {
	#trivi-ad-rail {
		display: block;
		position: fixed;
		top: 88px;
		left: 16px;
		width: 300px;
		z-index: 5;
	}

	html:not(.has-ad-rail) #trivi-ad-rail {
		display: none;
	}

	/* An ordinary page (no html.quiz-ads) cannot absorb the column below 1700px without
	   being pushed against the right screen edge; the 1700px block below turns it back on. */
	html.has-ad-rail:not(.quiz-ads) #trivi-ad-rail {
		display: none;
	}

	/* The same space for a quiz page with the side column (AdPolicy.SideColumnGameplay). A
	   quiz is NOT moved right like .page-root: that put the play area right of the screen
	   centre (Isak, 2026-10-30). The quiz root keeps the full width, so the top bar runs edge
	   to edge, and the play area (the scroll container or stage under the top bar) pads by
	   this width on BOTH sides. The play area then stays centred on the screen and gets
	   narrower, as the results column does (QuizEndScreen .es-col): 616px at 1280px. */
	html.has-ad-rail {
		--ad-rail-inset: 332px;
	}

	/* The results column (Mediavine:ResultsSideColumn, App.razor page script): a quiz without a
	   side column gets one while its results screen shows. The board under the results screen
	   keeps its full width, so it does not move or redraw. */
	html.has-ad-rail.ad-rail-results {
		--ad-rail-inset: 0px;
	}

	/* A quiz results screen (QuizEndScreen, .es-root) keeps the full width, so its content is
	   centred on the screen and never pushed right by the ad. The column sits above it: the
	   results screen is a full-screen layer (z-index 9999). Toasts (10500) stay on top. */
	html.has-ad-rail:has(.es-root) #trivi-ad-rail {
		z-index: 10000;
	}
}

/* Ordinary pages: the column and the 332px move right, from 1700px (see the comment above
   #trivi-ad-rail for the measurements behind the number). If the column should ever return
   on narrower screens, the way is NARROWER PAGE CONTAINERS (so page + column fit side by
   side), never a bigger push: Isak ruled out moving content for the ad (2026-11-09). */
@media (min-width: 1700px) {
	html.has-ad-rail:not(.quiz-ads) #trivi-ad-rail {
		display: block;
	}

	html.has-ad-rail .page-root {
		padding-left: 332px;
	}
}

@media (min-width: 1880px) {
	#trivi-ad-rail {
		left: calc((100vw - 1280px) / 2 - 292px);
	}

	html.has-ad-rail .page-root {
		padding-left: 0;
	}

	/* From 1880px the column sits in the gutter beside the centred content, and a quiz root
	   paints under it (the column has a z-index, the root has none or a lower one), so the
	   play area needs no padding. */
	html.has-ad-rail {
		--ad-rail-inset: 0px;
	}
}

/* Results ads on a gameplay document (App.razor mediavineQuizClass; in SpaMode the page script
   in App.razor sets the class on every gameplay path). The bottom bar arrives with the results
   screen; on every quiz page it is there from the start (2026-10-06). Two things the ad tag does on an ad page must
   not happen on a quiz page:
   * It pads <body> by the height of the bottom bar, so page content scrolls clear of it. A
     quiz page never scrolls: its box already ends above the bar through --ad-bottom
     (EmptyLayout), so the padding would only make the document taller than the screen.
   * It appends its "Update Privacy Settings" block to the end of <body>. There is no footer
     row to move it into here, so it would sit below the quiz box. The same link is in the
     footer of every other page, one tap away on "Home". */
html.quiz-ads,
html.quiz-ads body {
	overflow: hidden;
}

/* Their padding rules sit in an adopted stylesheet, all !important, so specificity decides.
   "body.adhesion.highImpact_adhesion" (150px, the account has high-impact adhesion on) beat
   a plain "html.quiz-ads body" until 2026-10-06, and the page was 150px taller than the
   screen. The second selector outranks it. */
html.quiz-ads body,
html.quiz-ads body.adhesion.highImpact_adhesion {
	padding-bottom: 0 !important;
}

html.quiz-ads #consumer-privacy-footer-wrapper {
	display: none;
}

/* ──────────────────────────────────────────────────────────────
   TRIVI+ (paid, ad-free membership). Brand tokens shared by the badge, the /plus page and the
   app bar button, then the badge itself (Components/Shared/PlusBadge.razor, an inline SVG).
   Flat colours from the site palette and the 3D shelf, no gradients: brand green, and gold
   for the badge's star rosette rim. The badge styles live here and not in the component: a
   leaderboard renders it 50 times, and a component <style> block is written once per instance.
   ────────────────────────────────────────────────────────────── */
:root {
	--plus-green: #58CC02;
	--plus-green-face: #79DE33;
	--plus-green-shade: #46A302;
	--plus-gold: #FFC800;
	--plus-gold-shade: #E09B00;
	--plus-ink: #3F8F00;
	--plus-soft: rgba(88, 204, 2, 0.12);
}

.mud-theme-dark {
	--plus-ink: #8FE05C;
	--plus-soft: rgba(88, 204, 2, 0.16);
}

/* Size: the size class sets --pb-default. An explicit --pb-size wins, whether it is set on the
   badge itself (".x .plus-badge { --pb-size: 48px }") or on any wrapper (".x { --pb-size: 48px }").
   The width is the size; the SVG is 48 x 52, so the height follows from the aspect ratio. */
.plus-badge {
	--pb: var(--pb-size, var(--pb-default, 18px));
	--pb-rim-fill: var(--plus-gold);
	--pb-rim-shade: var(--plus-gold-shade);
	--pb-disc-fill: var(--plus-green);
	--pb-disc-shade: var(--plus-green-shade);
	display: inline-block;
	flex: none;
	width: var(--pb);
	height: auto;
	aspect-ratio: 48 / 52;
	vertical-align: middle;
	overflow: visible;
	transition: transform 0.15s ease;
}

.plus-badge .pb-rim-shade { fill: var(--pb-rim-shade); }
.plus-badge .pb-rim { fill: var(--pb-rim-fill); }
.plus-badge .pb-disc-shade { fill: var(--pb-disc-shade); }
.plus-badge .pb-disc { fill: var(--pb-disc-fill); }
.plus-badge .pb-plus { fill: #fff; }
.plus-badge .pb-ps { fill: var(--pb-disc-shade); display: none; }
.plus-badge-md .pb-ps,
.plus-badge-lg .pb-ps,
.plus-badge-xl .pb-ps { display: inline; }

.plus-badge-xs { --pb-default: 14px; }
.plus-badge-sm { --pb-default: 18px; }
.plus-badge-md { --pb-default: 24px; }
.plus-badge-lg { --pb-default: 40px; }
.plus-badge-xl { --pb-default: 64px; }

.plus-badge-link {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	flex: none;
	line-height: 1;
	text-decoration: none;
	vertical-align: middle;
}

a.plus-badge-link:hover .plus-badge { transform: translateY(-1px); }
a.plus-badge-link:focus-visible { outline: 3px solid rgba(88, 204, 2, 0.45); outline-offset: 2px; border-radius: 6px; }
