:root {
    --bg: #f4f5f8;
    --surface: #ffffff;
    --surface-hover: #f7f8fa;
    --border: #e4e6eb;
    --text: #16181d;
    --text-muted: #6b7280;
    /* Primary blue-violet, plus two accents derived from it (teal,
       orange) - the "flottere design" pass (see CLAUDE.md), reusing the
       token system rather than introducing a new one. Kept to exactly
       two extra accents, as scoped. */
    --accent: #5b57ec;
    --accent-hover: #4944d4;
    --accent-contrast: #ffffff;
    --accent-bg: #eeedfd;
    --accent-teal: #0d9488;
    --accent-teal-bg: #e3f7f4;
    --accent-orange: #ea7f1f;
    --accent-orange-bg: #fdf0e1;
    --danger: #d1293d;
    --danger-bg: #fdecee;
    --warning: #8a6100;
    --warning-bg: #fdf0d1;
    --success: #1a7f4b;
    --success-bg: #e3f6ec;
    /* A plain, neutral "this text is a link" blue - distinct from
       --accent (purple-violet, reserved for primary actions/brand) so a
       clickable name in a list reads as "clickable" without competing
       with a real button nearby. Not one of the redesign's own capped
       "two extra accents" (teal/orange, both stat-card-specific) - this
       is a generic link color, the kind most UI kits have separately. */
    --link: #2454c7;
    --link-hover: #1b3d99;
    /* Deliberately identical in both themes (not redefined under the dark
       media query below) - a couple of surfaces (e.g. the JID code
       badge) are meant to always look the same dark chip
       regardless of the site's own light/dark theme, not flip with it. */
    --ink: #16181d;
    --ink-contrast: #ffffff;
    /* A bright gold, not --accent-orange (true orange - correct for the
       points stat card and the continent badge, but a different hue from
       this pairing's own dark-background accents: the location-redesign
       mockup's rule-number circles and point-tooltip/scoring-card figures
       are all a warm gold, hue ~90, distinct from --accent-orange's hue
       ~45. Two distinct shades, not one averaged value: the mockup uses a
       darker, more saturated oklch(.78 .15 90) only for the filled
       rule-number badge background, and a lighter, less saturated
       oklch(.85 .13 90) for every gold *text* value on the dark surfaces
       (tooltip figures, bonus/minmax numbers) - sampled to sRGB via
       canvas since this browser's getComputedStyle doesn't resolve
       oklch() to rgb(). Deliberately identical in both themes, same
       reasoning as --ink above - these two dark surfaces (the point-chip
       tooltip, the scoring card) don't flip with the site's light/dark
       theme. :root-level (not page-scoped) since both the participant
       page and the main location page's "Seneste fund"/points-system
       card use them - was page-scoped to .loc-participant-content at
       first, which silently broke the main location page's own point
       tooltip (var(--gold-text) resolved to nothing, so the value fell
       back to the tooltip's inherited white) until promoted here. */
    --gold-bg: #ddb227;
    --gold-text: #efca61;
    --radius: 14px;
    --radius-sm: 8px;
    --radius-lg: 22px;
    --shadow: 0 1px 2px rgba(16, 20, 30, 0.04), 0 12px 32px rgba(16, 20, 30, 0.06);
    --shadow-lg: 0 4px 12px rgba(16, 20, 30, 0.08), 0 16px 40px rgba(16, 20, 30, 0.1);
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #0e0f13;
        --surface: #191b21;
        --surface-hover: #21232b;
        --border: #2b2e37;
        --text: #eceef2;
        --text-muted: #9aa0ab;
        --accent: #8b87ff;
        --accent-hover: #a19dff;
        --accent-contrast: #0e0f13;
        --accent-bg: #232052;
        --accent-teal: #2dd4bf;
        --accent-teal-bg: #0f2e2b;
        --accent-orange: #f5a259;
        --accent-orange-bg: #3a2712;
        --danger: #ff6b7a;
        --danger-bg: #34191d;
        --warning: #e8b93f;
        --warning-bg: #3a2e0f;
        --success: #4ade80;
        --success-bg: #123122;
        --link: #7ba2ff;
        --link-hover: #a3c0ff;
        --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 12px 32px rgba(0, 0, 0, 0.35);
        --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.35), 0 16px 40px rgba(0, 0, 0, 0.4);
    }
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    min-height: 100vh;
    /* Outfit used to be scoped to just the location-redesign pages (see
       main.location-canvas/main.app-canvas below) - now loaded sitewide
       from header.php's own <head> and applied here instead, so the
       generic topbar (header.php's default, non-hideHeaderChrome branch)
       reads as the same design as the location pages instead of falling
       back to the plain system stack. Still a plain fallback stack after
       it, same as before, in case the Google Fonts request is
       slow/blocked. See CLAUDE.md. */
    font-family: 'Outfit', ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    background:
        radial-gradient(circle at 15% -10%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 45%),
        var(--bg);
    color: var(--text);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
}

/* flex-wrap: wrap (found testing at iPhone SE's 320px width) - without
   it, .brand plus .header-controls' own profile badge (its name can run
   long, e.g. an admin's) and menu icon simply overflowed the viewport
   horizontally on the handful of pages still on this older, plain header
   (ui/log/*, ui/control/login|register|name-location - see CLAUDE.md)
   instead of wrapping onto a second line, the way .loc-topbar's own
   identical layout already does. */
header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 0.9rem 1.5rem;
    border-bottom: 1px solid var(--border);
    background: color-mix(in srgb, var(--surface) 85%, transparent);
    backdrop-filter: blur(8px);
    position: sticky;
    top: 0;
    z-index: 10;
}

.brand {
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--text);
    text-decoration: none;
}

.header-controls {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.menu {
    position: relative;
}

.menu-toggle {
    all: unset;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    width: 34px;
    height: 34px;
    padding: 8px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background 0.15s ease;
}

.menu-toggle:hover {
    background: var(--surface-hover);
}

.menu-toggle span {
    display: block;
    height: 2px;
    border-radius: 999px;
    background: var(--text);
    transition: transform 0.2s ease, opacity 0.15s ease, background 0.15s ease;
}

.menu-toggle:hover span {
    background: var(--accent);
}

/* Morphs the three bars into a close (X) icon while the dropdown it
   controls is open - :has() reacts to #menu-dropdown's own [hidden]
   attribute directly, so this needs no JS change: menuToggle's click
   handler only ever flips that one attribute (see app.js), on every
   page's own copy of this markup alike. translateY's 6px = one span's
   own height (2px) plus the gap between spans (4px) - exactly enough to
   land the top/bottom bars on the middle bar's own line. */
.menu:has(#menu-dropdown:not([hidden])) .menu-toggle span:nth-child(1) {
    transform: translateY(6px) rotate(45deg);
}

.menu:has(#menu-dropdown:not([hidden])) .menu-toggle span:nth-child(2) {
    opacity: 0;
}

.menu:has(#menu-dropdown:not([hidden])) .menu-toggle span:nth-child(3) {
    transform: translateY(-6px) rotate(-45deg);
}

.menu-dropdown {
    position: absolute;
    /* Leaflet's own controls sit at z-index 1000 (see
       vendor/leaflet/leaflet.css) - without a higher z-index here, the
       location page's map would paint over this dropdown when open,
       since it had no z-index at all (implicit "auto") on pages with a
       map on them, unlike every other page using this same shared
       component. */
    z-index: 1100;
    top: calc(100% + 0.6rem);
    right: 0;
    display: flex;
    flex-direction: column;
    min-width: 180px;
    padding: 0.4rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    animation: menu-in 0.12s ease;
}

/* Equal-specificity tiebreak: without this, some browsers (Firefox) let
   the display:flex above win over the browser's own [hidden] rule, since
   both have the same specificity and this stylesheet loads later. */
.menu-dropdown[hidden] {
    display: none;
}

@keyframes menu-in {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.menu-dropdown a {
    padding: 0.6rem 0.75rem;
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--text);
    text-decoration: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
}

.menu-dropdown a:hover {
    background: var(--surface-hover);
}

/* The language switcher's own dropdown row - a plain "Sprog"/"Language"
   label plus two flags, laid out and sized like every other row in this
   dropdown (.menu-dropdown a's own padding/height) rather than its own
   separate boxed control, so it reads as one more menu item instead of
   a visually distinct widget bolted onto the bottom of the list. */
.menu-lang-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* A floor on top of space-between - the dropdown's own min-width
       (180px) leaves little real "space between" once "Sprog" and two
       big flags are laid out, so without this the label and the first
       flag ended up sitting right next to each other. */
    gap: 0.75rem;
    padding: 0.6rem 0.75rem;
}

.menu-lang-label {
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--text);
}

/* Deliberately a fully self-contained rule - no border/background/
   padding/border-radius of its own at all (unlike .lang-switch, the
   bordered pill-track this used to also carry as a second class - see
   app.js's own comment on why that's gone now). Nothing here for a
   button-level rule to need to override or reset. */
.menu-lang-switch {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* Two plain, big flags, no button chrome (no border/background/shadow
   of any kind) - the unselected one desaturated to near black-and-white
   (grayscale, not opacity dimming, which - on the flag glyph's own
   colors - still read as a colored, if faint, pill) and back to full
   color on hover; the selected one is always full color. */
.menu-lang-switch button {
    all: unset;
    font-size: 2rem;
    line-height: 1;
    cursor: pointer;
    filter: grayscale(1);
    transition: filter 0.15s ease;
}

.menu-lang-switch button:hover {
    filter: grayscale(0);
}

.menu-lang-switch button.active {
    filter: none;
}

/* A small avatar+name link, top of the topbar where the language
   switcher used to sit (see CLAUDE.md's "sitewide profile badge"
   entry) - built by app.js, once logged in as either a participant or
   an admin. Deliberately the exact same box model as .add-jid-button
   (min-height/padding scale/border-radius/font-weight/font-size), just
   a white/card surface instead of that button's purple gradient - on
   request, after the two ended up visually mismatched (a full pill next
   to a squarer, taller-reading chip) despite sitting right next to each
   other in every topbar. The left padding stays smaller than the
   right, to hug the round avatar snugly the way .add-jid-button has no
   equivalent leading icon to balance against. */
.profile-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    box-sizing: border-box;
    /* A real height, not min-height - the 34px avatar plus any
       vertical padding would otherwise push this past 44px regardless
       of a min-height that only ever raises a *shorter* box, never
       caps a taller one (a real, reported mismatch: this badge
       measured 60px tall next to .add-jid-button's own real 44px).
       Padding is horizontal-only here for the same reason; align-items:
       center on this and the avatar/name inside still center both
       within the fixed 44px box. */
    height: 44px;
    padding: 0 1.4rem 0 0.6rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow);
    color: var(--text);
    font-weight: 700;
    font-size: 0.95rem;
    font-family: inherit;
    text-decoration: none;
    cursor: pointer;
    max-width: 200px;
}

.profile-badge:hover {
    background: var(--surface-hover);
}

.profile-badge span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A participant's own badge is a <button> now, not a plain link - it
   toggles a two-item dropdown ("Min side"/"Log ud") instead of
   navigating straight to Min side, since the hamburger menu (the only
   other place "Log ud" used to live) no longer exists for anyone but an
   admin (on request - see CLAUDE.md). An admin's own badge is still a
   plain <a>, unaffected - this wrapper only matters for the button
   case. position: relative so the dropdown below can anchor to it the
   same way .menu-dropdown already anchors to .menu. */
.profile-badge-wrap {
    position: relative;
}

/* Reuses .menu-dropdown's own look/animation/[hidden] fix wholesale
   (same class, see below) - just anchored under this wrapper instead of
   under .menu, and with no min-width of its own (two short items don't
   need the hamburger dropdown's 180px floor, which at the right edge of
   a narrow phone screen left more empty padding than this one plain
   avatar-sized badge really wanted). */
.profile-badge-dropdown {
    min-width: 0;
}

/* The language switcher's new home for anyone without a hamburger menu
   (a brand-new visitor, or a logged-in participant) - sits in the exact
   topbar slot the hamburger icon itself used to occupy (on request: see
   CLAUDE.md's "no menu at all" entry). Reuses .menu-lang-switch's own
   button styling (grayscale/active, see above) wholesale; only the
   flag size and the lack of a dropdown wrapper around it differ here -
   a topbar icon can afford to be a bit smaller than a dropdown row's
   own larger touch target.
*/
.topbar-lang-switch {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.topbar-lang-switch button {
    font-size: 1.4rem;
}

/* The one remaining way to reach /control/login for anyone who isn't
   already a real admin, now that the hamburger menu (the only place
   this used to live) only shows for an admin themselves (on request -
   see CLAUDE.md). Fixed to the viewport's own bottom-right corner, not
   the end of each page's own content flow - a persistent, reachable-
   without-scrolling corner link, deliberately low-emphasis (small,
   muted text, no button chrome) so it never competes with a page's own
   real primary actions. */
.admin-login-corner-link {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    z-index: 900;
    padding: 0.4rem 0.6rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow);
    color: var(--text-muted);
    font-size: 0.8rem;
    font-weight: 500;
    text-decoration: none;
}

.admin-login-corner-link:hover {
    color: var(--text);
    background: var(--surface-hover);
}

/* Reserves enough bottom clearance that the fixed corner link above never
   overlaps a page's own real bottom content once scrolled all the way
   down (app.js adds this class only while that link actually exists) -
   found testing at iPhone SE's short viewport, where the location page's
   map legend ended right where the link sits. Named specifically enough
   (body class + each main variant) to still win against
   main.app-canvas/.location-canvas's own `padding` shorthand resets
   further down this file (including inside their narrow-viewport @media
   rule) regardless of source order - a bare `body.has-admin-login-corner
   main` would tie with those on specificity and lose to whichever comes
   later. */
body.has-admin-login-corner main,
body.has-admin-login-corner main.app-canvas,
body.has-admin-login-corner main.location-canvas {
    padding-bottom: 3.5rem;
}

main {
    max-width: 440px;
    margin: 3.5rem auto;
    padding: 2.25rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

main h1 {
    font-size: 1.3rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    margin: 0 0 0.25rem;
}

main > p:first-of-type:not(.message) {
    color: var(--text-muted);
    margin-top: 0.75rem;
}

label {
    display: block;
    font-size: 0.85rem;
    font-weight: 600;
    margin-top: 1.5rem;
    margin-bottom: 0.4rem;
}

input[type="text"],
input[type="email"],
input[type="date"],
input[type="time"] {
    display: block;
    width: 100%;
    padding: 0.65rem 0.8rem;
    font-size: 1rem;
    font-family: inherit;
    color: var(--text);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

input[type="text"]:focus,
input[type="email"]:focus,
input[type="date"]:focus,
input[type="time"]:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}

/* Chrome/Edge/Safari's own calendar/clock icon inside a date/time input -
   without this it renders in the browser's default dark-gray, which
   reads as noticeably duller than the rest of this form's own purple
   accent (caught from a live screenshot, not obvious from the CSS
   alone). Firefox has no equivalent icon to restyle, so this is a
   no-op there, not a missing fallback. */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
    cursor: pointer;
    opacity: 0.65;
    filter: invert(38%) sepia(53%) saturate(3000%) hue-rotate(230deg) brightness(97%) contrast(96%);
}

/* The webmaster settings page's own timezone picker - a search-as-you-
   type combobox (on request: hundreds of zones is too many to scan in
   a plain <select>), not a vendored autocomplete widget. The visible
   #timezone-search text input already gets the shared input[type="text"]
   look above; this only adds the dropdown list beneath it. */
.tz-combobox {
    position: relative;
}

.tz-listbox {
    position: absolute;
    z-index: 20;
    top: calc(100% + 0.4rem);
    left: 0;
    right: 0;
    max-height: 280px;
    overflow-y: auto;
    margin: 0;
    padding: 0.4rem;
    list-style: none;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
}

/* Same equal-specificity [hidden] tiebreak as .menu-dropdown/
   .loc-sort-toggle elsewhere in this file - without it, some browsers
   let the display rules below win over the browser's own [hidden] rule. */
.tz-listbox[hidden] {
    display: none;
}

.tz-option {
    display: grid;
    grid-template-columns: 1fr auto;
    column-gap: 0.6rem;
    row-gap: 0.1rem;
    padding: 0.5rem 0.6rem;
    border-radius: var(--radius-sm);
    cursor: pointer;
}

.tz-option:hover {
    background: var(--surface-hover);
}

/* The currently-chosen zone, when browsing the full unfiltered list
   (focusing the search box with nothing typed - see
   ui/control/config/index.php) - sorted to the very top there so it's
   immediately visible, and given its own accent tint here so it still
   reads as "this one's already selected" once you scroll past it. */
.tz-option.active {
    background: var(--accent-bg);
}

.tz-option.active .tz-option-id {
    color: var(--accent);
}

.tz-option-id {
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.85rem;
    font-weight: 700;
    color: var(--text);
}

.tz-option-offset {
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.78rem;
    font-weight: 700;
    color: var(--accent);
    white-space: nowrap;
}

.tz-option-long {
    grid-column: 1 / -1;
    font-size: 0.78rem;
    color: var(--text-muted);
}

.tz-listbox-empty {
    padding: 0.6rem;
    font-size: 0.85rem;
    color: var(--text-muted);
    text-align: center;
}

/* A "Starts"/"Ends" date+time pair, side by side under one shared
   legend (control/location-edit's own opening-hours form) - reads
   better as one grouped moment in time than four separate full-width
   label+input stacks, each repeating "Start"/"End" in its own label. */
.date-time-group {
    margin: 1.5rem 0 0;
    padding: 0;
    border: none;
}

.date-time-group legend {
    padding: 0;
    font-size: 0.85rem;
    font-weight: 600;
}

.date-time-row {
    display: flex;
    gap: 0.75rem;
    margin-top: 0.4rem;
}

.date-time-row > div {
    flex: 1;
    min-width: 0;
}

.date-time-row label {
    margin-top: 0;
    font-weight: 500;
    color: var(--text-muted);
}

.hint {
    color: var(--text-muted);
    font-size: 0.85rem;
    margin-top: 0.5rem;
}

button[type="submit"] {
    display: inline-block;
    width: 100%;
    margin-top: 1.75rem;
    padding: 0.7rem 1.25rem;
    font-size: 0.95rem;
    font-weight: 600;
    font-family: inherit;
    color: var(--accent-contrast);
    background: var(--accent);
    border: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background 0.15s ease, transform 0.05s ease;
}

/* Pairs a form's own submit button with a neutral "Annuller"/"Cancel"
   link back out of editing (control/participant-edit, the opening-hours
   form on control/location-edit) - side by side, submit taking the
   remaining space rather than each stacking full-width on its own line
   the way a lone submit button normally does. */
.form-actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-top: 1.75rem;
}

.form-actions button[type="submit"] {
    flex: 1;
    margin-top: 0;
}

.form-actions .cancel-link {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.7rem 1.25rem;
    font-size: 0.95rem;
    font-weight: 600;
    font-family: inherit;
    color: var(--text);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    text-decoration: none;
    cursor: pointer;
    transition: background 0.15s ease;
}

.form-actions .cancel-link:hover {
    background: var(--surface-hover);
}

button[type="submit"]:hover {
    background: var(--accent-hover);
}

button[type="submit"]:active {
    transform: translateY(1px);
}

button[type="submit"]:disabled {
    opacity: 0.5;
    cursor: default;
}

/* Same .btn-danger reasoning as the .btn-chip variant above, for the
   one submit button that actually carries out a location's own
   destructive delete (its "are you sure" already happened via the
   emailed-code step itself - see CLAUDE.md). */
button[type="submit"].btn-danger {
    color: #fff;
    background: var(--danger);
}

button[type="submit"].btn-danger:hover {
    background: color-mix(in srgb, var(--danger) 85%, black);
}

.message {
    margin-top: 1.5rem;
    padding: 0.75rem 1rem;
    border-radius: var(--radius-sm);
    background: var(--bg);
    font-size: 0.9rem;
}

.message.error {
    background: var(--danger-bg);
    color: var(--danger);
}

.message.created {
    background: var(--warning-bg);
    color: var(--warning);
}

.message.created ul {
    margin: 0.5rem 0 0;
    padding-left: 1.25rem;
}

.message.created p {
    margin: 0;
    font-weight: 600;
}

/* The "you just added a code" result card (see
   ui/location/participant/index.php) - a point receipt (left) plus,
   only when this code unlocked a new avatar part, a reveal of it
   (right). Design provided directly, see CLAUDE.md - replaced the
   earlier .message.created banner + separate .avatar-unlock-reveal box.
   `.no-unlock` (no 2nd column to show - either from the start, or once
   the reveal removes itself via its own two actions below) collapses
   back to one. */
.code-result-card {
    display: flex;
    flex-wrap: wrap;
    overflow: hidden;
    border-radius: 1.5rem;
    background: var(--surface);
    box-shadow: var(--shadow-lg);
    margin-bottom: 1.5rem;
}

.code-result-left {
    display: flex;
    min-width: 0;
    flex: 9 1 380px;
    flex-direction: column;
    gap: 1.1rem;
    padding: 1.75rem 2rem;
}

/* No 2nd column to show (from the start, or once the reveal removes
   itself via its own "Tag den i brug" - see below) - the left column
   just takes the full card instead of leaving room for one. */
.code-result-card.no-unlock .code-result-left {
    flex: 1 1 380px;
}

.code-result-top {
    display: flex;
    align-items: center;
    gap: 0.9rem;
}

.code-result-badge {
    display: grid;
    place-items: center;
    flex-shrink: 0;
    width: 52px;
    height: 52px;
    border-radius: 1rem;
    background: var(--success-bg);
    color: var(--success);
    font-size: 1.35rem;
    font-weight: 800;
}

.code-result-heading {
    margin: 0;
    font-size: 1.5rem;
    font-weight: 800;
}

.code-result-subtitle {
    margin: 0.15rem 0 0;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--text-muted);
}

.code-result-code {
    display: inline-block;
    margin: 0 0.15rem;
    padding: 0.05rem 0.4rem;
    border-radius: 0.35rem;
    background: var(--bg);
    color: var(--text);
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.85rem;
}

/* Only ever holds reasons that actually earned points - see
   buildAwardsAndFacts()/CLAUDE.md. A fixed 2-up grid rather than a
   single-column list, so a maximum submission's 6-7 awards still fit in
   3-4 short rows instead of a long scroll. Deliberately a *fixed* 2
   columns, not `repeat(auto-fit, minmax(...))` - auto-fit's empty-track
   collapsing turned out not to happen here: the streak award's own
   `grid-column: 1 / -1` still touches every auto-fit-sized track via
   its span, so none of them ever counted as "empty" and none collapsed
   - a real reported bug, two plain award boxes sitting narrow with a
   large blank gap where a 3rd (entirely unused) auto-fit track had
   been reserved next to them. A fixed 2 columns has no tracks to
   (fail to) collapse in the first place. */
.code-result-awards {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 0.5rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* An odd number of plain awards leaves the last one alone in its own
   row - given to it explicitly (see the JS that builds this list)
   rather than left to sit in column 1 alone with column 2 blank next
   to it, the same dead-space problem this whole rule exists to avoid. */
.code-result-award.span-2 {
    grid-column: 1 / -1;
}

.code-result-award {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.625rem 0.75rem;
    border-radius: 0.75rem;
    border: 1px solid var(--success-bg);
    background: color-mix(in srgb, var(--success-bg) 55%, transparent);
}

.code-result-award-text {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.9rem;
    font-weight: 700;
}

.code-result-award-points {
    flex-shrink: 0;
    padding: 0.1rem 0.55rem;
    border-radius: 999px;
    background: var(--success-bg);
    color: var(--success);
    font-size: 0.85rem;
    font-weight: 800;
}

/* The streak award is visually distinct (a bolt icon, indigo instead of
   green) and always spans the full row width, whatever column it would
   otherwise have landed in - it reads as a different *kind* of bonus
   (a run of activity, not a one-off "first"/"new for you" find), and
   its own text ("Streak — 4. time i træk, maksimum") tends to run
   longer than the others. */
.code-result-award.streak {
    grid-column: 1 / -1;
    border-color: color-mix(in srgb, var(--accent) 30%, var(--border));
    background: var(--accent-bg);
}

.code-result-award.streak .code-result-award-points {
    background: color-mix(in srgb, var(--accent) 20%, var(--accent-bg));
    color: var(--accent);
}

.code-result-award-bolt {
    display: inline-flex;
    flex-shrink: 0;
    color: var(--accent);
}

/* The purely informational half (see buildAwardsAndFacts()) - never a
   point-earning box, just plain sentences below a small caption,
   separated from the awards above by a hairline. */
.code-result-facts {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--border);
}

.code-result-facts-title {
    margin: 0;
    font-size: 0.7rem;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
}

.code-result-fact {
    margin: 0;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--text-muted);
    line-height: 1.5;
}

.code-result-unlock {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 0.9rem;
    flex: 1 1 300px;
    max-width: 100%;
    padding: 1.75rem 2rem;
    background: var(--warning-bg);
}

.code-result-unlock-label {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    font-size: 0.7rem;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--warning);
}

.code-result-unlock-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--warning);
}

.code-result-unlock-title {
    margin: 0;
    font-size: 1.4rem;
    font-weight: 800;
    color: var(--warning);
}

.code-result-unlock-avatar-wrap {
    position: relative;
    width: 120px;
}

.code-result-unlock-avatar-wrap .avatar-stack {
    border: 3px solid var(--ink);
}

.code-result-unlock-new-badge {
    position: absolute;
    bottom: -6px;
    left: 50%;
    transform: translateX(-50%);
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    border: 2px solid var(--warning-bg);
    background: var(--accent-orange);
    color: #ffffff;
    font-size: 0.65rem;
    font-weight: 800;
    letter-spacing: 0.04em;
    white-space: nowrap;
}

.code-result-unlock-button {
    align-self: stretch;
    border: none;
    border-radius: 0.75rem;
    padding: 0.75rem 1.4rem;
    background: var(--accent);
    color: #ffffff;
    font-size: 1rem;
    font-weight: 800;
    cursor: pointer;
    box-shadow: var(--shadow);
}

.code-result-unlock-button:hover {
    background: var(--accent-hover);
}

.name-field {
    position: relative;
}

.name-field input {
    padding-right: 2.5rem;
}

.name-ok {
    position: absolute;
    right: 0.8rem;
    top: 50%;
    transform: translateY(-50%);
    color: #1a9d5c;
    font-weight: 700;
    font-size: 1.1rem;
}

.pin-boxes {
    justify-content: flex-start;
}

ul.locations {
    list-style: none;
    padding: 0;
    margin: 1.25rem 0 0;
}

ul.locations li {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: 0.85rem 0;
    border-bottom: 1px solid var(--border);
}

ul.locations li:last-child {
    border-bottom: none;
}

/* The invites page's admin/invite rows also carry a full-width line of
   location chips underneath (see locationsEditor() in
   ui/control/invites/). */
#admins-list li {
    flex-wrap: wrap;
    gap: 0.4rem 0.75rem;
}

.admin-locations {
    flex-basis: 100%;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
}

.admin-location-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.2rem 0.3rem 0.2rem 0.7rem;
    border-radius: 999px;
    background: var(--accent-bg);
    color: var(--accent);
    font-size: 0.85rem;
    font-weight: 600;
}

.admin-location-remove {
    width: 1.35rem;
    height: 1.35rem;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}

.admin-location-remove:hover:not(:disabled) {
    background: var(--danger-bg);
    color: var(--danger);
}

.admin-location-remove:disabled {
    opacity: 0.35;
    cursor: default;
}

.admin-location-add {
    width: auto;
    padding: 0.2rem 0.5rem;
    font-size: 0.85rem;
}

.invite-locations-fieldset {
    margin: 0 0 1rem;
    padding: 0;
    border: none;
}

.invite-locations-fieldset legend {
    margin-bottom: 0.4rem;
    font-weight: 600;
}

.invite-locations-choices {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.invite-location-choice {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0;
    font-weight: 500;
}

ul.locations .year {
    color: var(--text-muted);
    font-size: 0.85rem;
}

ul.locations a {
    color: var(--accent);
    font-weight: 600;
    text-decoration: none;
}

ul.locations a:hover {
    color: var(--accent-hover);
    text-decoration: underline;
}

/* "Mine lokationer" (see ui/control/locations/index.php) - one wide card
   per location instead of one plain text row, each with its own key
   figures (participants/countries/continents/points, from
   LocationService::listLocations()'s own subqueries - see CLAUDE.md) and
   an edit button, rather than the old flat "here's every management
   sub-page" link list. Wider than .admin-card's own 640px default - a
   "big, nice" list (on request) reads better with more room per card,
   and there's rarely more than a handful of locations to show at once. */
.location-cards {
    max-width: 900px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

.location-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    padding: 1.5rem;
}

.location-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.25rem;
}

.location-card-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.location-card-title {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    min-width: 0;
}

/* Matches .loc-topbar-name's own scale (the same big Outfit heading
   every individual location's own page uses) - on request, next to the
   same .loc-code-badge this card and that topbar both already share,
   the previous 1.15rem name read as a visibly different, smaller
   product than the page "Rediger" leads to. */
.location-card-name {
    font-size: 3.75rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1;
}

.location-card-unnamed {
    color: var(--text-muted);
    font-style: italic;
}

/* Scaled up to match, same secondary-to-a-big-heading role as
   .loc-topbar-name-suffix elsewhere (a smaller, lighter-weight detail
   sitting right next to the big name) rather than the old 0.85rem,
   which read as barely-there next to a 60px heading. */
.location-card-year {
    color: var(--text-muted);
    font-size: 1.75rem;
    font-weight: 700;
}

.location-card-stats {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 1rem;
}

.location-card-stat {
    text-align: center;
    padding: 0.75rem 0.5rem;
    background: var(--bg);
    border-radius: var(--radius-sm);
}

.location-card-stat-value {
    margin: 0 0 0.15rem;
    font-size: 1.5rem;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

.location-card-stat-label {
    margin: 0;
    font-size: 0.75rem;
    color: var(--text-muted);
}

@media (max-width: 560px) {
    .location-card-stats {
        grid-template-columns: repeat(2, 1fr);
    }
}

.participant-manage-list {
    list-style: none;
    padding: 0;
    margin: 1.25rem 0 0;
}

.participant-manage-list li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    padding: 0.85rem 0;
    border-bottom: 1px solid var(--border);
}

.participant-manage-list li:last-child {
    border-bottom: none;
}

.participant-manage-list .participant-name-form {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.participant-manage-list .participant-name-input {
    width: auto;
    padding: 0.4rem 0.6rem;
}

.participant-manage-list .participant-name-form button,
.participant-manage-list .profile-field button {
    width: auto;
    margin-top: 0;
    padding: 0.4rem 0.9rem;
    font-size: 0.85rem;
}

/* .badge sets its own `display: inline-block`, which otherwise beats the
   browser's `[hidden] { display: none }` at equal specificity (this
   stylesheet loads after the UA one) - the same trap already documented
   several times elsewhere in this file for other badges/buttons. Without
   this override, every row's "Locked out" badge rendered regardless of
   the actual lockedOut value. */
.locked-out-badge[hidden] {
    display: none;
}

.locked-out-badge {
    color: var(--warning);
    background: var(--warning-bg);
}

.jid-input {
    text-transform: uppercase;
    letter-spacing: 0.3em;
    text-align: center;
    font-weight: 600;
}

.jid-prompt {
    text-align: center;
    font-weight: 600;
    margin: 0;
}

.jid-boxes {
    display: flex;
    justify-content: center;
    gap: 0.5rem;
    margin: 1.5rem 0 0.75rem;
}

/* Square, not the taller rectangle this used to be (on request - "store
   firkantede felter") - width stays the same 58px this was already sized
   to (the largest that still fits all 6 boxes plus gaps inside
   .admin-card.narrow's own 392px content width - 440px max-width minus
   its 2x1.5rem padding - at that card's full size), height now matches
   it instead of 76px. The @media rule below scales back down on a narrow
   phone, where .admin-card itself shrinks below 440px along with the
   viewport - without it, the same fixed size would overflow there. */
input.jid-box {
    width: 58px;
    height: 58px;
    padding: 0;
    font-size: 1.8rem;
    text-align: center;
    text-transform: uppercase;
}

@media (max-width: 400px) {
    .jid-boxes {
        gap: 0.35rem;
    }

    input.jid-box {
        width: 42px;
        height: 42px;
        font-size: 1.4rem;
    }
}

/* The 2026 format's 9 boxes (see jid-code-formats.js) don't fit at the
   6-box sizing above - mountJidInput() adds this class whenever its
   `positions` has more than 6 entries. Sized the same way the default
   tier above was (the largest that still fits inside .admin-card.narrow's
   content width, with the @media rule below scaling down again on a
   narrow phone) - also square, same reasoning as above. */
.jid-boxes.jid-boxes-many {
    gap: 0.3rem;
}

.jid-boxes-many input.jid-box {
    width: 38px;
    height: 38px;
    font-size: 1.3rem;
}

@media (max-width: 400px) {
    .jid-boxes-many {
        gap: 0.2rem;
    }

    .jid-boxes-many input.jid-box {
        width: 27px;
        height: 27px;
        font-size: 0.95rem;
    }
}

/* Every box is colored by its own fixed type, not just by what's been
   typed into it - an empty box already hints at what belongs there (on
   request: "tal skal være blålige og bogstaver skal være rødlige - både
   de tomme felter og når man har tastet dem"). Reuses the existing
   --link (blue)/--danger (red) tokens rather than introducing new ones -
   both already have correct light/dark variants (see :root above), so
   this adapts to the theme for free. Tinted background/border (not a
   solid fill) so the typed character and the .invalid flash below both
   stay clearly legible on top. Same two classes, same meaning, on every
   page this component is used - see jid-input.js. */
input.jid-box-digit {
    background: color-mix(in srgb, var(--link) 14%, var(--bg));
    border-color: color-mix(in srgb, var(--link) 45%, var(--border));
    color: var(--link);
}

input.jid-box-letter {
    background: color-mix(in srgb, var(--danger) 14%, var(--bg));
    border-color: color-mix(in srgb, var(--danger) 45%, var(--border));
    color: var(--danger);
}

/* Two classes beats one on specificity, so this still wins over the
   digit/letter background tints above regardless of source order. */
.jid-box.invalid {
    border-color: var(--danger);
}

.jid-hint {
    text-align: center;
    color: var(--text-muted);
    font-size: 0.8rem;
    margin: 0;
}

/* Same muted look as .jid-hint, but on the pincode page it sits under
   the boxes (and any error message), so it needs its own top margin. */
.pincode-forgot-hint {
    margin-top: 1.5rem;
}

.jid-not-found {
    margin-top: 1.5rem;
    text-align: center;
}

.jid-not-found .error-title {
    color: var(--danger);
    font-weight: 700;
    margin: 0 0 0.35rem;
}

.link-button {
    all: unset;
    margin-top: 0.5rem;
    color: var(--accent);
    font-weight: 600;
    font-size: 0.9rem;
    cursor: pointer;
}

.link-button:hover {
    color: var(--accent-hover);
}

.location-name {
    font-size: 1.2rem;
    font-weight: 700;
    margin: 0 0 0.25rem;
}

.jid-code {
    letter-spacing: 0.3em;
    font-weight: 600;
    color: var(--text-muted);
    margin: 0;
}

.location-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 1rem;
    flex-wrap: wrap;
}

/* The location page's primary CTA - a gradient pill with a soft colored
   shadow, per the location redesign (see CLAUDE.md). */
.add-jid-button {
    /* inline-flex, not the plain inline a <button> defaults to close
       enough to - needed so this class centers its text/height the same
       way whether it's on a <button> (every location page's own "+
       Tilføj JID kode") or an <a> (the admin pages' "+ Ny lokation"/"+
       Ny invitation", styled as this same pill but not always a real
       form action). */
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    white-space: nowrap;
    min-height: 44px;
    padding: 0.75rem 1.4rem;
    font-size: 0.95rem;
    font-weight: 700;
    font-family: inherit;
    color: var(--accent-contrast);
    background: linear-gradient(135deg, var(--accent), var(--accent-hover));
    border: none;
    border-radius: var(--radius-sm);
    text-decoration: none;
    cursor: pointer;
    box-shadow: 0 8px 20px color-mix(in srgb, var(--accent) 35%, transparent);
    transition: transform 0.1s ease, box-shadow 0.15s ease;
}

.add-jid-button:hover {
    transform: translateY(-1px);
    box-shadow: 0 10px 24px color-mix(in srgb, var(--accent) 45%, transparent);
}

/* Same box model as .add-jid-button (min-height/padding/font-size/
   border-radius), a lighter/secondary surface instead of its gradient -
   for a second action sitting right next to a primary .add-jid-button
   (e.g. "Døgnerquiz" next to "Rediger" on a location card) that would
   otherwise mismatch in size (a real, reported bug) the same way
   .profile-badge's own comment above describes for that unrelated
   pairing. */
.add-jid-button.secondary {
    background: var(--accent-bg);
    color: var(--accent);
    box-shadow: none;
}

/* .add-jid-button's own display: inline-flex beats the browser's
   [hidden] rule (see CLAUDE.md's CSS gotchas) - needed for a button
   that's only shown to some viewers, e.g. the location page's
   admin-only "Dashboard". */
.add-jid-button[hidden] {
    display: none;
}

.add-jid-button.secondary:hover {
    transform: translateY(-1px);
    background: color-mix(in srgb, var(--accent-bg) 70%, var(--accent) 30%);
    box-shadow: none;
}

main.wide {
    max-width: 900px;
}

/* The location redesign's own canvas - full-width sections directly on
   the page background instead of the single narrow white card every
   other page uses (see CLAUDE.md). Paired with header.php's
   $hideHeaderChrome, since such a page builds its own topbar/hero
   instead of the shared sticky header + generic <h1>.
   main.app-canvas is the exact same canvas under a page-neutral name -
   used by every /control/* admin page plus the homepage once those
   adopted this same topbar/card design system sitewide (see CLAUDE.md's
   "extend the location redesign to every page" entry), so a page whose
   content isn't a "location" (Indstillinger, Invitationer, ...) doesn't
   have to claim the location-specific class name just to get the same
   canvas. */
main.location-canvas,
main.app-canvas {
    /* The mockup's own wrapper is content-box with max-width:1200px and
       padding:0 32px, so its content area is a full 1200px wide - this
       element is border-box (site-wide * reset), so the max-width has to
       be padded out by the same amount to give children the same 1200px
       of usable width, not 1200px minus the padding. */
    max-width: calc(1200px + 4rem);
    margin: 0 auto;
    padding: 2rem;
    background: none;
    border: none;
    box-shadow: none;
    border-radius: 0;
}

@media (max-width: 640px) {
    main.location-canvas,
    main.app-canvas {
        padding: 1.25rem;
    }
}

/* A single centered card inside .app-canvas, for a page whose content is
   one form or one list rather than a full dashboard grid (every
   /control/* page except the locations list, plus the homepage's own
   JID-entry box) - .loc-card supplies the white surface/border/shadow,
   this just caps its width and centers it under the full-width topbar.
   .narrow is for a plain form (Indstillinger, Åbningstider, the homepage
   JID box) - the same width the old generic <main> card used site-wide
   before this page adopted the topbar/canvas design; the wider default
   suits a name+action list (Mine lokationer, Invitationer, Deltagere). */
.admin-card {
    max-width: 640px;
    margin: 1.5rem auto 0;
}

.admin-card.narrow {
    max-width: 440px;
}

.admin-card h1,
.admin-card h2 {
    margin-top: 0;
}

/* A plain section divider inside a card (e.g. participant-edit's own
   "take over" section, below its rename/pincode form) - the browser
   default <hr> otherwise looks visibly out of place next to this
   design system's soft, token-driven borders everywhere else. */
.admin-card hr {
    border: none;
    border-top: 1px solid var(--border);
    margin: 1.25rem 0;
}

/* A secondary, low-emphasis action chip - the shared look for every
   plain <button type="button"> on an admin page (Vis/Nulstil pincode,
   Gør til webmaster, Kopiér invite-link) that isn't the page's one
   primary call-to-action (.add-jid-button already covers that) and
   isn't a form's own submit button (button[type="submit"] already
   covers that). Kept as an opt-in class, not a bare button[type=button]
   selector - the latter would beat .add-jid-button/.loc-copy-button/
   .menu-toggle/.menu-lang-switch button on specificity (an attribute
   selector counts the same as a class) and silently override their own
   look wherever they're also type="button", which every one of them is. */
.btn-chip {
    display: inline-block;
    padding: 0.4rem 0.85rem;
    font-size: 0.8rem;
    font-weight: 600;
    font-family: inherit;
    color: var(--accent);
    background: var(--accent-bg);
    border: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
    text-decoration: none;
    transition: background 0.15s ease;
}

/* .btn-chip's own display: inline-block beats the browser's own
   [hidden] { display: none } at equal specificity, since this
   author stylesheet loads after the UA one (see CLAUDE.md's CSS
   gotchas) - any .btn-chip that also gets shown/hidden via its
   `hidden` attribute (the simulate start/advance/stop buttons, the
   quiz history's "vis alle") needs this explicit override, or toggling
   `hidden` silently does nothing visually. A real, reported bug: the
   simulate buttons stayed visibly on screen after being hidden. */
.btn-chip[hidden] {
    display: none;
}

.btn-chip:hover {
    background: color-mix(in srgb, var(--accent-bg) 60%, var(--accent) 15%);
}

/* A .btn-chip modifier for the one genuinely destructive action in this
   app (deleting a location) - --danger/--danger-bg instead of the
   default accent coloring, same token pair error messages already use. */
.btn-chip.btn-danger {
    color: var(--danger);
    background: var(--danger-bg);
}

.btn-chip.btn-danger:hover {
    background: color-mix(in srgb, var(--danger-bg) 60%, var(--danger) 15%);
}

.location-map {
    height: 420px;
    border-radius: var(--radius-sm);
    overflow: hidden;
    border: 1px solid var(--border);
    transition: height 0.2s ease;
    /* The sea/background - a fixed, neutral cool light gray rather than
       a theme token (same fixed-regardless-of-theme treatment as the
       medal colors elsewhere in this file - see .loc-rank-badge.gold/
       silver/bronze), and deliberately *not* blue (on request, second
       time around): blue and green (the first purple-free redesign's
       found-country ramp) sat too close together, especially at the
       lightest end, so a not-yet-found white/pale country and the sea
       itself were nearly indistinguishable. Set here as a plain
       background (the pre-Leaflet-init state - avoids a flash of
       leaflet.css's own gray before L.map() attaches its container
       class) *and* again below on `.leaflet-container` once it does. */
    background: #eef1f8;
}

/* 2 classes, not 1 - L.map() stamps its container with leaflet.css's
   own `.leaflet-container { background: #ddd }`, equal specificity to a
   plain `.location-map` rule, and leaflet.css's <link> loads *after*
   this stylesheet's own (see ui/partials/header.php's $extraHead), so on
   source order alone that #ddd always wins regardless of what's written
   here - same equal-specificity trap already documented elsewhere in
   this file (e.g. the tooltip arrow rule below), just on a plain map
   background this time instead of a leaflet-specific class. */
.location-map.leaflet-container {
    background: #eef1f8;
}

/* Bigger map right after adding a code, so a zoomed-in, just-highlighted
   country (see .country-just-added) still has room to breathe instead of
   filling a map sized for the normal zoomed-out view. */
.location-map.expanded {
    height: 560px;
}

/* Leaflet's SVG renderer draws each country as a real <path>, so plain
   CSS (rather than Leaflet's own JS style options) can target them
   directly via the className set in L.geoJSON()'s style() - see
   ui/assets/country-map.js. Purple fills - the site's own primary/
   accent color, the same one "43 JID koder i alt" and the `#1` rank
   badges already use - not green: green is reserved for the trend
   badges (▲ +1, meaning "up since last time"), a completely different
   concept from "found", and having both on the same page as found-
   country colors was confusable. A single thin black border shared by
   every state (on request - each state briefly had its own subtle
   stroke color, "duede ikke", too hard to tell land from land) rather
   than a per-state color, so this one rule carries stroke for all of
   them; fill is the only thing each state still sets on its own. */
.country,
.country-invalid,
.country-heat-1,
.country-heat-2,
.country-heat-3,
.country-heat-4 {
    stroke: #000000;
    stroke-width: 0.6;
    /* Leaflet's own L.Path default options set fillOpacity: 0.2 as an
       inline SVG presentation attribute on every path it draws - since
       our style() callback only ever returns a className (an inline
       fillColor would silently lose to this stylesheet's own `fill`
       anyway - see CLAUDE.md), that default 20% was never overridden,
       so every country's real fill color was only ever 20% visible and
       the background showed through all of them - a real reported bug
       ("alle landene er svagt gennemsigtige"), not a color choice.
       Presentation attributes only lose to a CSS rule that actually
       sets the same property, so fill-opacity needed setting explicitly
       here even though nothing above ever mentioned it. */
    fill-opacity: 1;
}

.country {
    fill: #ffffff;
}

/* A territory with no WOSM region at all (country.region IS NULL - see
   migration 017 and CLAUDE.md) can never legitimately have a JID code -
   clearly darker than both the sea and a not-yet-found WOSM member's
   white, but fully desaturated so it never competes for attention. */
.country-invalid {
    fill: #dcdfe8;
}

/* Hover feedback on the country shape itself - plain CSS :hover, not a
   JS mouseover handler, since a fill change is just presentation and
   SVG paths support :hover natively. On request: the border itself no
   longer changes on hover (weight/color both stay exactly as above) -
   the country's own *fill* turns yellow instead, tiered the same way
   .country-heat-1..4's own purple is (lightest for "not found yet",
   darkest for "most found"), so hovering already hints at a country's
   found-ness even before the tooltip's own text loads. A non-WOSM
   territory gets a darker gray instead of yellow (it was never "found"
   to begin with) - see .country-invalid:hover below. */
.country:hover {
    fill: #fff6c9;
}

.country-heat-1:hover {
    fill: #ffe98a;
}

.country-heat-2:hover {
    fill: #ffd93d;
}

.country-heat-3:hover {
    fill: #f4c000;
}

.country-heat-4:hover {
    fill: #d9a600;
}

.country-invalid:hover {
    fill: #b8bcc7;
}

/* leaflet.css's own .leaflet-interactive sets cursor:pointer for every
   interactive layer, WOSM member or not. Only found countries actually
   link anywhere (see the click handler in ui/assets/country-map.js), so
   only .country-heat-* below keep that pointer - a not-yet-found WOSM
   member and a non-WOSM territory both still get the yellow/gray hover
   feedback above, just with the default arrow cursor. Two classes, not
   one, to reliably out-specificity leaflet.css's own rule regardless of
   <link> order - same equal-specificity trap as the tooltip arrow rule
   below, just on cursor instead of border-color. */
.leaflet-interactive.country,
.leaflet-interactive.country-invalid {
    cursor: default;
}

/* Leaflet's own default tooltip is a plain white box with a CSS-triangle
   arrow baked into its stylesheet - restyled to match this site's own
   dark chip look instead of looking like an unstyled browser default.
   Bound with direction:'top' explicitly (see ui/assets/country-map.js),
   so only that one arrow variant needs restyling. */
.leaflet-tooltip.country-tooltip {
    background: #14172c;
    color: #ffffff;
    border: none;
    border-radius: 10px;
    padding: 8px 11px;
    box-shadow: var(--shadow);
}

.country-tooltip-name {
    font-size: 13px;
    font-weight: 700;
}

.country-tooltip-sub {
    font-size: 12px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.7);
}

/* Extra specificity (3 classes vs leaflet.css's own 2) is needed here,
   not just source order - leaflet.css's <link> is injected into <head>
   *after* this stylesheet's own (see ui/partials/header.php's
   $extraHead), so on load order alone leaflet.css's own
   .leaflet-tooltip-top:before would always win regardless of what's
   written here. Same equal-specificity trap already documented
   elsewhere in this file, just from an unusually-ordered <link> instead
   of a same-specificity same-file rule. */
.leaflet-tooltip-top.country-tooltip::before {
    border-top-color: #14172c;
}

/* Every found country, on every map page - 4 discrete fill tiers by
   that country's own absolute code count (see tierClass() in
   ui/assets/country-map.js). There used to be a separate flat "Lande"
   mode next to a tiered "Varme" mode (a Lande/Varme toggle) - once
   color always carries this nuance, a toggle to turn the nuance off had
   nothing left to do, so it's gone; this is just how a found country
   looks now. Purple, not the ColorBrewer BuGn green this project tried
   in between - green and blue sat too close together at the pale end
   (see .location-map above), and purple ties the map back to the
   site's own primary/accent color instead of introducing a third color
   system alongside it and the green trend badges. Stroke comes from the
   shared rule above now (plain black, like every other state) - it used
   to be white here specifically, paired with bringToFront() in
   ui/assets/country-map.js, so a found country's own border would draw
   over an unfound neighbor's instead of the other way around; a shared
   black border shows up against any fill regardless of z-order, so that
   pairing is no longer load-bearing (bringToFront() is still called,
   harmlessly - removing it bought nothing once the border stopped being
   the thing it protected). Tiers, not a continuously JS-computed color:
   a Leaflet style() callback only ever sets SVG presentation
   *attributes*, which this stylesheet's own `fill` always wins against
   regardless of source order (confirmed against the vendored
   leaflet.js's own _updateStyle) - so an inline fillColor from JS would
   have silently had no visible effect at all. Tiers keep the "className
   only, no inline JS colors" rule from CLAUDE.md intact and sidestep
   that trap entirely. */
.country-heat-1 { fill: #c7c5f7; }
.country-heat-2 { fill: #9a97f4; }
.country-heat-3 { fill: #7d79f0; }
.country-heat-4 { fill: #4b47e0; }

/* The country a code was *just* added from - a distinct color from every
   other found-country state, so it stands out as "this one, right now"
   rather than blending in. */
.country-just-added {
    fill: var(--warning);
    fill-opacity: 0.85;
    stroke: var(--warning);
    stroke-width: 1.5;
}

/* The 3-item legend row below every map on the location/country/
   continent/JID-code/participant pages - static markup (data-i18n, same
   as the rest of each page), not JS-inserted, so it needs no extra
   wiring for the existing global applyTranslations()/langchange cycle
   to reach it. */
.map-legend {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    margin-top: 0.6rem;
    font-size: 12px;
    color: var(--text-muted);
    flex-wrap: wrap;
}

.map-legend-item {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

.map-legend-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex-shrink: 0;
    border: 1px solid #cfd3de;
}

.map-legend-dot.wosm {
    background: #ffffff;
}

.map-legend-dot.invalid {
    background: #dcdfe8;
}

/* The found-country legend swatch is one small bar showing all 4 tiers
   at once (hard color stops, not a smooth blend, so it reads as 4
   discrete steps - the same tiers .country-heat-1..4 actually use)
   rather than 2 separate dots for "low"/"high" - a truer picture of
   what the map itself does. */
.map-legend-swatch {
    width: 28px;
    height: 10px;
    border-radius: 3px;
    flex-shrink: 0;
    background: linear-gradient(to right,
        #c7c5f7 0% 25%, #9a97f4 25% 50%, #7d79f0 50% 75%, #4b47e0 75% 100%);
}

.stats-empty {
    color: var(--text-muted);
    font-size: 0.85rem;
    margin: 0;
}

.badge {
    display: inline-block;
    padding: 0.05rem 0.45rem;
    font-size: 0.7rem;
    font-weight: 600;
    border-radius: 999px;
    vertical-align: middle;
    text-decoration: none;
}

.admin-badge {
    color: var(--accent);
    background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.badge-first-code {
    color: var(--success);
    background: var(--success-bg);
}

.badge-first-country {
    color: var(--warning);
    background: var(--warning-bg);
}

/* Stillingen's own streak badge (see LocationService::stats()'s
   currentStreakHours/streakExtendable and CLAUDE.md) - only shown for a
   real streak (2+ hours), colored by whether it's still *extendable*
   right now, not by its length: --accent-orange for "still alive, keep
   it going", a plain neutral gray (same tone as .loc-rank-badge.plain)
   for "this streak has already gone cold" - a badge a viewer could
   otherwise easily mistake for still-live if it kept the warm color
   forever. */
.badge-streak-live {
    color: var(--accent-orange);
    background: var(--accent-orange-bg);
}

.badge-streak-cold {
    color: var(--text-muted);
    background: color-mix(in srgb, var(--text-muted) 12%, transparent);
}

@media (max-width: 520px) {
    main {
        margin: 1.5rem 1rem;
        padding: 1.5rem;
    }
}

/* ============================================================
   Location page redesign - see CLAUDE.md's "location redesign" entry
   for the full reasoning and what's still using the older shared
   .stats-table/.stats-panel-style components (the other five location/
   sub-pages, untouched by this pass).
   ============================================================ */

/* Topbar + hero merged into one row (see CLAUDE.md's location-redesign
   entry) - the location's name and JID-code badge sit inline with the
   lang-switch/menu instead of a separate centered hero block below. */
.loc-topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 2rem;
}

.loc-topbar-brand {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
    min-width: 0;
}

.loc-topbar-name {
    font-size: 3.75rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1;
    margin: 0;
}

/* The JID-code page's own heading appends " · <code>" after the country
   name - at the heading's own 60px size, a long country name plus a
   full-size code reliably wrapped onto two lines (a real, reported
   layout bug). Scaled down and normal-weight, same idea as a currency
   symbol or unit sitting smaller next to a large number. */
.loc-topbar-name-suffix {
    font-size: 1.75rem;
    font-weight: 700;
    letter-spacing: 0;
}

/* A location sub-page's big topbar heading is the entity the page is
   actually about (a country, continent, participant name, or single
   code), not the location's own name - that reads as the more useful
   "what am I looking at" title for a page several clicks deep. The
   location's own code badge (same badge as the main location page -
   copy button and all - plus a link back) sits right next to that
   heading in .loc-topbar-brand, not on its own separate line - an
   earlier pass tried a smaller line below labelled with the location's
   own name too, corrected on request: the badge belongs beside the
   heading, and the location name label wasn't wanted at all. See
   CLAUDE.md. */
.loc-topbar-actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
}

/* Matches the 44px height of its topbar neighbors (.add-jid-button,
   .profile-badge) without matching their width - a plain, narrow icon
   button (no border/fill/shadow of its own, just the shared :hover
   background) reads as an actual hamburger-menu control, the way one
   looks on most other sites, rather than another bordered pill competing
   with the real buttons next to it for visual weight. */
.loc-topbar .menu-toggle {
    width: 36px;
    height: 44px;
    padding: 12px 8px;
}

/* Deliberately always dark, regardless of the site's own light/dark
   theme - a fixed "this is your code, memorize it" chip, not a themed
   surface. --ink/--ink-contrast (see :root) are the one pair of tokens
   that stay identical in both themes for exactly this reason. */
/* A real height, not padding-driven - padding sits *around* a flex
   row's tallest child, not absorbed by it, so this still grew past the
   copy button's own 44px (a real, reported mismatch against
   .add-jid-button/.profile-badge, both also a real 44px - see their
   own comments) even after cutting the padding down, as long as any
   vertical padding remained at all. box-sizing: border-box (site-wide
   `*` reset) makes this height include that padding, so the JID text/
   copy button just center inside it via align-items, same as before. */
.loc-code-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    height: 44px;
    padding: 0 0.85rem;
    background: var(--ink);
    color: var(--ink-contrast);
    border-radius: var(--radius-sm);
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 1.1rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    transition: background 0.15s ease;
}

/* The title tooltip (see data-i18n-title above) sits on this element
   itself, so it already fires anywhere in the box, including over the
   copy button - but with no hover style at all, hovering the code text
   itself (as opposed to the copy button, which has its own hover
   background below) gave no visual sign anything was there to notice.
   A subtle whole-box background shift fixes that for every use of this
   badge, linked or not. */
.loc-code-badge:hover {
    background: color-mix(in srgb, var(--ink) 85%, white 15%);
}

/* Used on every location sub-page's badge (country/continent/JID-code/
   participant - see CLAUDE.md) - unlike the
   main location page's badge (just a code + copy button, nothing to
   link to), a sub-page's badge also needs a way back. Copy needs its
   own real <button>, so the link can't be the *whole* badge (nesting
   one interactive element inside another is invalid) - instead this
   class goes on a small <a> that's a sibling of the copy button inside
   the badge, wrapping only the code text itself. */
.loc-code-badge-link {
    text-decoration: none;
    /* Explicit, not `inherit` - `inherit` would resolve against this
       link's real parent (.loc-code-badge, which does set the color we
       want), so this is belt-and-suspenders rather than the fix itself;
       kept explicit so this class never silently depends on exactly
       which element it's nested inside. */
    color: var(--ink-contrast);
    cursor: pointer;
}

.loc-code-badge-link:hover {
    color: var(--accent-teal);
}

/* The main location page's own badge (ui/location/index.php) has
   nowhere to link to (you're already there) - clicking it reloads the
   page instead (on request). A real <button> needs its own explicit
   reset - unlike an inline <a>/<span>, a <button> doesn't inherit
   font/background/border from its parent by default. */
.loc-code-badge-refresh {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    color: inherit;
    font: inherit;
    letter-spacing: inherit;
    cursor: pointer;
}

/* A custom tooltip, not the native `title` attribute - `title` only
   shows after the browser's own built-in hover delay, which isn't
   controllable via CSS/JS and read as "broken" when an immediate
   response was expected. `data-tooltip` is filled in by i18n.js's
   `[data-i18n-tooltip]` handling (see CLAUDE.md); this shows it on
   `:hover`/`:focus-visible` with no transition, so it appears the
   instant the pointer lands rather than fading or waiting. Below the
   element, not above - these tooltips currently only ever sit in a
   page's topbar, near the very top of the viewport, where there isn't
   reliably room to render upward. */
[data-tooltip] {
    position: relative;
}

[data-tooltip]::after {
    content: attr(data-tooltip);
    position: absolute;
    top: calc(100% + 8px);
    left: 50%;
    transform: translateX(-50%);
    background: var(--ink);
    color: var(--ink-contrast);
    padding: 0.5rem 0.75rem;
    border-radius: var(--radius-sm);
    font-size: 0.8rem;
    font-weight: 600;
    line-height: 1.35;
    white-space: normal;
    width: max-content;
    max-width: 260px;
    text-align: center;
    box-shadow: var(--shadow);
    opacity: 0;
    pointer-events: none;
    z-index: 20;
}

[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::after {
    opacity: 1;
}

.loc-copy-button {
    all: unset;
    box-sizing: border-box;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Holds the full word "Kopiér"/"Copy", not an icon - a fixed 44x44
       square (an earlier attempt at this button's hit-target fix, back
       when it was still icon-only) can't fit that text. min-height alone
       already satisfies the 44px minimum (see CLAUDE.md's
       location-redesign entry); width follows the text naturally. */
    min-height: 44px;
    padding: 0.4rem 0.85rem;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.15);
    color: inherit;
    letter-spacing: normal;
    font-size: 0.85rem;
    font-weight: 700;
    transition: background 0.15s ease;
}

.loc-copy-button:hover {
    background: rgba(255, 255, 255, 0.28);
}

.loc-stats-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1.25rem;
    margin-bottom: 1.5rem;
}

/* Wraps a page's stat-card row in the same 1fr/340px split as
   .loc-main-grid below it, so whichever stat card is "the odd one out"
   (unique-countries on the public location page, points on the
   participant page) actually lines up with the sidebar card underneath
   it (Stillingen, Næste bonus) instead of belonging to its own,
   unrelated 3-or-4-equal-column grid - on request, after a screenshot
   showed the two grids' column widths visibly not matching. Only ever
   wraps a nested .loc-stats-grid (whose own column count is overridden
   below, scoped to this wrapper so the other /location/* pages' plain
   3-equal-column .loc-stats-grid is untouched) plus one lone
   .loc-stat-card taking the fixed second column directly. */
.loc-stats-row {
    display: grid;
    grid-template-columns: 1fr 340px;
    gap: 18px;
    margin-bottom: 1.5rem;
}

.loc-stats-row .loc-stats-grid {
    grid-template-columns: repeat(2, 1fr);
    margin-bottom: 0;
}

.loc-stats-row .loc-stats-grid.with-avatar {
    grid-template-columns: auto repeat(2, 1fr);
}

.loc-stat-card {
    position: relative;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    padding: 1.5rem;
}

.loc-stat-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.6rem;
}

.loc-stat-value {
    /* Without this reset, the browser's default <p> margin (1em) computes
       against this element's own huge font-size, adding ~50px of unwanted
       space below the number - caught visually, not obvious from the CSS
       alone. See CLAUDE.md's location-redesign entry. */
    margin: 0;
    font-size: 3.1rem;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

/* A stat card whose value is a name or a formatted date/time (the
   JID-code detail page's "first found by"/"first found at" cards - see
   CLAUDE.md) rather than a number to animate-count - the default
   3.1rem number size is far too large for a sentence-length string and
   would overflow the card. */
.loc-stat-value-text {
    font-size: 1.3rem;
    line-height: 1.3;
    overflow-wrap: break-word;
}

/* A stat card whose content is a name or a formatted date/time (see
   .loc-stat-value-text) rather than an animated count - label first,
   then the value, the reverse of a numeric card's own order. A numeric
   card's height comes from its value+label+hint stack; a short name or
   date alone left a visibly empty gap at the bottom of an equal-height
   grid row without something to fill that same vertical rhythm - this
   centers the (label, value) pair in the card instead, so it reads as
   deliberately laid out rather than a shrunk-down number card with
   space left over. */
.loc-stat-card-text {
    display: flex;
    flex-direction: column;
    justify-content: center;
    height: 100%;
}

.loc-stat-card-text .loc-stat-label {
    margin: 0 0 0.5rem;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

/* The stat cards' "new in the last hour" trend indicator - a fixed-size
   pill (matching the approved design reference exactly - the pill itself
   never changes size), green (the --success tokens, already used
   elsewhere for a first-finder badge) for the two frequent, routine
   counts; a neutral gray (.neutral) for the countries card, since a new
   *country* is meant to read as a rarer, more special event rather than
   blending into the same "routine activity" green. The *triangle*
   inside is what grows with a bigger count, not the pill around it - its
   font-size is set in JS - linearly, from a tiny 0.6rem at the smallest
   real increase (+1) up to a 1.6rem cap reached exactly at +10 (see
   setTrendPill() in ui/location/index.php and its copies), not a sqrt
   curve that barely grew across realistic counts and looked like every
   trend arrow was the same size regardless of how big the jump was -
   caught from a live screenshot. An even earlier pass had this
   backwards entirely, dropping the pill and scaling the triangle all
   the way up to the big number's own huge size instead. */
/* Equal-specificity tiebreak: without this, some browsers (Firefox) let
   the display:inline-flex below win over the browser's own [hidden]
   rule, since both have the same specificity and this stylesheet loads
   later - same fix already established for .menu-dropdown[hidden]. This
   one is real, not theoretical: the trend arrow kept showing even at
   count 0, when setTrendPill() had already set pill.hidden = true. */
.loc-stat-trend[hidden] {
    display: none;
}

.loc-stat-trend {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.45rem 0.9rem;
    border-radius: 999px;
    background: var(--success-bg);
    color: var(--success);
    font-variant-numeric: tabular-nums;
}

.loc-stat-trend.neutral {
    background: color-mix(in srgb, var(--text-muted) 15%, transparent);
    color: var(--text-muted);
}

.loc-stat-trend-arrow {
    display: inline-block;
    line-height: 1;
}

.loc-stat-trend-value {
    font-size: 1.1rem;
    font-weight: 800;
    white-space: nowrap;
}

.loc-stat-card.accent-blue .loc-stat-value { color: var(--accent); }
.loc-stat-card.accent-teal .loc-stat-value { color: var(--accent-teal); }
.loc-stat-card.accent-orange .loc-stat-value { color: var(--accent-orange); }

.loc-stat-label {
    margin: 0 0 0.2rem;
    font-weight: 700;
}

.loc-stat-hint {
    margin: 0;
    color: var(--text-muted);
    font-size: 0.83rem;
}

/* Døgnerquiz ("24h quiz") card - sits directly above .loc-stats-row on
   both the location and participant pages (see CLAUDE.md). Reuses
   .loc-card's own surface/border/shadow rather than a one-off look;
   hidden entirely (via the [hidden] attribute, set from JS) when there's
   nothing to show, same convention as #just-added-banner. */
.loc-quiz-card {
    margin-bottom: 1.5rem;
}

.loc-quiz-card-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    margin-bottom: 1rem;
}

.loc-quiz-card-head-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem 0.75rem;
}

/* "What real clock time this simulated run is currently pretending it
   is" - webmaster-only, shown next to the inline advance button (on
   request - see CLAUDE.md's effectiveNow()). */
.loc-quiz-simulated-clock {
    color: var(--text-muted);
    font-size: 0.78rem;
    white-space: nowrap;
}

/* The small, permanent sidebar entry point under Stillingen - see
   CLAUDE.md and ui/location/index.php's updateQuizSidebar(). */
.loc-quiz-sidebar-status {
    margin: 0;
    color: var(--text-muted);
    font-size: 0.9rem;
}

/* On its own row, below the head/status - this 340px-wide column has
   no room to also fit these into the head row alongside the title/
   "Se alle spørgsmål" link without spilling outside the card itself
   (a real, reported bug: the clock text + button rendered visibly
   outside the white card). flex-wrap as a second line of defense, in
   case the clock's own formatted date/time text still doesn't fit
   next to the button at this width. */
.loc-quiz-sidebar-simulate-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.5rem;
}

/* Same [hidden]-losing-to-author-display bug as .btn-chip (see CLAUDE.md's
   CSS gotchas) - display: flex above beats the UA [hidden] rule at equal
   specificity, so this row stayed visible (with its "Næste spørgsmål"
   button clickable) even before a quiz had started at all. */
.loc-quiz-sidebar-simulate-row[hidden] {
    display: none;
}

.loc-quiz-card-title {
    margin: 0;
    font-size: 1.15rem;
    font-weight: 800;
}

.loc-quiz-card-see-all {
    color: var(--accent);
    font-weight: 700;
    font-size: 0.85rem;
    text-decoration: none;
    white-space: nowrap;
}

.loc-quiz-card-see-all:hover {
    text-decoration: underline;
}

/* The current question and the just-closed reveal side by side, same
   1fr/340px split as .loc-stats-row below (so the two columns roughly
   line up with the stat cards underneath) rather than stacked - on
   request, after the reveal's own bright colors made it hard not to
   overlook the new question sitting right underneath it ("vi må kunne
   lave dette mere neutralt ... vis sidste spørgsmål i højresiden ...
   imens det næste spørgsmål vises i venstre side"). .solo (added in JS
   whenever only one of the two columns actually has anything to show)
   drops back to a single column instead of leaving an empty 340px gap. */
.loc-quiz-body {
    display: grid;
    grid-template-columns: 1fr 340px;
    gap: 18px;
    align-items: start;
}

.loc-quiz-body.solo {
    grid-template-columns: 1fr;
}

/* Each column its own visibly framed white card - on request ("den
   højre og venstre side skal have hver sin hvide ramme"). Sits nested
   inside the outer .loc-quiz-card's own white card, but border+shadow
   still reads as a distinct edge even on the same white background.
   The reveal's own tinted panel (.loc-quiz-reveal.correct/incorrect/
   not-answered) gets the same border+shadow treatment directly, since
   it already supplies its own background/radius/padding - see below. */
.loc-quiz-col-current {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    padding: 1.1rem 1.25rem;
}

/* Smaller text on this side specifically - on request ("den venstre
   side skal have mindre tekst"), now that it sits right next to the
   more compact reveal column instead of spanning the full card width
   alone. */
.loc-quiz-col-current .loc-quiz-question {
    font-size: 0.95rem;
}

.loc-quiz-col-current .loc-quiz-option {
    padding: 0.6rem 0.8rem;
    font-size: 0.82rem;
    min-height: 38px;
}

.quiz-schedule-status {
    margin: 0;
    font-weight: 700;
}

/* "Next question at HH:MM" right under the open question - see
   loadQuizCard(). */
.loc-quiz-next {
    margin: -0.6rem 0 0.9rem;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-muted);
}

.loc-quiz-question {
    margin: 0 0 1rem;
    font-size: 1.05rem;
    font-weight: 700;
    line-height: 1.4;
}

.loc-quiz-options {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 0.75rem;
}

/* The thick correct/wrong outline frames (see .correct-frame/.wrong-frame
   below) protrude past each tile's own edge - the default gap above is
   fine while just picking an answer (no frames yet), but two framed
   tiles sitting right next to each other would otherwise have their
   frames overlap/touch (a real, reported bug) - only widen the gap once
   a frame is actually present. */
.loc-quiz-options:has(.correct-frame, .wrong-frame, .selected-frame) {
    gap: 1.25rem;
}

.loc-quiz-option {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    width: 100%;
    min-height: 44px;
    padding: 0.75rem 1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: inherit;
    font-family: inherit;
    font-size: 0.9rem;
    text-align: left;
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease, filter 0.15s ease;
}

.loc-quiz-option:disabled {
    cursor: default;
    opacity: 0.6;
}

.loc-quiz-option-letter {
    flex-shrink: 0;
    font-weight: 800;
    color: var(--accent);
}

/* Kahoot-inspired tint per answer letter, live "pick an answer"
   quiz-card buttons only now (the reveal's own re-shown tiles and the
   history page's option list both stay fully neutral - see
   buildQuizOptionTile()). Started as a full solid Kahoot fill
   (white-on-color) but read as too loud/colorful sitting right next to
   the now-neutral reveal column - toned down to a light tint over
   --surface plus a small solid letter badge, on request ("den venstre
   side skal have ... færre farver, den skal være lidt mere
   gråtonet") - color-mix against --surface/--border rather than a
   flat lighter hex so this still adapts correctly under dark mode. */
.kahoot-a, .kahoot-b, .kahoot-c, .kahoot-d {
    color: var(--text);
}

.kahoot-a { background: color-mix(in srgb, #e21b3c 16%, var(--surface)); border-color: color-mix(in srgb, #e21b3c 35%, var(--border)); }
.kahoot-b { background: color-mix(in srgb, #1368ce 16%, var(--surface)); border-color: color-mix(in srgb, #1368ce 35%, var(--border)); }
.kahoot-c { background: color-mix(in srgb, #d89e00 16%, var(--surface)); border-color: color-mix(in srgb, #d89e00 35%, var(--border)); }
.kahoot-d { background: color-mix(in srgb, #26890c 16%, var(--surface)); border-color: color-mix(in srgb, #26890c 35%, var(--border)); }

button.kahoot-a:not(:disabled):hover { background: color-mix(in srgb, #e21b3c 26%, var(--surface)); }
button.kahoot-b:not(:disabled):hover { background: color-mix(in srgb, #1368ce 26%, var(--surface)); }
button.kahoot-c:not(:disabled):hover { background: color-mix(in srgb, #d89e00 26%, var(--surface)); }
button.kahoot-d:not(:disabled):hover { background: color-mix(in srgb, #26890c 26%, var(--surface)); }

/* The letter itself stays the one small, saturated dab of color per
   Kahoot tile - a solid badge, not just colored text, so the
   letter/answer is still identifiable at a glance even this muted.
   Scoped to .kahoot-* specifically - the reveal's own plain letters
   (no .kahoot-* class any more) keep the base .loc-quiz-option-letter
   styling above (colored text, no badge shape) untouched. */
.kahoot-a .loc-quiz-option-letter,
.kahoot-b .loc-quiz-option-letter,
.kahoot-c .loc-quiz-option-letter,
.kahoot-d .loc-quiz-option-letter {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.5em;
    height: 1.5em;
    padding: 0 0.3em;
    border-radius: 999px;
    color: #fff;
}

.kahoot-a .loc-quiz-option-letter { background: #e21b3c; }
.kahoot-b .loc-quiz-option-letter { background: #1368ce; }
.kahoot-c .loc-quiz-option-letter { background: #d89e00; }
.kahoot-d .loc-quiz-option-letter { background: #26890c; }

/* The reveal's own thick frame - on request: "en tyk grøn frame hvad
   det rigtige svar var og en tyk rød frame om det jeg svarede hvis jeg
   svarede forkert". An outline (not a border) so the frame doesn't
   shift the tile's own layout/size. Shared by the live reveal's
   re-shown tiles and the history page's option list. */
.loc-quiz-option.correct-frame {
    outline: 4px solid var(--success);
    outline-offset: 3px;
}

.loc-quiz-option.wrong-frame {
    outline: 4px solid var(--danger);
    outline-offset: 3px;
}

/* Right after answering, before the question has closed - a neutral
   "this is what you picked" frame, distinct from the correct/wrong
   colors above since correctness isn't known yet (on request - "jeg
   skal bare ikke kunne ændre mit svar"). */
.loc-quiz-option.selected-frame {
    outline: 4px solid var(--accent);
    outline-offset: 3px;
}

/* The other 3 tiles dim once disabled (see .loc-quiz-option:disabled)
   after answering - your own pick stays at full brightness despite also
   being disabled, so the frame doesn't end up highlighting a washed-out
   tile. */
.loc-quiz-option.selected-frame:disabled {
    opacity: 1;
}

/* The "you answered, here's what's next" state - a plain accent-tinted
   row, no correct/incorrect color yet since that's still unknown. */
.loc-quiz-answered {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-top: 1rem;
    padding: 1rem 1.25rem;
    background: var(--accent-bg);
    border-radius: var(--radius-sm);
}

.loc-quiz-answered-potential {
    color: var(--text-muted);
    font-size: 0.82rem;
    white-space: nowrap;
}

/* The dismissible reveal card, once a question has closed -
   .correct/.incorrect pick --success/--danger the same way
   .badge-first-code/.btn-chip.btn-danger already do elsewhere. Shows the
   question itself plus all 4 answer tiles again (neutral, no per-letter
   color - see .loc-quiz-body below for why - with a thick frame on the
   correct one and on your own pick if it was wrong, see
   .correct-frame/.wrong-frame above), not just a plain result sentence
   - on request ("i den grønne kasse skal den vise hvad spørgsmålet var
   og hvad jeg svarede"). margin-bottom keeps it from sitting flush
   against whatever follows (a real, reported gap). border+box-shadow
   give this column its own visible white-card-style frame too, same
   as .loc-quiz-col-current on the left (on request - see there). */
.loc-quiz-reveal {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
    padding: 1rem 1.25rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    margin-bottom: 1rem;
}

/* The panel itself stays plain grayscale regardless of correct/
   incorrect/not-answered - only the .correct-frame/.wrong-frame
   outlines on the tiles themselves (plus the spelled-out result text)
   carry the green/red signal now, on request ("rammen med resultatet
   er da stadig både grøn og blå ... den skal være i gråtoner men med
   grøn ramme om det rigtige svar og rød ramme omkring et forkert svar
   ... der skal ikke være grøn baggrund") - a green/red-tinted panel
   background plus the accent-colored letter badges (see .kahoot-*
   above) read as two competing color signals at once. */
.loc-quiz-reveal.correct,
.loc-quiz-reveal.incorrect,
.loc-quiz-reveal.not-answered {
    background: var(--surface);
    color: var(--text);
}

.loc-quiz-reveal-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
}

.loc-quiz-reveal-question {
    margin: 0;
    font-weight: 700;
}

/* The reveal's own re-shown tiles are read-only - no pointer cursor,
   unlike the live "pick an answer" buttons that share the same
   .loc-quiz-option/.loc-quiz-options classes. Smaller text/padding
   than even .loc-quiz-col-current's own reduced size - this column is
   the fixed 340px one, so a 2-per-row option grid leaves each tile
   only ~150px wide, not enough room for a longer option's full text at
   the base size without wrapping onto 3+ lines (on request: "teksten
   skal være mindre så det kan være der uden at have ofte
   ordbrydning"). The letter itself drops to a neutral gray too, not
   --accent's own blue/purple - the frame outlines below are the only
   color left in this panel (on request - see .loc-quiz-reveal.correct
   above). */
.loc-quiz-reveal .loc-quiz-option {
    cursor: default;
    padding: 0.5rem 0.65rem;
    font-size: 0.74rem;
    min-height: auto;
    line-height: 1.3;
}

.loc-quiz-reveal .loc-quiz-option-letter {
    color: var(--text-muted);
}

.loc-quiz-reveal-content {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.75rem;
}

.loc-quiz-reveal-text {
    margin: 0;
    font-weight: 700;
}

/* A real, reported bug: a bare "×" with no background, only 70% opaque
   until hovered, read as "not obviously a button" ("krydset skal være
   tydeligere så man ikke er i tvivl om at man kan trykke på det") -
   worse still on touch, which has no hover state to reveal it at full
   strength at all. A visible round chip, always at full opacity,
   fixes both - same pill shape as .btn-chip, just circular. Shared by
   the reveal's own dismiss button and buildQuizAnsweredBox()'s. */
.loc-quiz-reveal-dismiss {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--border);
    border: none;
    border-radius: 999px;
    color: var(--text-muted);
    font-size: 1.05rem;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    opacity: 1;
    transition: background 0.15s ease, color 0.15s ease;
}

.loc-quiz-reveal-dismiss:hover {
    background: var(--danger-bg);
    color: var(--danger);
}

/* The dedicated /location/24h-quiz page's question-by-question history -
   see CLAUDE.md. Each question renders as its own small block rather
   than a compact row, since it has to hold a question, 4 options, and a
   tally, unlike every other list on this site. */
.loc-quiz-history-item {
    padding: 1.25rem 0;
    border-bottom: 1px solid var(--border);
}

.loc-quiz-history-item:last-child {
    border-bottom: none;
}

.loc-quiz-history-heading {
    margin: 0 0 0.35rem;
    color: var(--text-muted);
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.loc-quiz-history-question {
    margin: 0 0 0.75rem;
    font-weight: 700;
}

.loc-quiz-history-options {
    list-style: none;
    margin: 0 0 0.5rem;
    padding: 0;
    display: grid;
    gap: 0.4rem;
}

.loc-quiz-history-option {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: 0.9rem;
}

/* Deliberately plain, not the Kahoot per-letter colors the live quiz
   card and its reveal use - 24 questions × 4 colored tiles each read as
   "too much" on this one page that lists everything at once (on
   request: "det bliver lidt for meget med farverne på
   svarmulighederne i den fulde døgnerquiz oversigt"). Correct/chosen-
   wrong still get their own distinct tint, just the original
   background/text-color swap instead of an outline frame. */
.loc-quiz-history-option.correct {
    border-color: var(--success);
    background: var(--success-bg);
    color: var(--success);
}

.loc-quiz-history-option.chosen:not(.correct) {
    border-color: var(--danger);
    background: var(--danger-bg);
    color: var(--danger);
}

/* The question that's still open (not yet closed) - correctness isn't
   knowable yet, so this must never look like .chosen's own red/wrong
   treatment above (a real, reported bug: it did, which read as "you
   answered wrong" before the question had even closed). Neutral
   accent blue instead, same meaning as the live quiz card's own
   .selected-frame on its still-open question. */
.loc-quiz-history-option.chosen-pending {
    border-color: var(--accent);
    background: var(--accent-bg);
    color: var(--accent);
}

.loc-quiz-history-option-letter {
    font-weight: 800;
}

.loc-quiz-history-option-tally {
    margin-left: auto;
    color: var(--text-muted);
    font-size: 0.82rem;
    white-space: nowrap;
}

.loc-quiz-history-your-answer {
    margin: 0;
    font-weight: 700;
}

.loc-quiz-history-tally-hidden {
    margin: 0.35rem 0 0;
    color: var(--text-muted);
    font-size: 0.82rem;
}

/* Who answered what, once revealed (same gate as the tally right above
   it - see CLAUDE.md). A wrapped chip list, not a plain row-per-line
   list, since this can be one name or many. */
.loc-quiz-history-answers {
    list-style: none;
    margin: 0.6rem 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

.loc-quiz-history-answer {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.25rem 0.6rem;
    border-radius: 999px;
    background: var(--bg);
    border: 1px solid var(--border);
    font-size: 0.8rem;
}

.loc-quiz-history-answer.correct {
    border-color: var(--success);
    background: var(--success-bg);
}

.loc-quiz-history-answer-name {
    color: inherit;
    text-decoration: none;
    font-weight: 600;
}

.loc-quiz-history-answer-name:hover {
    text-decoration: underline;
}

.loc-quiz-history-answer-chosen {
    color: var(--text-muted);
    font-weight: 800;
}

.loc-quiz-history-answer.correct .loc-quiz-history-answer-chosen {
    color: var(--success);
}

/* control/location-quiz-edit's own 24-question editor and create/
   default chooser - see CLAUDE.md. */
textarea {
    display: block;
    width: 100%;
    padding: 0.65rem 0.8rem;
    font-size: 1rem;
    font-family: inherit;
    color: var(--text);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    resize: vertical;
}

textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}

.quiz-choice-cards {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
    margin-top: 1rem;
}

.quiz-choice-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
    padding: 1.25rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    text-align: left;
    cursor: pointer;
    font-family: inherit;
}

.quiz-choice-card:hover {
    border-color: var(--accent);
    background: var(--accent-bg);
}

.quiz-choice-card-title {
    font-weight: 700;
}

.quiz-question-fieldset {
    margin: 1.5rem 0 0;
    padding: 1.25rem 0 0;
    border: none;
    border-top: 1px solid var(--border);
}

.quiz-question-fieldset legend {
    padding: 0 0 0.5rem;
    font-weight: 700;
    font-size: 1rem;
}

/* "Move up"/"move down" next to each question's heading - see
   addMoveControls() in ui/control/location-quiz-edit/. Only shown in the
   one-line "change order" view (.reorder-mode, see addReorderToggle()),
   together with the question's own text and its slot's end time. */
.quiz-move-controls,
.quiz-question-summary,
.quiz-question-slot-time,
.quiz-question-number,
.reorder-mode .quiz-question-heading {
    display: none;
}

.reorder-mode .quiz-question-number {
    display: block;
    flex-shrink: 0;
    min-width: 1.5rem;
    text-align: right;
}

.quiz-reorder-bar {
    margin-top: 1.25rem;
}

.quiz-reorder-hint[hidden] {
    display: none;
}

/* min-width: 0 - a <fieldset> defaults to min-width: min-content, so it
   grows to fit its longest question instead of letting the text below
   be cut off with an ellipsis (it ran straight out of the card). */
.reorder-mode .quiz-question-fieldset {
    min-width: 0;
    margin: 0;
    padding: 0.2rem 0;
}

.reorder-mode .quiz-question-fieldset > :not(legend) {
    display: none;
}

.reorder-mode .quiz-question-fieldset legend {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
    padding: 0;
    font-size: 0.9rem;
    white-space: nowrap;
}

.reorder-mode .quiz-move-controls {
    display: inline-flex;
    gap: 0.3rem;
    flex-shrink: 0;
}

/* Wraps onto up to 3 lines, and is only cut off with "…" beyond that
   (on request) - line-clamp, not a single-line text-overflow. */
.reorder-mode .quiz-question-summary {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    flex: 1;
    min-width: 0;
    overflow: hidden;
    white-space: normal;
    font-weight: 500;
    line-height: 1.35;
}

.reorder-mode .quiz-question-slot-time {
    display: block;
    flex-shrink: 0;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.8rem;
    color: var(--text-muted);
}

.quiz-move-button {
    width: 2rem;
    height: 1.75rem;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--accent);
    font: inherit;
    font-size: 0.75rem;
    line-height: 1;
    cursor: pointer;
}

.quiz-move-button:hover:not(:disabled) {
    background: var(--accent-bg);
}

.quiz-move-button:disabled {
    color: var(--text-muted);
    opacity: 0.4;
    cursor: default;
}

/* A short highlight on the question that just moved, so it's easy to
   see where it went. */
.quiz-question-fieldset.just-moved {
    animation: quiz-just-moved 0.9s ease;
}

@keyframes quiz-just-moved {
    from { background: var(--accent-bg); }
    to { background: transparent; }
}

.quiz-options-wrap {
    display: grid;
    gap: 0.6rem;
    margin-top: 0.6rem;
}

.quiz-option-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

.quiz-option-row label {
    margin: 0;
    flex-shrink: 0;
    white-space: nowrap;
}

.quiz-option-row input[type="text"] {
    flex: 1;
}

.quiz-locked-hint {
    margin: 0.5rem 0 0;
    color: var(--text-muted);
    font-size: 0.82rem;
    font-style: italic;
}

.quiz-simulate-status {
    margin: 0.75rem 0;
    font-weight: 600;
}

.quiz-simulate-actions {
    display: flex;
    gap: 0.75rem;
}

/* A fixed 340px sidebar, not a fraction of the remaining width - measured
   directly against the "Min Side" mockup's own source
   (`grid-template-columns: 1fr 340px; gap: 18px`, see CLAUDE.md) rather
   than approximated. Originally participant/index.php-only (a 1.55fr/1fr
   split elsewhere rendered its sidebar ~100px too wide against that
   mockup - caught by comparing rendered pixel widths, not visible from
   the markup alone); applied to every page sharing this grid for the
   same reason every other `/location/*` subpage already shares one
   `.loc-card` system - one width convention, not a different one per
   page. */
.loc-main-grid {
    display: grid;
    grid-template-columns: 1fr 340px;
    gap: 18px;
    align-items: start;
}

.loc-main-left {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

.loc-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    padding: 1.5rem;
}

/* A row-list card ("Mine koder", "Seneste fund") - the mockup's own
   version of this card has NO padding of its own; every row inside
   (.loc-day-header/.loc-code-row) carries its own matching horizontal
   padding instead, so a row's own background (the new-country/first-code
   highlight, a hover state) reaches the card's real edges. .loc-card's
   plain 24px-all-round padding left a visible gap of *unhighlighted*
   card background on both sides of every row - a real, reported bug
   (rows read shorter/inset compared to the mockup), not a stylistic
   choice. overflow: hidden clips a full-width row's own corners to the
   card's already-rounded ones. */
.loc-list-card {
    padding: 0;
    overflow: hidden;
}

.loc-list-card > .loc-feed-head {
    padding: 1.375rem 1.5rem 1rem;
    margin-bottom: 0;
}

.loc-list-card > .stats-empty {
    padding: 0 1.5rem 1.5rem;
}

/* Every other direct child of .loc-list-card that isn't a full-width
   .loc-code-row/.loc-day-header needs this same horizontal padding
   re-applied (the card's own base padding is 0 - see above) - the
   filter toggle and the "vis flere"/"vis alle" pagination links were
   both missing it, a real reported bug: both sat flush against the
   card's left edge, spilling visually past its rounded corner instead
   of sitting inside it like every other bit of chrome on this card. */
.loc-list-card > #feed-filter-toggle {
    margin-left: 1.5rem;
    margin-right: 1.5rem;
}

.loc-list-card > .loc-list-more {
    padding: 0 1.5rem 1.5rem;
}

.loc-map-card {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
}

.loc-card-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}

.loc-card-title {
    font-size: 1.05rem;
    font-weight: 700;
    margin: 0 0 0.2rem;
}

/* Participant page only - the mockup's own card titles run larger
   (20px sidebar cards, 22px "Mine koder") than the site's generic
   16.8px .loc-card-title elsewhere. Scoped to .loc-participant-content
   rather than changed on the shared class itself, so other pages built
   against that class (country/continent/most-found/points) keep their
   already-approved rhythm - #content is reused as a generic wrapper id
   across all of them, so it can't be the scope on its own. See CLAUDE.md. */
.loc-participant-content .loc-card-title {
    font-size: 1.25rem;
    letter-spacing: -0.02em;
}

.loc-participant-content .loc-card-title-lg {
    font-size: 1.375rem;
}

/* Public location page only ("Jeres verdenskort"/"Fundne JID-koder"),
   same reasoning and target size as .loc-participant-content above -
   these two read as noticeably smaller than every other heading on the
   page (the topbar's own 60px name, the stat cards' own big numbers),
   on request. Scoped the same way (a dedicated class on this page's own
   #content, not the shared .loc-card-title itself) so country/
   continent/JID-code pages, sharing that same class, keep their own
   already-approved rhythm. */
.loc-location-content .loc-card-title {
    font-size: 1.25rem;
    letter-spacing: -0.02em;
}

/* The "Dine verdensdele" fraction (e.g. "4 / 6") - a plain muted mono
   figure in the mockup, not the small-caps accent-colored style
   .loc-card-caption's *other* use (a column-header-style label like
   "Unique codes") establishes elsewhere; reusing that class here would
   have borrowed its look without its meaning. */
.loc-continents-fraction {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.8125rem;
    font-weight: 700;
    color: var(--text-muted);
}

.loc-card-subtitle {
    color: var(--text-muted);
    font-size: 0.82rem;
    margin: 0;
}

/* A small "what does this number mean" column-header-style label, e.g.
   "UNIKKE KODER" over the Lande card's counts - text stays normal-case in
   markup/translations, text-transform draws the small-caps look, same
   convention as the old .pill-badge before it was removed. */
.loc-card-caption {
    flex-shrink: 0;
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--accent);
}

/* The Stillingen card's 3-way Koder/Lande/Point sort toggle, and every
   other pill-style toggle on these pages (feed mode/filter). */
/* Equal-specificity tiebreak: without this, some browsers let the
   display:flex below win over the browser's own [hidden] rule, since
   both have the same specificity and this stylesheet loads later - same
   fix already established for .menu-dropdown/.loc-card-footer-link
   [hidden] elsewhere in this file. Real, not theoretical: #feed-filter-toggle
   toggling hidden based on the feed mode is exactly this case. */
.loc-sort-toggle[hidden] {
    display: none;
}

.loc-sort-toggle {
    display: flex;
    padding: 0.2rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 999px;
    margin: 0.6rem 0 1rem;
}

.loc-sort-toggle button {
    all: unset;
    box-sizing: border-box;
    /* Content-sized, not stretched to equal thirds (flex: 1) - fine for
       this toggle's original short single-word labels (Koder/Lande/Point
       on the main location page), but the participant page's own longer
       ones (Først med koden/Nye lande) wrapped mid-word when forced into
       an equal share of a narrow container - a real, reported bug, not
       theoretical. white-space: nowrap so a label never wraps regardless
       of how tight the container gets - the toggle just grows instead. */
    flex: 0 0 auto;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Not the 44px touch-target minimum real buttons elsewhere on this
       site get (.add-jid-button, .loc-copy-button) - a compact filter
       tab, tapped while browsing rather than a single deliberate action,
       matches the mockup's own shorter pill (~32px) instead. Forcing 44px
       here made this toggle nearly twice as tall as the mockup's own -
       a real, reported difference, not a stylistic one. */
    padding: 0.5rem 0.875rem;
    font-size: 0.8125rem;
    font-weight: 700;
    color: var(--text-muted);
    border-radius: 999px;
    cursor: pointer;
    text-align: center;
}

.loc-sort-toggle button.active {
    background: var(--surface);
    color: var(--accent);
    box-shadow: var(--shadow);
}

/* The Stillingen card's own Koder/Lande/Point toggle, on request, goes
   back to equal thirds (~1/3 of the full width each) rather than the
   content-sized tabs above - its three labels are short and never wrap,
   so the problem that .loc-sort-toggle button's flex:0 0 auto exists to
   avoid doesn't apply here. A real class, not an id, and applied on
   *every* Stillingen toggle markup (the main location page's own, and
   ui/assets/location-sidebar.js's shared copy for the country/continent/
   JID-code pages) - the id-scoped version only ever caught the first of
   those, a real reported inconsistency. The participant/seneste-fund
   filter tabs (which do have long labels) never get this class, so they
   keep their content-sized behavior. */
.loc-sort-toggle-thirds button {
    flex: 1;
}

/* The public location page's own "Fundne JID-koder" card has *two*
   controls (a Seneste/Mest fundne sort, plus an Alle/Først med
   koden/Nye lande filter) - both now live in one shared toolbar band
   (design provided directly, on request): filter tabs as underlined
   text (not pill buttons) with each one's own live count, and the sort
   control as a plain text dropdown rather than a two-button pill
   toggle. A light background band the full width of the card, boxed in
   by its own top/bottom border, rather than the plain top-border-only
   strip this replaced - visually separates it from both the title
   above and the list below without needing a caption of its own to
   explain what the row is for. */
.loc-feed-toolbar {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.4rem 1.5rem;
    background: var(--bg);
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
}

.loc-feed-tabs {
    display: flex;
    align-items: center;
    gap: 1.5rem;
    flex-wrap: wrap;
}

/* Same equal-specificity [hidden] tiebreak as .loc-sort-toggle/
   .menu-dropdown elsewhere - real here too: only shown in "Seneste
   fundne" mode (JidService::mostFound() has no per-viewer "new for
   you" concept for the filter to apply to - see CLAUDE.md). */
.loc-feed-tabs[hidden] {
    display: none;
}

.loc-feed-tab {
    all: unset;
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
    padding: 0.65rem 0 0.5rem;
    cursor: pointer;
}

.loc-feed-tab-label {
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--text-muted);
    padding-bottom: 0.65rem;
    border-bottom: 2px solid transparent;
}

.loc-feed-tab-count {
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--text-muted);
}

.loc-feed-tab.active .loc-feed-tab-label {
    color: var(--text);
    border-bottom-color: var(--accent);
}

.loc-feed-tab.active .loc-feed-tab-count {
    color: var(--accent);
}

/* A native <select>, not a custom dropdown - no framework/extra JS
   needed for open/close or click-outside handling, and it's still just
   a 2-way choice underneath (see feedMode in ui/location/index.php's
   own script). appearance: none strips its default chrome down to
   plain text; the chevron next to it is a separate, purely decorative
   (pointer-events: none) icon, not the browser's own arrow, which
   varies too much in size/position across browsers to match the
   mockup's own small, consistently-placed one. */
.loc-feed-mode-select {
    position: relative;
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    /* Sticks to the toolbar's right edge on its own (rather than via
       justify-content: space-between on the toolbar itself) so it stays
       put when .loc-feed-tabs is hidden in "Mest fundne" mode - with
       only one visible flex child left, space-between has nothing left
       to space *between* and collapses this back to the left edge
       instead - a real, reported mismatch. */
    margin-left: auto;
}

.loc-feed-mode-select select {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background: none;
    border: none;
    margin: 0;
    padding: 0.5rem 1.4rem 0.5rem 0;
    font-family: inherit;
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--text);
    cursor: pointer;
}

.loc-feed-mode-chevron {
    position: absolute;
    right: 0;
    color: var(--text-muted);
    pointer-events: none;
}

.loc-feed-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 0.25rem;
}

.loc-feed-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.6rem;
}

.loc-feed-item {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.65rem 0.75rem;
    border-radius: var(--radius-sm);
    background: var(--bg);
}

.loc-feed-code {
    flex-shrink: 0;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-weight: 700;
    text-decoration: none;
    color: inherit;
}

.loc-feed-code-dots {
    color: var(--text-muted);
    letter-spacing: 0.15em;
    font-weight: 400;
}

.loc-feed-item.latest .loc-feed-code {
    color: var(--accent);
}

.loc-feed-info {
    flex-grow: 1;
    font-size: 0.82rem;
    color: var(--text-muted);
    line-height: 1.3;
}

/* ui/location/country/index.php builds this with a real <a> (the
   finder's name, linking to their participant page) - anchors don't
   inherit color from an ancestor by default (the browser's own <a>
   color always wins over inherited color, only losing to a rule that
   targets the anchor itself), so without this it rendered as the
   default blue link color. A dedicated class, not a broad ".loc-feed-info a"
   descendant selector - that shape (one extra type selector) would beat
   a plain ".badge-first-country" class regardless of source order and
   silently override its own color, the same specificity trap documented
   elsewhere in this file. */
.loc-feed-found-by {
    color: var(--link);
    text-decoration: none;
}

.loc-feed-found-by:hover {
    color: var(--link-hover);
    text-decoration: underline;
}

.loc-feed-time {
    flex-shrink: 0;
    font-size: 0.78rem;
    color: var(--text-muted);
}

.loc-feed-points {
    color: var(--success);
    font-weight: 600;
}

.loc-feed-item.latest .loc-feed-time {
    color: var(--accent-teal);
    font-weight: 700;
}

.loc-station-row {
    display: flex;
    align-items: center;
    gap: 0.875rem;
    padding: 0.6875rem 0.875rem;
    border-radius: var(--radius-sm);
    /* A small gap between rows, on request - without it, two adjacent
       rank-1/2/3 rows' differently-colored backgrounds touched directly
       with no visual separation, reading as one merged block instead of
       distinct rows. */
    margin-bottom: 0.375rem;
    /* The public standings list renders this on a plain <div> (only the
       name inside is its own <a>) - control/location-edit's own
       admin-management copy makes the *whole* row a single <a> instead
       (there's nothing else to click in that context), so this undoes
       the browser's default link styling either way. Harmless on the
       plain <div> case since neither property does anything there. */
    text-decoration: none;
    color: inherit;
}

.loc-station-row[href]:hover {
    background: var(--surface-hover);
}

.loc-station-row:last-child {
    margin-bottom: 0;
}

.loc-station-row.rank-1 {
    background: color-mix(in srgb, #d4af37 14%, transparent);
}

.loc-station-row.rank-2 {
    background: color-mix(in srgb, #9aa5b1 14%, transparent);
}

.loc-station-row.rank-3 {
    background: color-mix(in srgb, #b87333 14%, transparent);
}

.loc-rank-badge {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    border-radius: 11px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 800;
    font-size: 0.875rem;
    color: #ffffff;
    background: var(--border);
}

.loc-rank-badge.gold { background: #c89b1e; }
.loc-rank-badge.silver { background: #8b93a0; }
.loc-rank-badge.bronze { background: #a8632c; }
.loc-rank-badge.plain { background: var(--border); color: var(--text-muted); }

/* The avatar system's own stack of layered PNGs (see ui/assets/avatar.js)
   - a plain circular clip with a neutral background so a genuinely empty
   avatar (the grey placeholder silhouette PNG) still sits on something,
   not transparent page background. Sized per call site via an inline
   width/height (avatarElement()'s own `size` argument) - this class only
   ever sets the shape/clip/background, never a fixed size. */
.avatar-stack {
    border-radius: 50%;
    background: var(--bg);
    flex-shrink: 0;
}

/* A head/ears shape still waiting on a real skin tone (ui/assets/avatar.js's
   headSrc()/earsSrc()) - there's no dedicated white-skin art asset (only
   the 12 real h01-h12 tones), so this recolors whichever real-tone file
   got used for the shape's own outline to solid white instead, alpha
   channel (the silhouette itself) untouched. brightness(0) first crushes
   every opaque pixel to black, invert(1) then flips black to white - two
   steps because there's no single filter function that goes straight to
   "any color -> white" on its own. */
.avatar-layer-white {
    filter: brightness(0) invert(1);
}

/* The participant page's avatar card sits in the same row as the three
   numeric stat cards, not above them in a card of its own - on request,
   once it became clear there was plenty of spare width on that row.
   `.loc-stats-grid.with-avatar` adds a 4th, content-sized column ahead
   of the usual equal three; `.avatar-stat-card` reuses `.loc-stat-card`'s
   own surface/border/shadow/padding look (not the class itself, since
   this card centers a fixed-size image rather than an animated number -
   same "shares tokens, not a class" relationship `.loc-stat-card-text`
   already has to the plain numeric card) so all four cards in the row
   still read as one family. */
.loc-stats-grid.with-avatar {
    grid-template-columns: auto repeat(3, 1fr);
}

.avatar-stat-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    padding: 1.25rem 1.5rem;
    text-align: center;
}

.avatar-stat-card .loc-stat-label {
    margin: 0;
}

/* Positions the small edit-icon button against the avatar circle's own
   corner, not the card's - see ui/location/participant/index.php. */
.avatar-edit-wrap {
    position: relative;
    display: inline-flex;
}

/* The edit-icon button (design provided directly - see CLAUDE.md) - only
   ever unhidden for the owner (or the admin standing in for them);
   another participant's identical-looking card keeps this hidden, so it
   never implies their avatar is editable too. Editing happens by
   clicking this icon, not the avatar or the card itself - on request,
   replacing an earlier "click anywhere on the card" version and, before
   that, a separate text "Edit avatar" button. */
/* Equal-specificity tiebreak: without this, .avatar-edit-icon-button's
   own `display: grid` below wins over the browser's own [hidden] rule
   (both are a single-class/attribute selector, and this stylesheet
   loads after the UA one) - the exact same trap already documented
   several times elsewhere in this file (.menu-dropdown[hidden],
   .loc-stat-trend[hidden], etc.) - caught here as a real reported bug:
   the button rendered (and was clickable) on *every* participant's card,
   not just the owner's, even though the JS correctly left it `hidden`
   for everyone else. */
.avatar-edit-icon-button[hidden] {
    display: none;
}

.avatar-edit-icon-button {
    all: unset;
    box-sizing: border-box;
    position: absolute;
    top: -2px;
    right: -2px;
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: 3px solid var(--surface);
    background: var(--accent);
    color: #ffffff;
    cursor: pointer;
    box-shadow: var(--shadow);
    transition: transform 0.15s ease;
}

.avatar-edit-icon-button:hover {
    transform: scale(1.08);
}

.avatar-edit-icon-button:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Same "only the owner (or the admin standing in for them) ever sees
   this" reasoning as .avatar-edit-icon-button above, but sits inline next
   to the page heading (a flex row already, not a stat card needing an
   absolutely-positioned overlay), so this is a plain small round button,
   not a copy of the avatar button's own positioning. */
.profile-edit-icon-button[hidden] {
    display: none;
}

.profile-edit-icon-button {
    all: unset;
    box-sizing: border-box;
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--accent);
    color: #ffffff;
    cursor: pointer;
    transition: transform 0.15s ease;
}

.profile-edit-icon-button:hover {
    transform: scale(1.08);
}

.profile-edit-icon-button:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* #profile-panel's own compact fields - a plain label/input/button row,
   unlike the full-width block layout the rest of the site's forms use
   (main pages like /control/register), which would look oversized inside
   this page's narrower .loc-card design system. */
.profile-field {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-top: 0.9rem;
    flex-wrap: wrap;
}

/* control/participant-edit's own pincode row (unlike #profile-panel's
   rows elsewhere, which always have a flex:1 <input> already consuming
   the row's full width) has no naturally-growing element, so its two
   halves - label+value, and the two action buttons - are grouped and
   pushed to either edge here, to actually reach the same width as the
   name <input>/.form-actions row above and below it, instead of
   shrink-wrapping to a narrower cluster on the left. */
.profile-field-wide {
    justify-content: space-between;
}

.profile-field-info,
.profile-field-buttons {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

.profile-field label,
.profile-field > span:first-child {
    font-size: 0.85rem;
    font-weight: 600;
    margin: 0;
    white-space: nowrap;
}

.profile-field input[type="text"] {
    width: auto;
    flex: 1;
    min-width: 8rem;
    padding: 0.45rem 0.7rem;
}

.profile-field button {
    width: auto;
    margin-top: 0;
    padding: 0.45rem 1rem;
    font-size: 0.85rem;
}

.profile-pincode-value {
    font-family: 'JetBrains Mono', monospace;
    font-weight: 700;
    letter-spacing: 0.15em;
}

/* ui/assets/avatar-picker.js's own picker - only ever mounted on a
   participant's own page, for that participant (or the owning admin)
   only. Two columns: the preview stays in view on the left
   (position: sticky, so it's still visible after scrolling further
   down the category rows on the right) rather than sitting above them
   and scrolling out of view - on request, matching a reference
   builder's own layout. */
.avatar-picker-layout {
    display: flex;
    align-items: flex-start;
    gap: 2rem;
}

.avatar-picker-preview-wrap {
    position: sticky;
    top: 1rem;
    flex-shrink: 0;
}

.avatar-picker-categories {
    min-width: 0;
    flex: 1;
}

.avatar-picker-preview {
    display: flex;
    justify-content: center;
}

.avatar-picker-preview .avatar-stack {
    box-shadow: var(--shadow-lg);
}

@media (max-width: 640px) {
    /* Not enough width for both a fixed-size sticky column and a
       comfortable set of option rows - stacks normally instead, same
       as before this layout existed. */
    .avatar-picker-layout {
        flex-direction: column;
        align-items: center;
        gap: 1.25rem;
    }

    .avatar-picker-preview-wrap {
        position: static;
    }
}

.avatar-picker-section {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-bottom: 1.25rem;
}

.avatar-picker-section-title {
    margin: 0;
    font-size: 0.8125rem;
    font-weight: 800;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.avatar-picker-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.avatar-picker-option {
    all: unset;
    box-sizing: border-box;
    position: relative;
    cursor: pointer;
    width: 48px;
    height: 48px;
    border-radius: 50%;
    border: 2px solid var(--border);
    flex-shrink: 0;
}

.avatar-picker-option:hover {
    border-color: var(--accent);
}

.avatar-picker-option.active {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-bg);
}

.loc-station-info {
    flex: 1;
    min-width: 0;
}

.loc-station-name {
    font-weight: 700;
    color: var(--text);
    text-decoration: none;
}

.loc-station-name:hover {
    text-decoration: underline;
}

.loc-station-sub {
    margin: 0;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-muted);
}

.loc-station-row.empty-state .loc-station-name {
    color: var(--text-muted);
}

.loc-station-count {
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-weight: 700;
    font-size: 1.25rem;
    flex-shrink: 0;
}

.loc-country-row {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    padding: 0.55rem 0;
}

.loc-country-chip {
    flex-shrink: 0;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-weight: 700;
    font-size: 0.75rem;
    background: var(--accent-bg);
    color: var(--accent);
    padding: 0.15rem 0.4rem;
    border-radius: 6px;
}

.loc-country-name {
    /* A fixed width, not just a cap (max-width) - .loc-country-bar-track
       right after it is flex:1, so it absorbs whatever space this column
       doesn't use. A variable-width name column (short "USA" vs. long
       "Storbritannien") therefore made the grey track itself a different
       length on every row - a real, reported bug, not just a cosmetic
       nit: the bar's own background is supposed to represent a shared
       "out of the maximum" scale, which only reads correctly if it's the
       same length everywhere the count-proportional fill is compared
       against. */
    flex-shrink: 0;
    width: 9rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text);
    text-decoration: none;
    font-weight: 600;
}

.loc-country-name:hover {
    text-decoration: underline;
}

/* Every code from this country was found invalid (on request). */
.loc-country-row.all-invalid .loc-country-chip {
    background: var(--danger-bg);
    color: var(--danger);
}

.loc-country-row.all-invalid .loc-country-name,
.loc-country-row.all-invalid .loc-country-count {
    color: var(--danger);
}

.loc-country-row.all-invalid .loc-country-bar-fill {
    background: var(--danger);
}

.loc-country-bar-track {
    flex: 1;
    /* Without this, a flex item's default min-width is `auto`, not 0 -
       it won't shrink below its content's own intrinsic size. Firefox
       enforces that default more strictly than Chromium/WebKit do here,
       so the percentage-width bar-fill child inside it was computing
       against the wrong basis and every bar rendered close to full
       width regardless of its actual share - a real, reported bug, not
       a theoretical one. */
    min-width: 0;
    height: 6px;
    background: var(--bg);
    border-radius: 999px;
    overflow: hidden;
}

.loc-country-bar-fill {
    /* This is a <span> (inline by default) - `width` never applies to a
       non-replaced inline element, in any browser, so without this the
       bar was rendering at 0 width everywhere despite its `style.width`
       being set correctly - checking that inline style string (always
       whatever JS just set it to) is not the same as checking the
       element's actual rendered width, which is the check that was
       missed the first time this looked verified. */
    display: block;
    height: 100%;
    background: var(--accent);
    border-radius: 999px;
}

.loc-country-count {
    flex-shrink: 0;
    width: 1.6rem;
    text-align: right;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-weight: 700;
}

/* Equal-specificity tiebreak: without this, some browsers (Firefox) let
   the display:inline-flex below win over the browser's own [hidden]
   rule, since both have the same specificity and this stylesheet loads
   later - same fix already established for .menu-dropdown[hidden]. This
   one is real, not theoretical: #feed-see-all starts `hidden` in markup
   and would otherwise always show regardless. */
.loc-card-footer-link[hidden] {
    display: none;
}

.loc-card-footer-link {
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    /* min-height 44px - see the copy button's comment above on the same
       hit-target requirement; padding (not just min-height) so the extra
       height is actually part of the clickable box, not empty space
       above/below an inline-flex child. */
    min-height: 44px;
    padding: 0.75rem 0;
    margin-top: 0.1rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--accent);
    text-decoration: none;
}

.loc-card-footer-link:hover {
    text-decoration: underline;
}

/* Houses the "show 10 more"/"show all" pair a paginated list (Stillingen,
   Lande) grows into once it has more rows than fit on the first page -
   see renderPaginatedList() in ui/location/index.php. */
.loc-list-more {
    display: flex;
    gap: 1.25rem;
}

.loc-list-more[hidden] {
    display: none;
}

.loc-skeleton {
    border-radius: var(--radius-sm);
    background: linear-gradient(
        90deg,
        var(--surface-hover) 25%,
        var(--border) 37%,
        var(--surface-hover) 63%
    );
    background-size: 400% 100%;
    animation: loc-skeleton-sweep 1.4s ease infinite;
}

@keyframes loc-skeleton-sweep {
    0% { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}

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

.loc-empty-state {
    text-align: center;
    padding: 1.5rem 1rem;
}

.loc-empty-state p {
    color: var(--text-muted);
    margin: 0 0 0.85rem;
}

@media (max-width: 900px) {
    .loc-main-grid {
        grid-template-columns: 1fr;
    }

    .loc-stats-grid {
        grid-template-columns: 1fr;
    }

    .loc-stats-grid.with-avatar {
        grid-template-columns: 1fr;
    }

    .loc-stats-row {
        grid-template-columns: 1fr;
    }

    .loc-quiz-body {
        grid-template-columns: 1fr;
    }

    /* .loc-stats-row .loc-stats-grid's own unconditional rule (two
       classes) outranks the plain .loc-stats-grid override just above
       (one class) regardless of this media query, so the nested pair/
       avatar+pair grid needs its own explicit narrow-width override
       here too, or it would stay stuck at 2-3 columns even once
       .loc-stats-row itself has already collapsed to one. */
    .loc-stats-row .loc-stats-grid,
    .loc-stats-row .loc-stats-grid.with-avatar {
        grid-template-columns: 1fr;
    }

    .loc-feed-grid {
        grid-template-columns: 1fr;
    }

    .loc-topbar-name {
        font-size: 2.5rem;
    }

    .location-card-name {
        font-size: 2.5rem;
    }

    .location-card-year {
        font-size: 1.25rem;
    }
}

@media (max-width: 480px) {
    .loc-continents {
        grid-template-columns: repeat(2, 1fr);
    }

    .loc-topbar-name {
        font-size: 2rem;
    }

    .location-card-name {
        font-size: 2rem;
    }
}

/* ui/location/participant/index.php - the "Min Side" redesign (Claude
   Design mockup imported 2026-09-07 - see CLAUDE.md). Reuses .loc-card,
   .loc-stats-grid/.loc-stat-card, .loc-country-chip and
   .loc-country-bar-track/-fill as-is (same tokens as every other
   /location/* subpage); the rules below are only the pieces genuinely
   new to this page - the rank badge inside a stat card, the day-grouped
   code list, and the point chip's multi-line tooltip. */

.loc-stat-rank {
    display: inline-flex;
    align-items: center;
    /* Measured directly against the mockup's own source (10px 18px pad,
       30px font) - almost as large as the card's own value it sits next
       to, not a small secondary label. See CLAUDE.md. */
    padding: 0.625rem 1.125rem;
    border-radius: 999px;
    font-size: 1.875rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

.loc-stat-card.accent-blue .loc-stat-rank { background: var(--accent-bg); color: var(--accent); }
.loc-stat-card.accent-teal .loc-stat-rank { background: var(--accent-teal-bg); color: var(--accent-teal); }
.loc-stat-card.accent-orange .loc-stat-rank { background: var(--accent-orange-bg); color: var(--accent-orange); }

.badge-first-continent {
    color: var(--accent-orange);
    background: var(--accent-orange-bg);
}

/* The continent badge links to that continent's own page (see
   badgesFor()) - code/country badges used to link to a first-finders
   leaderboard but no longer do, on request (the page itself is gone
   now too - see CLAUDE.md). */
a.badge-first-continent:hover {
    text-decoration: underline;
}

.loc-day-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.875rem 1.5rem 0.5rem;
}

.loc-day-header-label {
    font-size: 0.75rem;
    font-weight: 800;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.loc-day-header-summary {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--text-muted);
}

.loc-code-row {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    padding: 0.75rem 1.5rem;
    border-top: 1px solid var(--border);
    flex-wrap: wrap;
}

/* Two independent highlights, not one generic "hot" threshold: a new
   country washes in gold from the left, first-with-the-code washes in
   green from the right - a row that's both gets both, layered as two
   separate gradients (one property can't otherwise hold two rules'
   worth of background, so the combined case needs its own selector). */
.loc-code-row.new-country {
    background: linear-gradient(to right, var(--warning-bg), transparent 60%);
}

.loc-code-row.first-code {
    background: linear-gradient(to left, var(--success-bg), transparent 60%);
}

.loc-code-row.new-country.first-code {
    background:
        linear-gradient(to right, var(--warning-bg), transparent 60%),
        linear-gradient(to left, var(--success-bg), transparent 60%);
}

/* A fixed 40px box, centered text, no horizontal padding - not
   .loc-country-chip's own auto-width shape (sized to its 2-letter
   content plus horizontal padding) elsewhere. Every chip in this list
   lines up in the same column regardless of how wide its own code is,
   which auto-width can't guarantee. */
.loc-code-row .loc-country-chip {
    width: 2.5rem;
    text-align: center;
    padding: 0.375rem 0;
    border-radius: 8px;
}

.loc-code-row-country {
    width: 8.25rem;
    flex-shrink: 0;
    font-size: 1rem;
    font-weight: 700;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: inherit;
    text-decoration: none;
}

.loc-code-row-country:hover {
    text-decoration: underline;
}

.loc-code-row-code {
    flex-shrink: 0;
    width: 6rem;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 1rem;
    font-weight: 700;
    color: var(--accent);
    letter-spacing: 0.08em;
    text-decoration: none;
}

.loc-code-row-code:hover {
    text-decoration: underline;
}

.loc-code-row-badges {
    flex: 1;
    min-width: 8rem;
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

/* Measured larger than the site's generic .badge (used for the small
   admin-badge elsewhere) - the mockup's own badges are a more prominent
   4px/10px pill at 800 weight, not the tighter 600-weight tag .badge
   defaults to. Scoped to this list rather than changed on .badge itself,
   so other pages' plain admin-badge usage is untouched. */
.loc-code-row-badges .badge {
    padding: 0.25rem 0.625rem;
    font-size: 0.6875rem;
    font-weight: 800;
}

.loc-code-row-time {
    flex-shrink: 0;
    width: 6.75rem;
    text-align: right;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-muted);
}

.loc-point-chip {
    position: relative;
    flex-shrink: 0;
    display: inline-flex;
    align-items: baseline;
    gap: 0.2rem;
    padding: 0.375rem 0.75rem;
    border-radius: 999px;
    /* The mockup's own flat colors for this chip, not the generic
       --bg/--text tokens - sampled/measured against the mockup source
       rather than assumed to match. Color itself depends on the point
       *value* (see the .hot modifier below), not on which badges the
       row has - a real, previously-missed rule (the .hot class used to
       be driven by the same new-country/first-code flags as the row's
       own highlight, which isn't what the mockup actually does). */
    background: #f1f2fb;
    font-family: 'JetBrains Mono', monospace;
    font-weight: 700;
    font-size: 1rem;
    color: #4a5075;
    cursor: help;
}

.loc-point-chip.hot {
    background: rgba(220, 180, 40, 0.2);
    color: #724000;
}

/* The "most found"/country/continent code lists' own count chip
   ("3 gange") reuses .loc-point-chip's look, but unlike a real point
   chip, has no hover breakdown to explain (there's no per-find point
   value here) - it used to still carry a native title tooltip
   ("gange fundet") plus this base class's own cursor: help, which
   together rendered as the browser's own plain dark tooltip box
   instead of this site's real, custom-styled one (a real, reported
   "looks broken" mismatch). The unit is now just always-visible text
   next to the number instead, so there's nothing left to hover for. */
.loc-count-chip {
    cursor: default;
}

.loc-point-chip-unit {
    font-size: 0.68rem;
    font-weight: 700;
    opacity: 0.7;
}

/* Appended to document.body, never nested inside .loc-point-chip - see
   ui/assets/points.js's buildPointChip() for the full reasoning: every
   row this chip sits in lives inside a card with its own
   overflow: hidden, which clips *any* descendant tooltip regardless of
   z-index (a real bug, reported twice, that no z-index value could
   ever have fixed - only moving the element out of the clipped subtree
   does). position: fixed since its top/left are computed from
   getBoundingClientRect() (viewport coordinates, see buildPointChip())
   each time it's shown; z-index comfortably clears every other value
   in this file (the highest otherwise is Leaflet's own 1000-1100), so
   it renders above literally anything else on the page, not just its
   own card. Shown/hidden via the .visible class below, toggled by real
   JS event listeners (buildPointChip() again) rather than a CSS :hover
   rule, since the tooltip is no longer a DOM descendant of the chip at
   all once portalled out this way. */
.loc-point-tooltip {
    position: fixed;
    width: 232px;
    padding: 0.6875rem 0.8125rem;
    border-radius: 12px;
    background: var(--ink);
    color: var(--ink-contrast);
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    text-align: left;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.16s ease;
    box-shadow: var(--shadow);
    z-index: 1200;
}

.loc-point-tooltip.visible {
    opacity: 1;
    pointer-events: auto;
}

.loc-point-tooltip-line {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1.35;
}

.loc-point-tooltip-line-value {
    flex-shrink: 0;
    font-weight: 700;
    color: var(--gold-text);
}

.loc-point-tooltip-line-text {
    flex: 1;
    font-weight: 500;
    color: rgba(255, 255, 255, 0.86);
}

.loc-point-tooltip-total {
    margin-top: 0.1875rem;
    padding-top: 0.375rem;
    border-top: 1px solid rgba(255, 255, 255, 0.18);
    font-size: 0.75rem;
    font-weight: 700;
}

.loc-continent-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.625rem 0.75rem;
    border-radius: 14px;
}

.loc-continent-row.reached {
    background: var(--bg);
}

.loc-continent-row.missing {
    border: 1px dashed var(--border);
}

.loc-continent-row-name {
    flex: 1;
    font-size: 0.9375rem;
    font-weight: 600;
    color: inherit;
    text-decoration: none;
}

.loc-continent-row-name:hover {
    text-decoration: underline;
}

.loc-continent-row.reached .loc-continent-row-name {
    font-weight: 700;
}

.loc-continent-row-missing-label {
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--text-muted);
}

/* 8px in the mockup, not .loc-country-bar-track's own default 6px (sized
   for the smaller inline bars elsewhere on the main location page) -
   scoped to this list rather than changed on the shared track/fill pair
   itself. */
.loc-continent-row .loc-country-bar-track {
    height: 8px;
}

.loc-continent-row-count {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.9375rem;
    font-weight: 700;
    width: 1rem;
    text-align: right;
}

/* "How many more codes until your next avatar item" - the participant
   page's only milestone (see buildUnlockProgress() in
   ui/location/participant/index.php), shown both in its own sidebar card
   and inside the just-added-a-code result card. Same track/fill idea as
   .loc-country-bar-track/-fill, just taller and with a two-tone gradient
   fill to read as a headline progress bar rather than an inline row
   detail. */
.unlock-progress-text {
    margin: 0;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--text-muted);
}

.unlock-progress-track {
    height: 14px;
    margin-top: 0.75rem;
    background: var(--bg);
    border-radius: 999px;
    overflow: hidden;
}

.unlock-progress-fill {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--accent), var(--accent-teal));
}

.unlock-progress-labels {
    margin-top: 0.5rem;
    display: flex;
    justify-content: space-between;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--text-muted);
}

/* Inside the result card it gets the same hairline separation as the
   facts section below it. */
.code-result-unlock-progress {
    padding-top: 0.75rem;
    border-top: 1px solid var(--border);
}

/* The points-system card at the bottom of the right column - a dark,
   distinct "reference" card so it reads as background info rather than
   another live stat, same intent as the dark tooltip above. Its content
   mirrors ui/location/points/index.php almost exactly (same four rules),
   just condensed for a sidebar instead of a full page. */
.loc-scoring-card {
    padding: 1.5rem 1.625rem;
    border-radius: 24px;
    background: linear-gradient(120deg, #1b1f3c, #2c2a63 60%, #453a86);
    box-shadow: 0 16px 34px rgba(27, 31, 60, 0.28);
    color: #fff;
}

.loc-scoring-eyebrow {
    margin: 0;
    font-size: 0.8125rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.55);
}

.loc-scoring-lede {
    margin: 0.5rem 0 0;
    font-size: 1.3125rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1.25;
}

.loc-scoring-rule {
    margin-top: 0.75rem;
    padding: 1.125rem 1.25rem;
    border-radius: 18px;
    background: rgba(255, 255, 255, 0.1);
    border: 1px solid rgba(255, 255, 255, 0.16);
}

/* The mockup wraps its 4 rule cards in their own container with
   margin-top:18px, separate from the 12px gap *between* rules - this
   markup has no such wrapper (the rules are plain siblings of the lede),
   so the first rule needs its own larger gap to match. */
.loc-scoring-lede + .loc-scoring-rule {
    margin-top: 1.125rem;
}

.loc-scoring-rule-head {
    display: flex;
    align-items: center;
    gap: 0.6875rem;
}

.loc-scoring-rule-number {
    width: 1.875rem;
    height: 1.875rem;
    border-radius: 10px;
    display: grid;
    place-items: center;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.9375rem;
    font-weight: 700;
    background: var(--gold-bg);
    color: #241d05;
}

.loc-scoring-rule-title {
    margin: 0;
    font-size: 1.0625rem;
    font-weight: 700;
    letter-spacing: -0.015em;
}

.loc-scoring-rule-text {
    margin: 0.75rem 0 0;
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.55;
    color: rgba(255, 255, 255, 0.78);
}

.loc-scoring-rule-bonus {
    margin-top: 0.5rem;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.625rem 0.8125rem;
    border-radius: 13px;
    background: rgba(255, 255, 255, 0.13);
}

.loc-scoring-rule-bonus-value {
    flex-shrink: 0;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.9375rem;
    font-weight: 700;
    color: var(--gold-text);
    white-space: nowrap;
}

.loc-scoring-rule-bonus-text {
    flex: 1;
    font-size: 0.875rem;
    font-weight: 600;
}

.loc-scoring-minmax {
    margin-top: 0.75rem;
    display: flex;
    align-items: center;
    gap: 0.875rem;
    padding: 1rem 1.125rem;
    border-radius: 18px;
    background: rgba(255, 255, 255, 0.16);
    border: 1px solid rgba(255, 255, 255, 0.22);
}

.loc-scoring-minmax-value {
    flex-shrink: 0;
    font-family: 'JetBrains Mono', monospace;
    font-size: 2.125rem;
    font-weight: 700;
    color: var(--gold-text);
    line-height: 1;
}

.loc-scoring-minmax-label {
    font-size: 0.8125rem;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.5);
}

.loc-scoring-minmax-text {
    margin-top: 0.1875rem;
    font-size: 0.9375rem;
    font-weight: 600;
    line-height: 1.4;
    color: rgba(255, 255, 255, 0.8);
}

.loc-scoring-footnote {
    margin: 0.875rem 0 0;
    font-size: 0.875rem;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.6);
    line-height: 1.5;
}

@media (max-width: 720px) {
    .loc-code-row {
        flex-wrap: wrap;
    }

    .loc-code-row-badges {
        order: 4;
        width: 100%;
    }

    .loc-code-row-time {
        text-align: left;
    }
}

/* ===================================================================
   TV dashboard (ui/location/dashboard/) - see CLAUDE.md's "TV
   dashboard" entry. Laid out on a fixed 1920x1080 "stage" in real
   pixels, then scaled as a whole to fit whatever screen it's actually
   on (dashboard JS sets --dash-scale), so every size below is exactly
   what a 1080p TV shows - no responsive breakpoints to reason about.
   Deliberately always dark regardless of the site's own light/dark
   theme (like .loc-code-badge): it's read from across a room, and a
   dark frame makes the light map and the colored banners pop.
   =================================================================== */
body:has(.dash-viewport) {
    margin: 0;
    overflow: hidden;
    background: #0b0a24;
}

/* The fixed corner "log in as admin" link (app.js) has no place on a TV. */
body:has(.dash-viewport) .admin-login-corner-link {
    display: none;
}

main.dash-main {
    max-width: none;
    margin: 0;
    padding: 0;
}

.dash-viewport {
    position: fixed;
    inset: 0;
    overflow: hidden;
    background: radial-gradient(ellipse at 20% 0%, #26215f 0%, #0b0a24 60%);
}

.dash-viewport.hide-cursor {
    cursor: none;
}

.dash-stage {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 1920px;
    height: 1080px;
    transform: translate(-50%, -50%) scale(var(--dash-scale, 1));
    transform-origin: center center;
    color: #ffffff;
    display: grid;
    grid-template-rows: 128px 1fr 150px;
    gap: 24px;
    padding: 32px;
    box-sizing: border-box;
}

/* --- header --- */
.dash-header {
    display: grid;
    grid-template-columns: 1fr auto auto;
    align-items: center;
    gap: 48px;
}

.dash-title {
    display: flex;
    align-items: center;
    gap: 24px;
    min-width: 0;
}

.dash-name {
    margin: 0;
    font-size: 64px;
    font-weight: 800;
    letter-spacing: -0.02em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-code {
    flex-shrink: 0;
    padding: 10px 20px;
    border-radius: 14px;
    background: rgba(255, 255, 255, 0.12);
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 34px;
    font-weight: 700;
    letter-spacing: 0.06em;
}

.dash-stats {
    display: flex;
    gap: 20px;
}

.dash-stat {
    min-width: 210px;
    padding: 14px 26px;
    border-radius: 20px;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.12);
}

.dash-stat-value {
    margin: 0;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 56px;
    font-weight: 700;
    line-height: 1;
}

.dash-stat.accent-blue .dash-stat-value { color: #a9a6ff; }
.dash-stat.accent-teal .dash-stat-value { color: #5eead4; }
.dash-stat.accent-orange .dash-stat-value { color: #fdba74; }

.dash-stat-label {
    margin: 8px 0 0;
    font-size: 20px;
    font-weight: 700;
    color: rgba(255, 255, 255, 0.7);
}

/* A value that just went up gets a short pop, so a change is visible
   from across the room even without a banner. */
.dash-stat-value.bump {
    animation: dash-bump 0.9s ease;
}

@keyframes dash-bump {
    0% { transform: scale(1); }
    30% { transform: scale(1.18); }
    100% { transform: scale(1); }
}

.dash-clock {
    text-align: right;
}

.dash-clock-time {
    margin: 0;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 72px;
    font-weight: 700;
    line-height: 1;
}

.dash-clock-status {
    margin: 10px 0 0;
    font-size: 18px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.55);
}

.dash-clock-status.offline {
    color: #fca5a5;
}

/* --- body: big map + rotating panel --- */
.dash-body {
    display: grid;
    grid-template-columns: 1fr 600px;
    gap: 24px;
    min-height: 0;
}

.dash-map-card {
    position: relative;
    border-radius: 24px;
    overflow: hidden;
    background: #eef1f8;
}

/* Same two-class trick as .location-map.leaflet-container - leaflet.css
   loads after this file and would otherwise win with its own #ddd. */
.dash-map,
.dash-map.leaflet-container {
    position: absolute;
    inset: 0;
    background: #eef1f8;
}

/* The just-found country, while the map is zoomed in on it - same
   warning color as the location page's own .country-just-added, plus a
   slow pulse so it reads as "this one, right now" from a distance. */
.dash-map .country-just-added {
    animation: dash-country-pulse 1.6s ease-in-out infinite;
}

@keyframes dash-country-pulse {
    0%, 100% { fill-opacity: 0.95; }
    50% { fill-opacity: 0.45; }
}

.dash-map-label {
    position: absolute;
    left: 28px;
    bottom: 28px;
    z-index: 500;
    padding: 14px 24px;
    border-radius: 16px;
    background: rgba(11, 10, 36, 0.86);
    color: #ffffff;
    font-size: 30px;
    font-weight: 800;
}

.dash-map-label[hidden] {
    display: none;
}

.dash-map-label-sub {
    display: block;
    margin-top: 4px;
    font-size: 20px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.7);
}

.dash-panel {
    position: relative;
    display: flex;
    flex-direction: column;
    padding: 28px 30px;
    border-radius: 24px;
    background: rgba(255, 255, 255, 0.07);
    border: 1px solid rgba(255, 255, 255, 0.12);
    overflow: hidden;
    min-height: 0;
}

.dash-panel-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 16px;
    margin-bottom: 20px;
}

.dash-panel-title {
    margin: 0;
    font-size: 32px;
    font-weight: 800;
}

.dash-panel-dots {
    display: flex;
    gap: 8px;
}

.dash-panel-dot {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.25);
}

.dash-panel-dot.active {
    background: #ffffff;
}

/* How long until the panel rotates to its next view. */
.dash-panel-timer {
    position: absolute;
    left: 0;
    bottom: 0;
    height: 6px;
    width: 100%;
    background: linear-gradient(90deg, #7d79f0, #5eead4);
    transform-origin: left center;
    animation: dash-timer linear forwards;
}

@keyframes dash-timer {
    from { transform: scaleX(1); }
    to { transform: scaleX(0); }
}

.dash-panel-content {
    flex: 1;
    min-height: 0;
    animation: dash-fade-in 0.6s ease;
}

@keyframes dash-fade-in {
    from { opacity: 0; transform: translateY(12px); }
    to { opacity: 1; transform: translateY(0); }
}

.dash-rows {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.dash-row {
    display: grid;
    grid-template-columns: 56px 60px 1fr auto;
    align-items: center;
    gap: 16px;
    padding: 8px 16px 8px 10px;
    border-radius: 16px;
    background: rgba(255, 255, 255, 0.05);
    min-height: 68px;
}

.dash-row.no-avatar {
    grid-template-columns: 56px 1fr auto;
}

.dash-row.rank-1 { background: rgba(221, 178, 39, 0.2); }
.dash-row.rank-2 { background: rgba(203, 213, 225, 0.14); }
.dash-row.rank-3 { background: rgba(217, 119, 6, 0.16); }

.dash-rank {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    border-radius: 14px;
    background: rgba(255, 255, 255, 0.12);
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 26px;
    font-weight: 700;
}

.dash-row.rank-1 .dash-rank { background: #ddb227; color: #2b2100; }
.dash-row.rank-2 .dash-rank { background: #cbd5e1; color: #1e293b; }
.dash-row.rank-3 .dash-rank { background: #d97706; color: #2a1500; }

.dash-row-avatar {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    overflow: hidden;
    background: rgba(255, 255, 255, 0.9);
}

.dash-row-name {
    min-width: 0;
    font-size: 30px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-row-sub {
    display: block;
    font-size: 18px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.6);
}

.dash-row-admin {
    margin-left: 10px;
    padding: 2px 10px;
    border-radius: 999px;
    background: rgba(125, 121, 240, 0.35);
    font-size: 16px;
    font-weight: 700;
    vertical-align: middle;
}

.dash-row-value {
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 36px;
    font-weight: 700;
    white-space: nowrap;
}

.dash-row-flag {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 48px;
    border-radius: 12px;
    background: rgba(255, 255, 255, 0.12);
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 22px;
    font-weight: 700;
}

.dash-empty {
    margin: 40px 0 0;
    font-size: 30px;
    font-weight: 700;
    color: rgba(255, 255, 255, 0.7);
    text-align: center;
}

/* --- points-system view --- */
.dash-scoring {
    display: flex;
    flex-direction: column;
    gap: 14px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.dash-scoring-rule {
    display: grid;
    grid-template-columns: 110px 1fr;
    align-items: center;
    gap: 18px;
    padding: 14px 18px;
    border-radius: 16px;
    background: rgba(255, 255, 255, 0.06);
}

.dash-scoring-points {
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 34px;
    font-weight: 700;
    color: #fde68a;
    text-align: center;
}

.dash-scoring-text {
    font-size: 25px;
    font-weight: 700;
    line-height: 1.25;
}

.dash-scoring-note {
    margin: 18px 0 0;
    font-size: 22px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.7);
}

/* --- Døgnerquiz turns (every second panel turn for the first 5
   minutes of a new question) --- */
.dash-panel.quiz-mode {
    background: linear-gradient(160deg, rgba(91, 87, 236, 0.45), rgba(13, 148, 136, 0.3));
    border-color: rgba(255, 255, 255, 0.3);
}

.dash-quiz-next {
    margin: 0 0 16px;
    font-size: 24px;
    font-weight: 700;
    color: #fde68a;
}

.dash-quiz-eyebrow {
    margin: 0;
    font-size: 24px;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #fde68a;
}

.dash-quiz-question {
    margin: 14px 0 24px;
    font-size: 38px;
    font-weight: 800;
    line-height: 1.2;
}

.dash-quiz-options {
    display: grid;
    grid-template-columns: 1fr;
    gap: 12px;
}

.dash-quiz-option {
    display: grid;
    grid-template-columns: 52px 1fr;
    align-items: center;
    gap: 16px;
    padding: 14px 18px;
    border-radius: 16px;
    font-size: 27px;
    font-weight: 700;
    line-height: 1.2;
    color: #ffffff;
}

/* The classic Kahoot palette, solid here (not the location page's
   toned-down tint) - this is the one place it's meant to shout. */
.dash-quiz-option.option-a { background: #e21b3c; }
.dash-quiz-option.option-b { background: #1368ce; }
.dash-quiz-option.option-c { background: #b88600; }
.dash-quiz-option.option-d { background: #26890c; }

.dash-quiz-letter {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 52px;
    height: 52px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.25);
    font-size: 28px;
    font-weight: 800;
}

/* The previous question's answer: the right option stays bright and
   framed, the rest fade back; each shows how many picked it. */
.dash-quiz-option.with-percent {
    grid-template-columns: 52px 1fr auto;
}

.dash-quiz-option.wrong {
    opacity: 0.4;
}

.dash-quiz-option.correct {
    outline: 5px solid #ffffff;
    outline-offset: 2px;
}

.dash-quiz-percent {
    font-size: 30px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

.dash-quiz-footer {
    margin: 22px 0 0;
    font-size: 22px;
    font-weight: 700;
    color: rgba(255, 255, 255, 0.85);
}

/* --- bottom ticker: latest finds --- */
/* The bottom row: the ticker, and the QR code in the bottom-right corner. */
.dash-bottom {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 24px;
    min-height: 0;
}

.dash-qr {
    display: flex;
    align-items: center;
    gap: 20px;
    padding: 0 24px 0 0;
    border-radius: 20px;
    background: rgba(255, 255, 255, 0.07);
    border: 1px solid rgba(255, 255, 255, 0.12);
    overflow: hidden;
}

/* display: flex beats [hidden] - see CLAUDE.md's CSS gotchas. */
.dash-qr[hidden] {
    display: none;
}

/* White quiet zone around the code - a phone camera needs the contrast. */
.dash-qr-code {
    width: 150px;
    height: 150px;
    box-sizing: border-box;
    padding: 10px;
    background: #ffffff;
}

.dash-qr-code svg {
    display: block;
    width: 100%;
    height: 100%;
}

.dash-qr-title {
    margin: 0 0 8px;
    font-size: 26px;
    font-weight: 800;
    line-height: 1.15;
    max-width: 260px;
}

.dash-qr-url {
    margin: 0;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 20px;
    color: rgba(255, 255, 255, 0.7);
}

.dash-ticker {
    display: grid;
    grid-template-columns: auto 1fr;
    /* A grid item next to the QR card - without this its long, scrolling
       track would push the QR card off screen. */
    min-width: 0;
    align-items: center;
    border-radius: 20px;
    background: rgba(255, 255, 255, 0.07);
    border: 1px solid rgba(255, 255, 255, 0.12);
    overflow: hidden;
}

.dash-ticker-label {
    align-self: stretch;
    display: flex;
    align-items: center;
    /* Wide enough for the longest of its three labels, so the strip
       doesn't jump sideways each time it switches mode. */
    min-width: 330px;
    box-sizing: border-box;
    padding: 0 28px;
    background: #5b57ec;
    font-size: 24px;
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.dash-ticker-window {
    overflow: hidden;
    white-space: nowrap;
    mask-image: linear-gradient(90deg, transparent, #000 4%, #000 96%, transparent);
}

.dash-ticker-track {
    display: inline-flex;
    gap: 56px;
    padding-left: 40px;
    animation: dash-ticker-scroll linear infinite;
}

@keyframes dash-ticker-scroll {
    from { transform: translateX(0); }
    to { transform: translateX(-50%); }
}

.dash-ticker-item {
    display: inline-flex;
    align-items: center;
    gap: 14px;
    font-size: 28px;
    font-weight: 700;
}

.dash-ticker-code {
    padding: 4px 12px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.12);
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 24px;
}

.dash-ticker-result {
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 24px;
}

.dash-ticker-result.correct { color: #86efac; }
.dash-ticker-result.wrong { color: #fca5a5; }

.dash-ticker-meta {
    color: rgba(255, 255, 255, 0.6);
    font-weight: 600;
}

/* --- F1-style "New race leader" banner --- */
.dash-banner {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 170px;
    z-index: 1000;
    display: flex;
    align-items: stretch;
    height: 170px;
    transform: translateX(-105%);
    transition: transform 0.7s cubic-bezier(0.2, 0.9, 0.2, 1);
    filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.5));
    pointer-events: none;
}

.dash-banner.show {
    transform: translateX(0);
}

.dash-banner.leaving {
    transform: translateX(105%);
}

.dash-banner-label {
    display: flex;
    align-items: center;
    padding: 0 60px 0 80px;
    clip-path: polygon(0 0, 100% 0, calc(100% - 50px) 100%, 0 100%);
    font-size: 50px;
    font-weight: 800;
    font-style: italic;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: #ffffff;
    background: #e10600;
}

.dash-banner-body {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin-left: -30px;
    padding: 0 80px 0 70px;
    clip-path: polygon(50px 0, 100% 0, 100% 100%, 0 100%);
    background: #ffffff;
    color: #0b0a24;
}

.dash-banner-text {
    margin: 0;
    font-size: 64px;
    font-weight: 800;
    line-height: 1.05;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-banner-sub {
    margin: 8px 0 0;
    font-size: 32px;
    font-weight: 700;
    color: #4b5563;
}

/* The banner's second line (and the map label's) start with a capital -
   their texts ("fundet af {name}", "er nu nr. 2 ...") are written
   lowercase because the same words also appear mid-sentence elsewhere,
   e.g. in the ticker. On request. */
.dash-banner-sub::first-letter,
.dash-map-label-sub::first-letter {
    text-transform: uppercase;
}

.dash-banner.kind-country .dash-banner-label { background: #d97706; }
.dash-banner.kind-leader .dash-banner-label { background: #e10600; }
.dash-banner.kind-top3 .dash-banner-label { background: #7c3aed; }
.dash-banner.kind-milestone .dash-banner-label { background: #0d9488; }
.dash-banner.kind-quiz .dash-banner-label { background: #1368ce; }

@media (prefers-reduced-motion: reduce) {
    .dash-banner {
        transition: none;
    }

    .dash-ticker-track,
    .dash-map .country-just-added,
    .dash-stat-value.bump {
        animation: none;
    }
}

/* --- on-screen controls, shown only while the mouse moves --- */
.dash-controls {
    position: fixed;
    top: 16px;
    right: 16px;
    z-index: 2000;
    display: flex;
    gap: 10px;
    transition: opacity 0.3s ease;
}

.dash-controls.idle {
    opacity: 0;
    pointer-events: none;
}

.dash-controls a,
.dash-controls button {
    padding: 10px 18px;
    border: none;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.92);
    color: #0b0a24;
    font: inherit;
    font-size: 15px;
    font-weight: 700;
    text-decoration: none;
    cursor: pointer;
}

.dash-controls button[hidden] {
    display: none;
}

/* ===================================================================
   Døgnerquiz banners - a participant's row background in the standings
   and behind their name on their own page, unlocked by correct
   Døgnerquiz answers (see api/src/Avatar/BannerService.php, which holds
   the unlock order, and ui/assets/banners.js - keep all three in sync).
   Pure CSS: each .banner-<slug> sets --banner-bg (any background layer
   list) and, for the framed ones, --banner-frame (a gradient drawn as
   the border by .has-banner::before). Every background stays light, so
   the site's dark text stays readable on top; the dashboard lays a dark
   veil over it for its own white text (.dash-row.has-banner below).
   =================================================================== */
.loc-station-row.has-banner,
.dash-row.has-banner,
.banner-surface.has-banner {
    position: relative;
    isolation: isolate;
    background: var(--banner-bg);
}

/* The frame: a gradient ring exactly the element's own rounded shape -
   a gradient background masked down to just its padding, the usual
   rounded gradient-border technique (a plain border-image would ignore
   border-radius). */
.has-banner::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: inherit;
    padding: var(--banner-frame-width, 3px);
    background: var(--banner-frame, none);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
    pointer-events: none;
}

/* On the always-dark dashboard: a dark veil over the banner so its
   white text stays readable. */
.dash-row.has-banner {
    background: linear-gradient(rgba(11, 10, 36, 0.6), rgba(11, 10, 36, 0.6)), var(--banner-bg);
}

/* The medals keep more of their shine under a lighter veil - their white
   text still reads on the darker metal tones. */
.dash-row.has-banner.banner-bronze,
.dash-row.has-banner.banner-silver,
.dash-row.has-banner.banner-gold {
    background: linear-gradient(rgba(11, 10, 36, 0.42), rgba(11, 10, 36, 0.42)), var(--banner-bg);
}

/* --- 1-3: plain, muted colours --- */
.banner-stone { --banner-bg: #e7e5e4; }
.banner-sand { --banner-bg: #efe6d4; }
.banner-mist { --banner-bg: #e2e8f0; }
.banner-sage { --banner-bg: #dfe9df; }

/* --- 4-10: lively colours with a decorative frame --- */
.banner-sunset { --banner-bg: linear-gradient(90deg, #ffe3c2, #ffd0dc); --banner-frame: linear-gradient(90deg, #ff8a00, #ff2d6f); }
.banner-ocean { --banner-bg: linear-gradient(90deg, #d3efff, #d5e0ff); --banner-frame: linear-gradient(90deg, #0077ff, #00c2ff); }
.banner-forest { --banner-bg: linear-gradient(90deg, #dcf5d3, #cdeed6); --banner-frame: linear-gradient(90deg, #1f9d55, #84cc16); }
.banner-berry { --banner-bg: linear-gradient(90deg, #f3dcff, #ffd6ee); --banner-frame: linear-gradient(90deg, #9b2fff, #ff3fa4); }
.banner-lemon { --banner-bg: linear-gradient(90deg, #fff7c2, #f0ffc2); --banner-frame: linear-gradient(90deg, #facc15, #a3e635); }
.banner-rainbow {
    --banner-bg: linear-gradient(90deg, #ffe0e0, #fff1d6, #fffbd1, #e0ffe4, #dff1ff, #ece0ff);
    --banner-frame: linear-gradient(90deg, #ff3b3b, #ff9f1a, #ffe11a, #2fd655, #1a9fff, #9b5cff);
}
.banner-neon { --banner-bg: linear-gradient(90deg, #e3ffef, #e0f6ff); --banner-frame: linear-gradient(90deg, #00e68a, #00b3ff, #00e68a); }
.banner-flame { --banner-bg: linear-gradient(90deg, #fff0d6, #ffdcd0); --banner-frame: linear-gradient(90deg, #ff3d00, #ff9100, #ffd000); }

/* --- 11+: simple patterns, kept faint so text always reads --- */
.banner-stripes { --banner-bg: repeating-linear-gradient(45deg, #eef2ff 0 12px, #e0e7ff 12px 24px); }
.banner-dots { --banner-bg: radial-gradient(#c7d2fe 2px, transparent 2.5px) 0 0 / 16px 16px, #f5f7ff; }
.banner-waves {
    --banner-bg: radial-gradient(circle at 50% 0, transparent 8px, #bbf7d0 9px, #bbf7d0 10px, transparent 11px) 0 0 / 22px 14px, #f0fdf4;
}
.banner-checks {
    --banner-bg: conic-gradient(#fde2cf 25%, #fff7f0 0 50%, #fde2cf 0 75%, #fff7f0 0) 0 0 / 24px 24px;
}
.banner-diamonds {
    --banner-bg:
        linear-gradient(135deg, #f5d0fe 25%, transparent 25%) -12px 0 / 24px 24px,
        linear-gradient(225deg, #f5d0fe 25%, transparent 25%) -12px 0 / 24px 24px,
        linear-gradient(315deg, #f5d0fe 25%, transparent 25%) 0 0 / 24px 24px,
        linear-gradient(45deg, #f5d0fe 25%, transparent 25%) 0 0 / 24px 24px,
        #fdf4ff;
}
.banner-confetti {
    --banner-bg:
        radial-gradient(circle, #fda4af 2px, transparent 2.5px) 0 0 / 34px 34px,
        radial-gradient(circle, #93c5fd 2px, transparent 2.5px) 11px 17px / 34px 34px,
        radial-gradient(circle, #fcd34d 2px, transparent 2.5px) 23px 6px / 34px 34px,
        radial-gradient(circle, #86efac 2px, transparent 2.5px) 6px 26px / 34px 34px,
        #fffdf7;
}
.banner-zigzag {
    --banner-bg:
        linear-gradient(135deg, #fde68a 25%, transparent 25%) -10px 0 / 20px 20px,
        linear-gradient(225deg, #fde68a 25%, transparent 25%) -10px 0 / 20px 20px,
        #fffbeb;
}
.banner-bubbles {
    --banner-bg:
        radial-gradient(circle, transparent 6px, #bfdbfe 7px, transparent 8px) 0 0 / 30px 30px,
        radial-gradient(circle, transparent 3px, #bfdbfe 4px, transparent 5px) 15px 15px / 30px 30px,
        #f0f7ff;
}
.banner-stars {
    --banner-bg:
        radial-gradient(#facc15 1.5px, transparent 2px) 0 0 / 22px 22px,
        radial-gradient(#fde68a 1px, transparent 1.5px) 11px 11px / 22px 22px,
        #fefce8;
}

/* --- 15, 18, 20: the medals - each with its own special frame --- */
.banner-bronze {
    --banner-bg:
        repeating-linear-gradient(115deg, rgba(255, 255, 255, 0.25) 0 2px, transparent 2px 14px),
        linear-gradient(135deg, #f6e1cb, #e9c29c 55%, #f3d5b5);
    --banner-frame: linear-gradient(135deg, #6e3f17, #e8a96f, #8c5523, #f4c99a, #6e3f17);
    --banner-frame-width: 5px;
    box-shadow: inset 0 0 0 7px rgba(255, 236, 214, 0.6), 0 2px 8px rgba(140, 85, 35, 0.35);
}
.banner-silver {
    --banner-bg:
        repeating-linear-gradient(115deg, rgba(255, 255, 255, 0.45) 0 2px, transparent 2px 14px),
        linear-gradient(135deg, #f4f6f9, #d9dfe7 55%, #eef1f5);
    --banner-frame: linear-gradient(135deg, #7d8894, #ffffff, #9aa5b1, #f7f9fb, #7d8894);
    --banner-frame-width: 5px;
    box-shadow: inset 0 0 0 7px rgba(255, 255, 255, 0.7), 0 2px 10px rgba(110, 125, 140, 0.4);
}
.banner-gold {
    --banner-bg:
        radial-gradient(circle, rgba(255, 255, 255, 0.9) 1px, transparent 1.6px) 0 0 / 18px 18px,
        linear-gradient(135deg, #fff7d6, #ffe28a 50%, #fff1b8);
    --banner-frame: linear-gradient(135deg, #8a6100, #ffd700, #fff6b0, #b8860b, #ffe066, #8a6100);
    --banner-frame-width: 6px;
    box-shadow: inset 0 0 0 8px rgba(255, 248, 210, 0.7), 0 0 14px rgba(255, 196, 0, 0.55);
}

/* The medals glint (on request): a light sweep across the row, and a
   few small sparkles twinkling on one after another. All of it is one
   ::after layer list - the sweep (layer 1) moves via background-position,
   each sparkle (layers 2-4) twinkles via its own background-size going
   0 -> visible -> 0 - so a single animation drives everything. Each
   medal runs at its own pace (and Gold has its own brighter sparkles),
   so three medal rows above each other never blink in step. */
.banner-bronze::after,
.banner-silver::after,
.banner-gold::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: inherit;
    background:
        linear-gradient(110deg, transparent 35%, var(--medal-sweep, rgba(255, 255, 255, 0.55)) 50%, transparent 65%),
        radial-gradient(circle, #fff 0 16%, var(--medal-sparkle, rgba(255, 255, 255, 0.75)) 28%, transparent 62%),
        radial-gradient(circle, #fff 0 16%, var(--medal-sparkle, rgba(255, 255, 255, 0.75)) 28%, transparent 62%),
        radial-gradient(circle, #fff 0 16%, var(--medal-sparkle, rgba(255, 255, 255, 0.75)) 28%, transparent 62%);
    background-repeat: no-repeat;
    background-position: -150% 0, 14% 28%, 57% 72%, 84% 24%;
    background-size: 60% 100%, 0 0, 0 0, 0 0;
    animation: banner-medal-glint var(--medal-pace, 5s) ease-in-out infinite;
    pointer-events: none;
}

.banner-bronze { --medal-pace: 6.5s; --medal-sweep: rgba(255, 236, 214, 0.5); --medal-sparkle: rgba(255, 214, 170, 0.8); }
.banner-silver { --medal-pace: 5.5s; --medal-sweep: rgba(255, 255, 255, 0.65); --medal-sparkle: rgba(225, 235, 255, 0.85); }
.banner-gold { --medal-pace: 4s; --medal-sweep: rgba(255, 255, 255, 0.7); --medal-sparkle: rgba(255, 240, 150, 0.95); }

.banner-silver::after { animation-delay: -1.7s; }
.banner-gold::after { animation-delay: -0.6s; }

@keyframes banner-medal-glint {
    0% { background-position: -150% 0, 14% 28%, 57% 72%, 84% 24%; background-size: 60% 100%, 0 0, 0 0, 0 0; }
    8% { background-size: 60% 100%, 22px 22px, 0 0, 0 0; }
    16% { background-size: 60% 100%, 0 0, 0 0, 0 0; }
    22% { background-size: 60% 100%, 0 0, 26px 26px, 0 0; }
    30% { background-size: 60% 100%, 0 0, 0 0, 0 0; }
    36% { background-size: 60% 100%, 0 0, 0 0, 20px 20px; }
    44% { background-position: -150% 0, 14% 28%, 57% 72%, 84% 24%; background-size: 60% 100%, 0 0, 0 0, 0 0; }
    100% { background-position: 250% 0, 14% 28%, 57% 72%, 84% 24%; background-size: 60% 100%, 0 0, 0 0, 0 0; }
}

/* Gold also breathes a soft glow, on top of its own sparkles. */
.loc-station-row.banner-gold,
.dash-row.banner-gold,
.banner-surface.banner-gold {
    animation: banner-gold-glow 4s ease-in-out infinite;
}

@keyframes banner-gold-glow {
    0%, 100% { box-shadow: inset 0 0 0 8px rgba(255, 248, 210, 0.7), 0 0 10px rgba(255, 196, 0, 0.4); }
    50% { box-shadow: inset 0 0 0 8px rgba(255, 248, 210, 0.7), 0 0 20px rgba(255, 196, 0, 0.75); }
}

@media (prefers-reduced-motion: reduce) {
    .banner-bronze::after,
    .banner-silver::after,
    .banner-gold::after {
        display: none;
    }

    .loc-station-row.banner-gold,
    .dash-row.banner-gold,
    .banner-surface.banner-gold {
        animation: none;
    }
}

/* Keep a row's own content above the frame and the medals' glint. */
.has-banner > * {
    position: relative;
    z-index: 2;
}

/* "Dine baggrunde" on a participant's own page - see renderBannerPicker()
   in ui/location/participant/index.php. */
.banner-picker-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 0.6rem;
    margin-top: 0.9rem;
}

.banner-swatch {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.4rem;
    min-height: 3rem;
    padding: 0.4rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: #16181d;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 700;
    text-align: left;
    cursor: pointer;
}

.banner-swatch.selected {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}

.banner-swatch-check {
    flex-shrink: 0;
    color: var(--accent);
    font-size: 1rem;
}

/* Every banner is light, so whatever sits on one gets the light theme's
   own dark text colours - even in dark mode, where the page's own text
   is light and would vanish on a pastel banner. (Not .dash-row - the
   dashboard veils its banners dark instead, see above.) */
.loc-station-row.has-banner,
.banner-surface.has-banner {
    --text: #16181d;
    --text-muted: #4b5563;
    color: #16181d;
}

.banner-swatch:not(.has-banner) {
    color: var(--text);
}

/* The webmaster gallery (ui/control/gallery/) - each banner as a light
   standings row and as a dark dashboard row. */
.gallery-tier-heading {
    margin: 1.5rem 0 0.5rem;
    font-weight: 800;
}

.gallery-banner {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.4rem 1rem;
    align-items: center;
    margin-bottom: 0.9rem;
}

.gallery-banner-caption {
    grid-column: 1 / -1;
    margin: 0;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text-muted);
}

.gallery-banner .loc-station-row {
    margin-bottom: 0;
}

/* The dashboard's own dark backdrop, scaled down - .dash-row's real
   sizes are for a 1080p TV. */
.gallery-dark {
    padding: 0.5rem;
    border-radius: var(--radius-sm);
    background: #0b0a24;
    color: #ffffff;
}

.gallery-dark .dash-row {
    grid-template-columns: 2rem 2.2rem 1fr auto;
    gap: 0.6rem;
    min-height: 0;
    padding: 0.35rem 0.6rem;
}

.gallery-dark .dash-rank {
    width: 1.8rem;
    height: 1.8rem;
    font-size: 0.9rem;
}

.gallery-dark .dash-row-avatar {
    width: 2.2rem;
    height: 2.2rem;
}

.gallery-dark .dash-row-name {
    font-size: 1rem;
}

.gallery-dark .dash-row-value {
    font-size: 1.1rem;
}

@media (max-width: 720px) {
    .gallery-banner {
        grid-template-columns: 1fr;
    }
}

/* The short TV-dashboard link on control/location-edit: the link itself
   in big monospace (it's meant to be read off a laptop and typed into a
   TV), with its copy button beside it. */
.dashboard-link-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0.75rem 0;
}

.dashboard-link-row input[type="text"] {
    flex: 1;
    font-family: 'JetBrains Mono', monospace;
    font-size: 1.15rem;
}

.dashboard-link-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* "Vis koder" on control/participant-edit - one participant's codes,
   unmasked, for an owner checking for made-up ones. A code only this
   participant has gets a warning tint: worth a second look, not proof. */
.participant-codes-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
}

.participant-codes-list {
    list-style: none;
    margin: 0.75rem 0 0;
    padding: 0;
}

.participant-code-row {
    display: grid;
    grid-template-columns: minmax(6.5rem, auto) auto 1fr auto;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem 0.6rem;
    border-bottom: 1px solid var(--border);
    font-size: 0.9rem;
}

.participant-code-row.only-one {
    background: var(--warning-bg);
}

.participant-code-value {
    font-family: 'JetBrains Mono', monospace;
    font-weight: 700;
}

.participant-code-info,
.participant-code-found-by {
    color: var(--text-muted);
    font-size: 0.8rem;
}

/* The home button on "Min side" and /log/add-jid - a house icon right of
   the JID badge, same height, back to the location's page (on request). */
.loc-home-button {
    display: inline-grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border-radius: var(--radius-sm);
    background: var(--accent-bg);
    color: var(--accent);
    transition: background 0.15s ease, transform 0.15s ease;
}

.loc-home-button:hover {
    background: color-mix(in srgb, var(--accent-bg) 70%, var(--accent) 30%);
    transform: translateY(-1px);
}

.loc-home-button:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* The quiz card's inline sign-up, for a logged-out visitor who picks an
   answer (see buildQuizSignupForm() in ui/location/index.php). */
.loc-quiz-signup {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 0.5rem;
    margin-top: 0.9rem;
}

.loc-quiz-signup-prompt,
.loc-quiz-signup .message {
    grid-column: 1 / -1;
    margin: 0;
    font-weight: 600;
}

.loc-quiz-signup input[type="text"] {
    grid-column: 1;
}

/* input's own display: block beats [hidden] - see CLAUDE.md's CSS gotchas. */
.loc-quiz-signup input[hidden] {
    display: none;
}

.loc-quiz-signup button[type="submit"] {
    grid-column: 2;
    grid-row: 2;
    margin-top: 0;
}

.loc-quiz-new-pincode {
    margin-top: 0.75rem;
}

/* The verification shield next to a JID code (ui/assets/verification.js):
   green = verified, red = checked and invalid, a faint dashed "ghost" =
   not checked yet. */
.verify-shield {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    cursor: help;
}

.verify-shield svg {
    display: block;
}

.verify-shield-mark {
    fill: none;
    stroke: #fff;
    stroke-width: 2.2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.verify-valid .verify-shield-body {
    fill: var(--success);
}

.verify-invalid .verify-shield-body {
    fill: var(--danger);
}

.verify-unverified .verify-shield-body {
    fill: none;
    stroke: var(--text-muted);
    stroke-width: 1.6;
    stroke-dasharray: 2.4 2;
}

.verify-unverified {
    opacity: 0.45;
}

/* The webmaster's verification key on the settings page. */
.verification-key {
    width: 100%;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.8rem;
    word-break: break-all;
    resize: none;
    padding: 0.6rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
}

.verification-key-actions {
    display: flex;
    gap: 0.5rem;
    margin-top: 0.75rem;
}

/* When a Døgnerquiz question was open, next to its heading on
   /location/24h-quiz, and which question the standings count up to. */
.loc-quiz-history-window {
    text-transform: none;
    letter-spacing: normal;
    font-weight: 500;
    font-size: 0.85em;
    color: var(--text-muted);
}

.quiz-standings-through {
    margin: 0.25rem 0 0.75rem;
}

/* "Slet kode" next to each code in an owner's code list - under the
   "found by" count, so the code's own details keep their width. */
.participant-code-side {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.3rem;
}

.participant-code-delete {
    font-size: 0.8rem;
    padding: 0.2rem 0.6rem;
}

/* Verified/invalid/not checked, in percent, under each participant on
   control/location-edit. */
.loc-station-verify {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin: 0.15rem 0 0;
    font-size: 0.78rem;
    font-weight: 600;
}

.loc-station-verify[hidden] {
    display: none;
}

.verify-text-valid {
    color: var(--success);
}

.verify-text-invalid {
    color: var(--danger);
}

.verify-text-unverified {
    color: var(--text-muted);
}

/* An earlier year's page: which year it is, next to the code badge. */
.loc-year-badge {
    display: inline-flex;
    align-items: center;
    height: 44px;
    padding: 0 0.85rem;
    border-radius: var(--radius-sm);
    background: var(--accent-soft, color-mix(in srgb, var(--accent) 14%, transparent));
    color: var(--accent);
    font-size: 1.1rem;
    font-weight: 700;
}
.loc-year-badge[hidden] { display: none; }

/* The same location's other years - one chip per year. */
.loc-other-years {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.75rem;
}
.loc-other-year {
    display: inline-flex;
    align-items: baseline;
    gap: 0.45rem;
    padding: 0.4rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    color: var(--text);
    text-decoration: none;
}
.loc-other-year:hover { border-color: var(--accent); color: var(--accent); }
.loc-other-year-year { font-weight: 700; }
.loc-other-year-code {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.8rem;
    color: var(--text-muted);
}

/* "58 verificeret" under a location card's codes/countries/continents. */
.location-card-stat-verified {
    margin: 0.2rem 0 0;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--success);
}
