/* Rail Pics — one stylesheet, no build step.
 *
 * Structure: tokens, reset, layout, then components in the order a page uses
 * them. Every colour comes from a custom property so light and dark are the
 * same stylesheet: the palette is defined once for light, overridden once for
 * dark, and the theme is chosen by the OS unless the visitor picks one (in
 * which case site.js writes data-theme on <html>).
 *
 * Conventions:
 * - Component classes are flat and reusable (.btn, .card, .chip, .field).
 *   Prefer composing them in templates over writing page-specific CSS.
 * - Mobile first: base rules are the small-screen layout, media queries add
 *   space. Nothing may scroll horizontally.
 * - Spacing/radius/shadow only from the scale below, so pages stay consistent.
 */

/* ---------------------------------------------------------------- tokens -- */

:root {
    /* Palette — light. Dark overrides only these, further down. */
    --bg:            #f7f7f5;
    --bg-raised:     #ffffff;
    --bg-glass:      rgba(251, 251, 249, 0.8);
    /* Glass INSIDE glass (issue #113). An element with backdrop-filter is a
     * backdrop root for its descendants, so a panel inside the header blurs
     * the header's own paint and nothing else -- the page behind it comes
     * through raw, which is why the dropdowns read worse than the map panel
     * even though every value is identical. They cannot blur, so they carry
     * more of the colour instead. */
    --bg-glass-nested: rgba(251, 251, 249, 0.95);
    --bg-sunken:     #ececea;
    --bg-inverse:    #16181d;
    --text:          #16181d;
    --text-muted:    #63676f;
    --text-inverse:  #f5f6f8;
    --border:        #dededa;
    --border-strong: #c3c3be;
    /* SBB red (issue #189, the owner's choice). #eb0000 is the railway's own
     * value and it clears 4.5:1 on this background, so it can carry link
     * text and not only fills. Dark mode lightens it -- the same red on
     * near-black does not. */
    --accent:        #eb0000;
    --accent-text:   #ffffff;
    --accent-soft:   rgba(235, 0, 0, 0.09);
    --map-cluster-small-halo:  rgba(181, 226, 140, 0.6);
    --map-cluster-small:       rgba(110, 204, 57, 0.6);
    --map-cluster-medium-halo: rgba(241, 211, 87, 0.6);
    --map-cluster-medium:      rgba(240, 194, 12, 0.6);
    --map-cluster-large-halo:  rgba(253, 156, 115, 0.6);
    --map-cluster-large:       rgba(241, 128, 23, 0.6);
    --map-cluster-text:        #16181d;
    /* The warm second accent (issue #65: "bring some colours"): signal
     * orange, the same family as the sun tool's lines, used for small
     * highlights -- never for whole surfaces. */
    --accent-2:      #d97a1a;
    --accent-2-soft: rgba(217, 122, 26, 0.12);
    --weather-sun:   #f3b32a;
    --weather-cloud: #9cb7d6;
    --weather-high-cloud: #74839c;
    --weather-rain:  #1d5fa4;
    --weather-snow:  #4e91bd;
    --weather-storm: #7557b7;
    --weather-night: #183153;
    --weather-lightning: #f3b32a;
    --weather-fog:   #7b818a;
    --danger:        #b3261e;
    --shadow-sm:     0 1px 2px rgba(16, 18, 22, 0.06), 0 1px 3px rgba(16, 18, 22, 0.08);
    --shadow-md:     0 2px 6px rgba(16, 18, 22, 0.08), 0 8px 24px rgba(16, 18, 22, 0.10);
    --shadow-lg:     0 8px 20px rgba(16, 18, 22, 0.12), 0 24px 48px rgba(16, 18, 22, 0.16);
    /* Photos sit on a deliberately dark tile in both themes: a bright frame
     * around a photograph shifts how its own colours read. */
    --photo-mat:     #22242a;

    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 24px;
    --space-6: 32px;
    --space-7: 48px;
    --space-8: 64px;

    /* Corners, after issue #189: "nicht mehr alles so abgerundet, bzw
     * einfach nur noch dort wo es Sinn macht."
     *
     * Surfaces -- cards, panels, sheets, photos -- are square. What stays
     * round is what rounding says something about: a control you press
     * (2px, enough to read as a button and not as a box), and the pills
     * that are pills because they are pills. */
    --radius-sm: 2px;
    --radius-md: 0px;
    --radius-lg: 0px;
    --radius-pill: 999px;

    --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --header-height: 60px;
    --content-width: 1180px;
    --prose-width: 720px;
    /* A Reisebericht's own measure (issue #276, the owner: "Zudem sind die
     * Textblöcke nicht lange genug"). `none` means the card it sits in is the
     * measure, which is the editor's textarea width -- see the
     * `.report-page .report-text` rule for the arithmetic, the character count
     * and how to dial it back. Kept apart from --prose-width because that one
     * is shared with thirteen other places and none of them asked to change.
     *
     * Width tokens live in this block only, like the two above: they do not
     * change with the theme, so the dark blocks have nothing to say about
     * them. */
    --report-measure: none;

    /* The map's own credit line (issue #334). Theme-independent on purpose,
     * like the width tokens above and for a reason of its own: Google
     * renders its copyright row itself and offers no way to theme it, so a
     * credit bar of ours that turned dark sat beside a light one that could
     * not. These three are Google's treatment -- a translucent white plate,
     * dark grey lettering, a slightly darker grey for the links -- and they
     * are deliberately absent from the two dark blocks below. */
    --map-credit-bg: rgba(255, 255, 255, 0.75);
    --map-credit-text: #3c4043;
    --map-credit-link: #202124;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg:            #0e1014;
        --bg-raised:     #171a20;
        --bg-glass:      rgba(18, 21, 27, 0.82);
        --bg-glass-nested: rgba(20, 23, 30, 0.96);
        --bg-sunken:     #090a0d;
        --bg-inverse:    #f5f6f8;
        --text:          #f2f4f7;
        --text-muted:    #98a0ad;
        --text-inverse:  #16181d;
        --border:        #262a33;
        --border-strong: #39404c;
        --accent:        #ff5f52;
        --accent-text:   #1a0503;
        --accent-soft:   rgba(255, 95, 82, 0.16);
        --map-cluster-small-halo:  rgba(181, 226, 140, 0.6);
        --map-cluster-small:       rgba(110, 204, 57, 0.6);
        --map-cluster-medium-halo: rgba(241, 211, 87, 0.6);
        --map-cluster-medium:      rgba(240, 194, 12, 0.6);
        --map-cluster-large-halo:  rgba(253, 156, 115, 0.6);
        --map-cluster-large:       rgba(241, 128, 23, 0.6);
        --map-cluster-text:        #16181d;
        --accent-2:      #f5993d;
        --accent-2-soft: rgba(245, 153, 61, 0.16);
        --weather-sun:   #ffd43b;
        --weather-cloud: #aec9ed;
        --weather-high-cloud: #6f7d8f;
        --weather-rain:  #3f7de0;
        --weather-snow:  #a7d8f5;
        --weather-storm: #bda4ff;
        --weather-night: #b8cbff;
        --weather-lightning: #ffd43b;
        --weather-fog:   #929ba8;
        --danger:        #ff6b6b;
        --shadow-sm:     0 1px 2px rgba(0, 0, 0, 0.5);
        --shadow-md:     0 2px 8px rgba(0, 0, 0, 0.55);
        --shadow-lg:     0 12px 32px rgba(0, 0, 0, 0.6);
    }
}

/* Explicit choice wins over the OS setting, in both directions. */
:root[data-theme="dark"] {
    --bg:            #0e1014;
    --bg-raised:     #171a20;
    --bg-glass:      rgba(18, 21, 27, 0.82);
    --bg-glass-nested: rgba(20, 23, 30, 0.96);
    --bg-sunken:     #090a0d;
    --bg-inverse:    #f5f6f8;
    --text:          #f2f4f7;
    --text-muted:    #98a0ad;
    --text-inverse:  #16181d;
    --border:        #262a33;
    --border-strong: #39404c;
    --accent:        #ff5f52;
    --accent-text:   #1a0503;
    --accent-soft:   rgba(255, 95, 82, 0.16);
    --map-cluster-small-halo:  rgba(181, 226, 140, 0.6);
    --map-cluster-small:       rgba(110, 204, 57, 0.6);
    --map-cluster-medium-halo: rgba(241, 211, 87, 0.6);
    --map-cluster-medium:      rgba(240, 194, 12, 0.6);
    --map-cluster-large-halo:  rgba(253, 156, 115, 0.6);
    --map-cluster-large:       rgba(241, 128, 23, 0.6);
    --map-cluster-text:        #16181d;
    --accent-2:      #f5993d;
    --accent-2-soft: rgba(245, 153, 61, 0.16);
    --weather-sun:   #ffd43b;
    --weather-cloud: #aec9ed;
    --weather-high-cloud: #6f7d8f;
    --weather-rain:  #3f7de0;
    --weather-snow:  #a7d8f5;
    --weather-storm: #bda4ff;
    --weather-night: #b8cbff;
    --weather-lightning: #ffd43b;
    --weather-fog:   #929ba8;
    --danger:        #ff6b6b;
    --shadow-sm:     0 1px 2px rgba(0, 0, 0, 0.5);
    --shadow-md:     0 2px 8px rgba(0, 0, 0, 0.55);
    --shadow-lg:     0 12px 32px rgba(0, 0, 0, 0.6);
}

/* ----------------------------------------------------------------- reset -- */

*,
*::before,
*::after { box-sizing: border-box; }

html {
    -webkit-text-size-adjust: 100%;
    /* Always reserve the scrollbar's width, whether or not this page needs
     * one (issue #20). Without it a short page and a long one are laid out in
     * viewports 15px apart, so moving between them shifts and slightly
     * narrows the content -- measured on the homepage: page 1 fits a tall
     * screen and does not scroll, page 2 does, and everything jumped left by
     * exactly the scrollbar width. Browsers that do not know the property
     * simply behave as before. */
    scrollbar-gutter: stable;
    /* Every fragment lands clear of the sticky header (issue #355). A save
     * in the photospot editor redirects to ?bearbeiten=1#anzeigename, and
     * without this the title field, its Speichern and its close link all
     * scrolled under the header: the editor looked like a page with no way
     * out. One property on the root scroller, instead of a scroll-margin
     * per anchor (the photo editor's fields keep theirs, which is larger
     * on purpose). */
    scroll-padding-top: calc(var(--header-height) + var(--space-4));
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font);
    font-size: 16px;
    line-height: 1.55;
    /* Nothing may cause a horizontal scrollbar. */
    overflow-x: hidden;

    /* The footer stays at the bottom of the window on a short page instead of
     * riding up under the content. dvh, not vh, so a phone's collapsing
     * address bar does not leave a gap. */
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
}

/* Takes the leftover height, which is what pushes the footer down. */
body > main {
    flex: 1 0 auto;
}

body > .site-footer {
    flex-shrink: 0;
}

img,
picture,
video { max-width: 100%; }

/* Photos carry intrinsic width/height ATTRIBUTES since issue #235, so the
 * browser can reserve the right box before the file arrives. Those
 * attributes map to CSS presentational hints -- and that is the trap: an
 * author rule saying `width: 100%` overrides the width hint, but nothing
 * was overriding the HEIGHT hint, so every card kept the pixel height of
 * the original photo and the gallery grew to thousands of pixels a row.
 * Found live on 2026-08-27, one day after the attributes shipped.
 *
 * `height: auto` is the default every sized image wants: the CSS decides
 * the box (aspect-ratio, max-height, object-fit) and the attribute is left
 * to do the one job it is for. A rule that genuinely wants a fixed height
 * still beats this one on specificity. */
img { height: auto; }

/* Bigger, tighter, with clearer steps between the levels (issue #189). The
 * character of this site is meant to come from its typography and its grid
 * rather than from decoration -- which is the tradition Swiss railway
 * graphics sit in anyway. */
h1, h2, h3, h4, h5 {
    margin: 0 0 var(--space-3);
    line-height: 1.18;
    font-weight: 700;
    letter-spacing: -0.022em;
    text-wrap: balance;
}

h1 { font-size: clamp(1.75rem, 1.3rem + 2vw, 2.75rem); }
h2 { font-size: clamp(1.35rem, 1.15rem + 1vw, 1.75rem); }
h3 { font-size: 1.15rem; letter-spacing: -0.015em; }
h4, h5 { letter-spacing: -0.01em; }

p { margin: 0 0 var(--space-3); }

a {
    color: inherit;
    text-decoration: none;
}

a:hover { text-decoration: underline; }

:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* Respect the OS "reduce motion" setting rather than animating regardless. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
    }
}

/* Visible only to screen readers — skip links, icon-button labels. */
.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.skip-link {
    position: absolute;
    left: var(--space-3);
    top: -100px;
    z-index: 100;
    padding: var(--space-2) var(--space-4);
    background: var(--accent);
    color: var(--accent-text);
    border-radius: var(--radius-sm);
    transition: top 0.15s ease;
}

.skip-link:focus { top: var(--space-3); }

/* ---------------------------------------------------------------- layout -- */

.page {
    width: 100%;
    max-width: var(--content-width);
    margin: 0 auto;
    padding: var(--space-5) var(--space-4) var(--space-8);
}

/* Two page widths, and they stay. Issue #177 asked for one -- everything at
 * the wide column, with prose capping itself inside it -- and the owner's
 * verdict on seeing it was "das hat nun alles verschlimmert". Reverted the
 * same day. Forms, prose and single cards read better in a narrow column
 * than they do stretched across 1180px, whatever the theory says. */
.page--narrow { max-width: var(--prose-width); }
.page--flush  { padding-left: 0; padding-right: 0; }

@media (min-width: 720px) {
    .page { padding: var(--space-6) var(--space-6) var(--space-8); }
}

/* A heading, its line of explanation, and a rule under both (issue #189).
 * The rule is what turns a heading into a section instead of a floating
 * sentence, and it costs one border rather than a shadow. */
.page-header {
    margin-bottom: var(--space-6);
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--border);
}

.page-header p {
    color: var(--text-muted);
    margin: 0;
    max-width: var(--prose-width);
}

/* A caption, not a headline. Used where the heading names the thing above it
 * rather than announcing a page (issue #189): a photo title at the full
 * heading size competes with the photograph. */
.page-header--caption h1 {
    font-size: clamp(1.25rem, 1.05rem + 0.8vw, 1.6rem);
    letter-spacing: -0.015em;
}

.stack     { display: flex; flex-direction: column; gap: var(--space-4); }
.row       { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.row--tight{ gap: var(--space-2); }
.push      { margin-left: auto; }

.muted { color: var(--text-muted); }

.text-sm { font-size: 0.875rem; }

.divider {
    height: 1px;
    border: 0;
    margin: var(--space-6) 0;
    background: var(--border);
}

/* ---------------------------------------------------------------- header -- */

.site-header {
    position: sticky;
    top: 0;
    /* Above everything the map floats (issue #117): its sidebar sits at
     * 1100 and Leaflet's controls at 1000, and because the header is its
     * own stacking context, its dropdowns can only be as high as it is --
     * they were opening UNDER the map panel. */
    z-index: 1200;
    /* The same liquid glass as every floating surface (owner's call:
     * "identisch") -- one recipe, --bg-glass + blur(14) saturate(1.5). */
    background: var(--bg);
    background: var(--bg-glass);
    backdrop-filter: blur(14px) saturate(1.5);
    -webkit-backdrop-filter: blur(14px) saturate(1.5);
    border-bottom: 1px solid var(--border);
}

.site-header__inner {
    max-width: var(--content-width);
    margin: 0 auto;
    padding: 0 var(--space-4);
    height: var(--header-height);
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

@media (min-width: 720px) {
    .site-header__inner { padding: 0 var(--space-6); }
}

.logo {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: 1.15rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    white-space: nowrap;
    flex-shrink: 0;
}

.logo:hover { text-decoration: none; }

/* Search grows to fill the middle; it is the widest thing in the header. */
.header-search {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
}

.header-search input {
    width: 100%;
    min-width: 0;
    padding: var(--space-2) var(--space-4);
    /* The same liquid glass as every other translucent surface
     * (issue #69): the search field was the last solid one. */
    background: var(--bg-raised);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    font: inherit;
    font-size: 0.9rem;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .header-search input {
        background: var(--bg-glass-nested);
        -webkit-backdrop-filter: blur(14px) saturate(1.5);
        backdrop-filter: blur(14px) saturate(1.5);
    }
}

.header-search input::placeholder { color: var(--text-muted); }

/* Search and Filter have separate boxes. The map's one filter section lives
 * in this dropdown; shared menu behavior handles all header menus. */
.header-search input { border-radius: var(--radius-md); }
.header-filter { position: relative; flex: 0 0 auto; }
.header-filter .header-search__filter { border-radius: var(--radius-md); }
.header-filter .map-filter-dropdown {
    width: calc(var(--space-8) * 6);
    max-width: calc(100vw - var(--space-5));
    min-width: 0;
    max-height: calc(100dvh - var(--header-height) - var(--space-5));
    padding: var(--space-4);
    overflow-y: auto;
    overscroll-behavior: contain;
    overflow-wrap: anywhere;
}
@media (max-width: 719px) {
    .header-filter .map-filter-dropdown {
        position: fixed;
        top: calc(var(--header-height) + var(--space-1));
        right: var(--space-3);
    }
}
@media (max-width: 419px) {
    .site-header__inner { gap: var(--space-2); }
    .header-search input { padding-inline: var(--space-2); }
    .header-filter .header-search__filter {
        padding-inline: var(--space-2);
        gap: var(--space-1);
    }
}
/* The same as every other field: the shared outline, nothing of its own. */

/* On phones the search field moves into the drawer, and the nav collapses. */
.header-nav {
    display: none;
    align-items: center;
    gap: var(--space-1);
}

.header-nav a,
.header-nav .menu__trigger {
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    font-size: 0.9rem;
    white-space: nowrap;
}

.header-nav a:hover,
.header-nav .menu__trigger:hover {
    background: var(--bg-sunken);
    text-decoration: none;
}

.header-nav a[aria-current="page"] {
    background: var(--accent-soft);
    color: var(--accent);
}

.nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px; height: 40px;
    flex-shrink: 0;
    background: none;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: inherit;
    cursor: pointer;
}

@media (min-width: 900px) {
    .header-nav { display: flex; }
    .nav-toggle { display: none; }
}

/* NO `overflow: hidden` here, and that is the whole point (issue #186).
 *
 * It used to be the scroll lock, and it is also what breaks `position:
 * sticky` on a descendant: with the overflow hidden the header stopped being
 * pinned the moment the drawer opened, fell back to its place at the top of
 * the DOCUMENT, and on a page scrolled 1900px down the whole menu opened
 * 1900px above the screen. It was open the entire time -- just up there.
 *
 * The lock lives in site.js now, which blocks the gestures outside the drawer
 * instead of freezing the scroller. That leaves the header sticky, so the
 * drawer opens where the reader is looking whatever the scroll position. */

/* Mobile drawer: the same links, stacked, revealed under the header. */
.drawer {
    display: none;
    border-bottom: 1px solid var(--border);
    background: var(--bg-raised);
    padding: var(--space-3) var(--space-4) var(--space-4);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .drawer {
        background: var(--bg-glass-nested);
        -webkit-backdrop-filter: blur(14px) saturate(1.5);
        backdrop-filter: blur(14px) saturate(1.5);
    }
}

.drawer.is-open {
    display: block;
    /* Taller than the screen (a signed-in photographer has every link) means
     * the drawer scrolls itself rather than being cut off. `contain` stops
     * that scroll chaining to the page when it reaches its end -- which is
     * the "don't scroll while touching the menu" half of the problem. */
    max-height: calc(100dvh - var(--header-height));
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}

@media (min-width: 900px) {
    .drawer, .drawer.is-open { display: none; }
}

.drawer a,
.drawer button {
    display: block;
    width: 100%;
    padding: var(--space-3) var(--space-2);
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.drawer a:hover,
.drawer button:hover { background: var(--bg-sunken); text-decoration: none; }

.drawer__group {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border);
}

.drawer__label {
    padding: 0 var(--space-2);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

/* ------------------------------------------------------------ menu (pop) -- */

.menu { position: relative; }

.menu__trigger {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    cursor: pointer;
}

/* The one-off gaps that used to be inline `style` attributes (issue #163).
 * Deliberately NOT a utility scale: four names for four distances that were
 * already in the markup, so the values are tokens and the markup can be
 * restyled. A fifth would be a sign that the component should carry its own
 * spacing instead. See docs/design.md. */
.spaced-above--sm { margin-top: var(--space-3); }
.spaced-above--md { margin-top: var(--space-4); }
.spaced-above--lg { margin-top: var(--space-5); }
.spaced-above--xl { margin-top: var(--space-6); }
.spaced-below--sm { margin-bottom: var(--space-3); }
.spaced-below--md { margin-bottom: var(--space-4); }
.spaced-below--lg { margin-bottom: var(--space-5); }
.spaced-below--xl { margin-bottom: var(--space-6); }

/* ONE panel recipe (issue #163). Everything that floats near the thing that
 * opened it -- the menus, the search suggestions, the autocomplete, the map's
 * controls -- is this, plus its own positioning. Four declarations copied into
 * five places is how the autocomplete ended up looking like a browser widget
 * instead of like this site. See docs/design.md. */
.panel,
.menu__panel,
.suggest__panel,
.header-search__panel {
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
}

.menu__panel {
    position: absolute;
    right: 0;
    top: calc(100% + var(--space-2));
    min-width: 220px;
    padding: var(--space-2);
    z-index: 950;
    /* A real dropdown, not "some hovering thing" (issue #65): it grows out
     * of its trigger with a quick fade-and-settle, Apple/Google style.
     * visibility instead of display so the transition can run. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-6px) scale(0.98);
    transform-origin: top right;
    transition: opacity 0.14s ease, transform 0.14s ease,
                visibility 0s linear 0.14s;
    pointer-events: none;
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .menu__panel {
        background: var(--bg-glass-nested);
        -webkit-backdrop-filter: blur(14px) saturate(1.5);
        backdrop-filter: blur(14px) saturate(1.5);
    }
}

.menu__panel.is-open {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity 0.14s ease, transform 0.14s ease;
    pointer-events: auto;
}

.menu__panel a,
.menu__panel button {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: inherit;
    font: inherit;
    font-size: 0.9rem;
    text-align: left;
    cursor: pointer;
}

.menu__panel a:hover,
.menu__panel button:hover { background: var(--bg-sunken); text-decoration: none; }

.menu__panel [aria-current="true"] { color: var(--accent); font-weight: 600; }

.menu__sep {
    height: 1px;
    margin: var(--space-2) 0;
    /* The text's own colour, faded just enough to stay a rule and not a
     * line of type (issue #71: "same color as the text"). */
    background: currentColor;
    opacity: 0.35;
}

/* The browser's own `[hidden] { display: none }` is a plain type-less rule, so
 * any component that sets `display` beats it and the attribute quietly does
 * nothing. `.action-bar` (flex) and `.btn` (inline-flex) both did exactly that:
 * the upload page's action bar was never hidden when it had nothing to act on.
 * This is the one place !important is the right tool -- the whole point of the
 * attribute is that it wins. */
[hidden] { display: none !important; }

/* --------------------------------------------------------------- buttons -- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-5);
    background: var(--bg-raised);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    font: inherit;
    font-size: 0.9rem;
    font-weight: 550;
    line-height: 1.2;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}

.btn:hover {
    background: var(--bg-sunken);
    text-decoration: none;
}

.btn:active { transform: translateY(1px); }

.btn:disabled,
.btn[aria-disabled="true"] {
    opacity: 0.55;
    cursor: not-allowed;
}

.btn--primary {
    background: var(--accent);
    color: var(--accent-text);
    border-color: transparent;
}

.btn--primary:hover { background: color-mix(in srgb, var(--accent) 85%, var(--text)); }

.btn--ghost {
    background: none;
    border-color: transparent;
}

.btn--ghost:hover { background: var(--bg-sunken); }

/* Outlined, never filled -- and with a red accent (issue #189) that stopped
 * being a matter of taste: a filled danger button and a filled primary button
 * would be the same button. */
.btn--danger { color: var(--danger); border-color: var(--danger); background: none; }
.btn--danger:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); }

.btn--sm { padding: var(--space-2) var(--space-3); font-size: 0.8rem; }
.btn--block { width: 100%; }

/* Round icon-only button, e.g. the delete cross on upload previews. */
.btn--icon {
    width: 32px; height: 32px;
    padding: 0;
    border-radius: var(--radius-pill);
    font-size: 1rem;
}

/* A close cross drawn from two bars instead of typeset as "×". The glyph sits
 * on the font's math axis, which is above the middle of the line box, so a
 * flex-centred × is always a little high in its circle however the box is
 * centred. Two rotated bars are centred by construction, in any font. */
.btn--close {
    position: relative;
    /* No text inside, but keep the accessible name on the element. */
    font-size: 0;
    line-height: 0;
}

.btn--close::before,
.btn--close::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 13px;
    height: 1.5px;
    border-radius: 1px;
    background: currentColor;
}

.btn--close::before { transform: translate(-50%, -50%) rotate(45deg); }
.btn--close::after { transform: translate(-50%, -50%) rotate(-45deg); }

/* Floating action bar, used by the upload page. */
.action-bar {
    position: sticky;
    bottom: 0;
    z-index: 500;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    justify-content: center;
    margin-top: var(--space-6);
    padding: var(--space-3);
    background: var(--bg);
    background: color-mix(in srgb, var(--bg) 92%, transparent);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border-top: 1px solid var(--border);
}

/* ----------------------------------------------------------------- chips -- */

.chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: 0 0 var(--space-4);
    padding: 0;
    list-style: none;
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-3);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    font-size: 0.8rem;
    color: var(--text-muted);
    white-space: nowrap;
}

a.chip:hover {
    color: var(--accent);
    border-color: var(--accent);
    background: var(--accent-soft);
    text-decoration: none;
}

.chip--accent { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }

/* One accent, not two (issue #189). The orange second accent beside the new
 * red read as a mistake rather than as a highlight; it keeps its token for
 * anything that genuinely wants a warm mark, but nothing in the chrome uses
 * it any more. */
.chip:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }

/* ----------------------------------------------------------------- cards -- */

.card {
    background: var(--bg-raised);
    border: 1px solid var(--border);
    /* --radius-md like every other container box (issue #56): corners that
     * agree are most of what "lined up" means. Controls stay --radius-sm,
     * chips stay pills. */
    border-radius: var(--radius-md);
    padding: var(--space-5);
}

.card--flush { padding: 0; overflow: hidden; }

/* ------------------------------------------------------------ photo grid -- */

/* One grid for every gallery on the site. auto-fill keeps cards a sane size
 * from phone to desktop without per-page breakpoints. */
.photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
    gap: var(--space-4);
}

.photo-grid--wide {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 380px), 1fr));
    gap: var(--space-4);
}

.photo-card {
    position: relative;
    display: block;
    overflow: hidden;
    border-radius: var(--radius-md);
    background: var(--photo-mat);
    box-shadow: var(--shadow-sm);
    transition: transform 0.18s ease, box-shadow 0.18s ease;
}

.photo-card:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow-md);
}

/* <picture> must not interrupt the card's sizing chain. */
.photo-card picture { display: contents; }

.photo-card img {
    display: block;
    width: 100%;
    aspect-ratio: 3 / 2;
    object-fit: cover;
}

.photo-card__body {
    position: absolute;
    inset: auto 0 0 0;
    padding: var(--space-5) var(--space-3) var(--space-3);
    color: #fff;
    /* Gradient rather than a flat panel: keeps more of the photo visible. */
    background: linear-gradient(to top, rgba(0, 0, 0, 0.78), rgba(0, 0, 0, 0.35) 55%, transparent);
}

.photo-card__title {
    margin: 0;
    font-size: 0.95rem;
    font-weight: 600;
    /* Two lines maximum, so cards in a row stay the same height. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* One big click target: a dedicated overlay anchor (see photo_card.html for
 * why not a stretched ::after). Real links inside the card sit above it. */
.photo-card__overlay {
    position: absolute;
    inset: 0;
    z-index: 1;
}

.photo-card__title a { position: relative; z-index: 2; }
.photo-card__title a:hover { text-decoration: none; }

.photo-card__meta {
    position: relative;
    z-index: 2;
    margin: var(--space-1) 0 0;
    font-size: 0.78rem;
    color: rgba(255, 255, 255, 0.82);
}

/* The photographer's name: a real link, above the stretched one. */
.photo-card__meta a {
    text-decoration: underline;
    text-underline-offset: 2px;
}

.photo-card__meta a:hover { color: #fff; }

/* Hero card — the featured slot on the homepage. */
.photo-card--hero img { aspect-ratio: 16 / 9; }

/* The two cards beside the hero live in a wrapper, which is one grid item.
 * Without a gap of its own they sat flush against each other while the hero
 * above them got the grid's gap -- the uneven spacing reported on mobile
 * (issue #31). Above 900px the wrapper becomes a column beside the hero and
 * the rule below adds the row sizing. */
.photo-grid--featured__side {
    display: grid;
    gap: var(--space-4);
    align-content: start;
}

@media (min-width: 900px) {
    .photo-grid--featured {
        grid-template-columns: 2fr 1fr;
        align-items: stretch;
    }
    .photo-grid--featured .photo-card--hero { height: 100%; }
    .photo-grid--featured .photo-card--hero img { height: 100%; aspect-ratio: auto; }
    .photo-grid--featured__side {
        grid-template-rows: 1fr 1fr;
        align-content: stretch;
    }
    .photo-grid--featured__side .photo-card,
    .photo-grid--featured__side .photo-card img { height: 100%; aspect-ratio: auto; }
}

/* The sort dropdown (issue #32, dropdown at the owner's request). Reuses the
 * generic menu contract; this only sizes it for page content rather than the
 * header, and gives the trigger a visible control shape -- a bare text
 * trigger reads as a label, not as something to click. */
.sort-menu { display: inline-block; min-width: 0; max-width: 100%; }

.sort-menu .menu__trigger {
    max-width: 100%;
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
    font-size: 0.85rem;
    text-align: left;
    overflow-wrap: anywhere;
    cursor: pointer;
}

.sort-menu .menu__trigger:hover { border-color: var(--border-strong); }

.sort-menu__label { color: var(--text-muted); }

.sort-menu .menu__panel {
    /* These controls sit at the row's right edge. Keep even the hidden
     * panel inside the page, and let longer translated choices wrap. */
    left: auto;
    right: 0;
    min-width: 100%;
    width: max-content;
    max-width: calc(100vw - var(--space-6));
    font-size: 0.9rem;
    text-align: left;
    overflow-wrap: anywhere;
}

.sort-menu .menu__panel a,
.sort-menu__current {
    display: block;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
}

.sort-menu .menu__panel a:hover { background: var(--bg-sunken); text-decoration: none; }

.sort-menu__current { font-weight: 650; background: var(--accent-soft); }

@media (max-width: 719px) {
    .sort-menu { display: flex; justify-content: flex-end; width: 100%; }
    .sort-menu .menu__panel { max-width: 100%; }
}

/* Homepage sections (issue #37): real air between the featured strip, the
 * newest-photos preview and the Reiseberichte. */

/* --------------------------------------------------------- section title -- */

.section-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    margin: var(--space-7) 0 var(--space-4);
}

.section-head:first-child { margin-top: 0; }

.section-head h2 { margin: 0; }

/* A short warm bar under every section title: rhythm and a dab of colour. */
.section-head h2::after {
    content: "";
    display: block;
    width: 36px;
    height: 3px;
    margin-top: var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--accent);
}

.section-head__hint {
    margin: 0;
    font-size: 0.85rem;
    color: var(--text-muted);
}

/* The page title, editable in place (issue #348). The input wears the
 * heading's own type so the words do not move when editing opens: same
 * size, same weight, same colour, and a hairline underneath instead of a
 * form field's box. The heading element stays -- the input sits inside it
 * -- so the page keeps exactly one h1 in both states. */
/* The in-place title editor (issue #348) takes the whole heading row, and
 * its close link sits beside Speichern INSIDE the form (issue #355). A save
 * redirects back into editing mode at #anzeigename; a close control that
 * wrapped under the row on its own, next to nothing, was never found. */
.title-edit {
    flex: 1 1 100%;
    min-width: 0;
}

.title-edit__form {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.title-edit__heading {
    flex: 1 1 16rem;
    margin: 0;
    min-width: 0;
}

.title-edit__input {
    width: 100%;
    font: inherit;
    color: inherit;
    letter-spacing: inherit;
    padding: 0 0 var(--space-1);
    border: 0;
    border-bottom: 2px solid var(--border-strong);
    border-radius: 0;
    background: transparent;
}

.title-edit__input:focus {
    outline: none;
    border-bottom-color: var(--accent);
}

.title-edit__hint {
    margin-top: var(--space-2);
    max-width: var(--prose-width);
}

/* ------------------------------------------------------------- photospot -- */

/* The spot's preview picture, under the heading (issue #335). The height is
 * capped because this is an identifier, not the gallery -- that is the grid
 * further down -- and `cover` therefore fills the box exactly, which is why
 * there is no mat behind it: the only thing a mat would sit under is the
 * caption, and a credit line on the photo stage's near-black plate reads
 * worse than one on the page. */
.spot-cover {
    margin: 0 0 var(--space-5);
    border-radius: var(--radius-md);
    overflow: hidden;
}

.spot-cover picture { display: contents; }

/* The picture's own link, and only that one: the child combinator keeps
 * this off the credit line's link in the caption, which a descendant
 * selector turned into a zero-height block (measured). */
.spot-cover > a {
    display: block;
    line-height: 0;
}

.spot-cover img {
    width: 100%;
    max-height: 420px;
    object-fit: cover;
    object-position: center;
}

.spot-cover__caption {
    margin: 0;
    padding: var(--space-2) 0 0;
    line-height: 1.4;
}

/* The photographers' own notes about the place (issue #340). Prose, so it
 * takes the shared measure rather than the full page width. */
.spot-notes {
    max-width: var(--prose-width);
    margin: 0 0 var(--space-5);
}

.spot-notes__title {
    margin: 0 0 var(--space-2);
    font-size: 1.05rem;
}

.spot-notes p { margin: 0 0 var(--space-3); }
.spot-notes p:last-child { margin-bottom: 0; }

/* ---------------------------------------------------------- photo detail -- */

.photo-stage {
    background: var(--photo-mat);
    border-radius: var(--radius-md);
    overflow: hidden;
    margin: 0 auto var(--space-5);
    /* Only as wide as the photo it holds (issue #121), so the mat is what
     * lies UNDER the image rather than beside it. */
    width: fit-content;
    max-width: 100%;
}

.photo-stage picture { display: contents; }

.photo-stage { position: relative; }

.photo-stage__download {
    position: absolute;
    right: var(--space-3);
    bottom: var(--space-3);
    opacity: 0;
    transition: opacity 0.15s ease;
}

.photo-stage:hover .photo-stage__download,
.photo-stage__download:focus-within,
.photo-stage__download:has(.is-open) {
    opacity: 1;
}

.photo-stage__download-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    border: 0;
    border-radius: var(--radius-pill);
    background: rgba(0, 0, 0, 0.65);
    color: #fff;
    font: inherit;
    font-size: 0.82rem;
    cursor: pointer;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}

.photo-stage__download-btn:hover { background: rgba(0, 0, 0, 0.85); }

/* The button sits at the image's bottom edge; the size list opens upwards. */
.menu__panel--up {
    top: auto;
    bottom: calc(100% + var(--space-2));
}

.menu__panel a .muted { float: right; margin-left: var(--space-4); }

.menu__note {
    margin: 0;
    padding: var(--space-2) var(--space-3);
    font-size: 0.82rem;
    color: var(--text-muted);
    max-width: 230px;
}

/* No hover on touch: the button has to be permanently visible there. */
@media (hover: none) {
    .photo-stage__download { opacity: 1; }
}

/* The image itself is the button that opens the full-screen view. A <button>
 * rather than a click handler on the <img>: it is focusable and announced,
 * and it keeps the keyboard route to a feature the mouse gets for free.
 * All the button chrome has to be reset -- it must look like the image. */
.photo-stage__zoom {
    display: block;
    width: auto;
    max-width: 100%;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    cursor: zoom-in;
}

.photo-stage__zoom img { display: block; }

/* Previous/next, for the mouse. The arrow keys do the same thing (see
 * photo_view.js); these exist because nothing on screen would otherwise
 * suggest that browsing is possible. */
.photo-stage__step {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: var(--radius-pill);
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-size: 1.6rem;
    line-height: 1;
    text-decoration: none;
    opacity: 0;
    transition: opacity 0.15s ease, background 0.15s ease;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}

.photo-stage__step--prev { left: var(--space-3); }
.photo-stage__step--next { right: var(--space-3); }

.photo-stage:hover .photo-stage__step,
.photo-stage__step:focus-visible { opacity: 1; }

.photo-stage__step:hover {
    background: rgba(0, 0, 0, 0.8);
    text-decoration: none;
}

/* Same reasoning as the download button: with no hover they must be visible. */
@media (hover: none) {
    .photo-stage__step { opacity: 1; }
}

.photo-browse-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin: 0 0 var(--space-4);
}

/* Full-screen view (issue #18), reworked by issue #263: "Lass die seite stehen
 * und lass das foto über de seite hoveren."
 *
 * It used to paint the page out behind a 0.92 backdrop, so opening a photo felt
 * like leaving the site. Now it is the same backdrop as every other overlay
 * here -- 0.55 and a 6px blur -- so the page stays recognisable underneath and
 * the photo floats on it. The page does not SCROLL while it is open
 * (`body.has-lightbox`), which is the other half of "lass die Seite stehen".
 *
 * z-index 1300, not the 1000 it had. The old comment claimed this was above
 * everything and named `.action-bar` at 500 as the highest rival; that stopped
 * being true when the header went to 1200 for the map (issue #117), so the
 * sticky header was drawing ON TOP of the full-screen photo. Nobody saw it
 * while the backdrop was nearly opaque. 1300 is what an overlay is on this site
 * (see .editor-overlay), and the two never open together.
 *
 * No `backdrop-filter` on anything inside this: an element with one is a
 * backdrop root for its descendants, so a blurred child would blur this
 * overlay's own paint rather than the page (see CLAUDE.md). */
.lightbox {
    position: fixed;
    inset: 0;
    z-index: 1300;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    cursor: zoom-out;
}

.lightbox[hidden] { display: none; }

/* Fits the viewport in both directions, so a portrait photo is not cropped
 * and a panorama is not scaled past the screen width.
 *
 * The picture zooms on a click (issue #337). The transform is built from
 * the CSS box's top-left corner, so the origin must be that corner and
 * nothing else -- see photo_view.js for the arithmetic. `touch-action:
 * none` hands the drag to the script instead of to the browser's own
 * panning, and only while zoomed, so an unzoomed overlay still scrolls and
 * pinches the way the page does. */
.lightbox__image {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    transform-origin: 0 0;
    cursor: zoom-in;
}

.lightbox__image:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.lightbox--zoomed .lightbox__image {
    cursor: grab;
    touch-action: none;
}

.lightbox--zoomed .lightbox__image:active { cursor: grabbing; }

.lightbox__close {
    position: absolute;
    top: var(--space-4);
    right: var(--space-4);
    width: 40px;
    height: 40px;
    border: 0;
    border-radius: var(--radius-pill);
    background: rgba(255, 255, 255, 0.15);
    color: #fff;
    cursor: pointer;
}

.lightbox__close:hover { background: rgba(255, 255, 255, 0.28); }

/* While the overlay is open the page behind must not scroll. */
body.has-lightbox { overflow: hidden; }

/* The frame hugs the photo (issue #121). Nothing here ever changes a photo's
 * proportions: the height cap is the one rule, and where that cap sits is
 * what decides which photos get the whole column and which get room left
 * and right.
 *
 * It used to sit at the height of a 3:2 photo filling the column
 * (column * 2/3, i.e. 744px once the column stops growing) -- issue #183.
 * That "fitted everything into 3:2": a 4:3, a square or a mildly upright
 * shot was hit by the cap, shrunk BELOW the column width, and came out
 * framed by empty page. The owner's follow-up to issue #183, 2026-08-27:
 * never touch the aspect ratio, fit the page height to the photo instead.
 *
 * So the cap is now column * 3/2, the height a 2:3 UPRIGHT photo has at full
 * column width, and the arithmetic is the entire rule: a photo fills the
 * column at height = column / (w/h), which exceeds column * 3/2 exactly when
 * w/h is below 2/3. Two formats fall out of it:
 *
 *   wider than 2:3    never clamped -- full column width, aspect ratio
 *                     untouched, and the page simply grows taller
 *   2:3 and narrower  clamped, so it stays narrower than the column and the
 *                     fit-content frame plus margin auto give it its room
 *                     left and right, as before
 *
 * Any cap BELOW column * 3/2 starts clamping photos wider than 2:3 again,
 * which is the bug this replaces. Do not lower it, and do not add a second
 * ceiling (a 100vh, say) next to it for the same reason.
 *
 * column is min(viewport, --content-width) minus the page's padding, which
 * is the smaller value below 720px. 100vw counts the scrollbar, so the cap
 * is a handful of pixels generous on a narrow desktop window; it can only
 * ever clamp a little late, never early.
 *
 * The old 80vh survives below 720px as a FLOOR, not a cap: on a phone held
 * upright it is the larger of the two, so a very tall photo keeps the size it
 * has today, and being larger than column * 3/2 it cannot clamp anything
 * wider than 2:3 either.
 *
 * width stays auto rather than 100%: an original narrower than the column
 * must not be upscaled into blur. */
.photo-stage img {
    display: block;
    width: auto;
    max-width: 100%;
    max-height: max(80vh, calc((100vw - 2 * var(--space-4)) * 3 / 2));
    margin: 0 auto;
}

@media (min-width: 720px) {
    .photo-stage img {
        max-height: calc((min(100vw, var(--content-width)) - 2 * var(--space-6)) * 3 / 2);
    }
}

.photo-layout {
    display: grid;
    gap: var(--space-6);
}

@media (min-width: 900px) {
    .photo-layout { grid-template-columns: minmax(0, 1fr) 340px; }
}

/* Definition-list metadata table: label above value on phones, two columns
 * from tablet up. Replaces the old <table> of <th> pairs. */
.spec-list {
    margin: 0;
    display: grid;
    gap: 0;
}

.spec-list > div {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0 var(--space-4);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}

.spec-list > div:first-child { padding-top: 0; }
.spec-list > div:last-child { border-bottom: 0; }

.spec-list dt {
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}

.spec-list dd { margin: 0; overflow-wrap: anywhere; }

@media (min-width: 560px) {
    .spec-list > div { grid-template-columns: 160px 1fr; align-items: baseline; }
    .spec-list dt { padding-top: 2px; }
}

.spot-teaser {
    padding: var(--space-3);
    border-top: 1px solid var(--border);
}

.spot-teaser__place { margin: 0; font-weight: 600; }

.spot-teaser__count {
    margin: 0 0 var(--space-3);
    font-size: 0.82rem;
    color: var(--text-muted);
}

/* Photospot page: the facts and the map sit next to each other once there is
 * room, and stack below it. The map is second in the source so a narrow screen
 * reads facts first, and it stays put while a long fact list scrolls. */
.spot-layout {
    display: grid;
    gap: var(--space-6);
    align-items: start;
}

@media (min-width: 900px) {
    .spot-layout {
        grid-template-columns: minmax(0, 1fr) 340px;
        /* Both columns end on the same line: a map that stops short of the
         * fact list looks adrift. Stretch instead of sticky, which needs a
         * fixed height and cannot fill a row. */
        align-items: stretch;
    }
    /* Where the map's top edge sits, relative to the top of the fact list.
     * 0 lines both columns up flush; one row of the spec-list rhythm is
     * calc(1.75rem + var(--space-3)). One value to change if it should move.
     *
     * No explicit height: with align-items:stretch the item fills the row
     * minus its margins, so the bottom edge stays flush with the fact list. */
    .spot-layout__map {
        --spot-map-offset: 0px;
        margin-top: var(--spot-map-offset);
        display: flex;
        flex-direction: column;
    }
.spot-layout__map .mini-map { flex: 1 1 auto; height: auto; }
}

/* MeteoSwiss forecast: one selected day drives the hourly strip below it.
 * Phones get a swipeable day rail with roughly three choices visible; desktop
 * keeps the whole provider horizon on one row. */
.weather {
    margin-top: var(--space-6);
    overflow: hidden;
}

.weather[hidden],
.weather-symbols { display: none; }

.weather__head { margin-bottom: var(--space-4); }
.weather__head p { margin: var(--space-1) 0 0; }
.weather .text-muted { color: var(--text-muted); }

.weather__days {
    display: grid;
    grid-auto-flow: column;
    gap: var(--space-2);
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scrollbar-color: var(--border-strong) var(--bg-sunken);
    scrollbar-width: auto;
}

.weather__days::-webkit-scrollbar { height: var(--space-2); }
.weather__days::-webkit-scrollbar-track { background: var(--bg-sunken); }
.weather__days::-webkit-scrollbar-thumb {
    background: var(--border-strong);
    border-radius: var(--radius-pill);
}

.weather__days {
    grid-auto-columns: minmax(9rem, 32vw);
    padding-bottom: var(--space-2);
}

.weather-day {
    display: flex;
    min-width: 0;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.weather-day:hover { border-color: var(--border-strong); }
.weather-day > * { min-width: 0; max-width: 100%; }
.weather-day.is-selected {
    border-color: var(--accent);
    background: var(--accent-soft);
}

.weather-day__date,
.weather-day__temperatures,
.weather__detail-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-2);
}

.weather-day__date span,
.weather-day__temperatures span,
.weather-day__condition,
.weather-hour__condition,
.weather__metric { color: var(--text-muted); }

.weather-day__date,
.weather-day__condition,
.weather-day__facts,
.weather-day__confidence,
.weather-hour__facts { font-size: 0.75rem; }

.weather-icon {
    width: var(--space-6);
    height: var(--space-6);
    fill: none;
    stroke: none;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.weather-glyph__sun { fill: var(--weather-sun); stroke: var(--accent-2); stroke-width: 1.2; }
.weather-glyph__sun-ray { fill: none; stroke: var(--weather-sun); stroke-width: 2.4; }
.weather-glyph__cloud { fill: var(--weather-cloud); stroke: var(--weather-high-cloud); stroke-width: 1.2; }
.weather-glyph__high-cloud-lines { fill: none; stroke: var(--weather-high-cloud); stroke-width: 2.2; }
.weather-glyph__rain-cloud { fill: var(--weather-high-cloud); stroke: var(--weather-rain); stroke-width: 1.2; }
.weather-glyph__rain { fill: var(--weather-rain); stroke: none; }
.weather-glyph__snow { fill: none; stroke: var(--weather-snow); stroke-width: 2; }
.weather-glyph__storm-cloud { fill: var(--weather-storm); stroke: var(--weather-night); stroke-width: 1.2; }
.weather-glyph__lightning { fill: var(--weather-lightning); stroke: var(--accent-2); stroke-width: 0.8; }
.weather-glyph__fog { fill: none; stroke: var(--weather-fog); stroke-width: 3.2; }
.weather-glyph__moon { fill: var(--weather-sun); stroke: var(--accent-2); stroke-width: 0.8; }

.weather-day__icon { align-self: center; width: var(--space-7); height: var(--space-7); }
.weather-day__condition { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.weather-day__temperatures strong { font-size: 1.1rem; }
.weather-day__facts,
.weather-hour__facts { display: grid; gap: var(--space-1); }
.weather__metric { display: flex; justify-content: space-between; gap: var(--space-2); }
.weather__metric-label::after { content: ":"; }
.weather__metric-label { min-width: 0; overflow-wrap: anywhere; }
.weather__metric-value { flex-shrink: 0; white-space: nowrap; }

.weather-day__fog {
    width: fit-content;
    border-radius: var(--radius-pill);
    padding: var(--space-1) var(--space-2);
    background: var(--bg-sunken);
}

.weather-day__confidence {
    align-self: stretch;
    width: 100%;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    font-size: 0.7rem;
    white-space: normal;
}

.weather__detail {
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}

.weather__detail-head { margin-bottom: var(--space-3); }
.weather__detail-head h3 { margin: 0; font-size: 1rem; }

.weather-chart-frame {
    position: relative;
    max-width: 100%;
    overflow: hidden;
    border: 1px solid var(--border);
    background: var(--bg-raised);
}

.weather-chart {
    max-width: 100%;
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scrollbar-width: none;
    cursor: grab;
    touch-action: pan-x pan-y;
    -webkit-overflow-scrolling: touch;
}
.weather-chart::-webkit-scrollbar { display: none; }
.weather-chart.is-dragging { cursor: grabbing; user-select: none; }
.weather-chart__marker {
    position: absolute;
    z-index: 2;
    inset-block: 0;
    left: 50%;
    width: 1px;
    background: var(--border-strong);
    pointer-events: none;
}
.weather-chart__fixed-axis {
    position: absolute;
    z-index: 3;
    top: 58px;
    bottom: auto;
    height: 150px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    color: var(--text-muted);
    font-family: var(--font);
    font-size: 10px;
    line-height: 1;
    pointer-events: none;
}
.weather-chart-frame.is-compact .weather-chart__fixed-axis {
    top: 52px;
    height: 130px;
}
.weather-chart__fixed-axis span { background: var(--bg-raised); }
.weather-chart__fixed-axis--temperature { left: var(--space-1); align-items: flex-start; }
.weather-chart__fixed-axis--precipitation { right: var(--space-1); align-items: flex-end; }
.weather-chart__fixed-axis--sunshine {
    top: 216px;
    bottom: auto;
    left: var(--space-1);
    height: 54px;
    align-items: flex-start;
}
.weather-chart-frame.is-compact .weather-chart__fixed-axis--sunshine {
    top: 190px;
    height: 44px;
}

.weather-chart__svg { display: block; }
.weather-chart__night { fill: var(--bg-sunken); }
.weather-chart__grid { stroke: var(--border); stroke-width: 1; }
.weather-chart__sunshine-grid { stroke: var(--border); stroke-width: 1; }
.weather-chart__sunshine { fill: var(--weather-sun); opacity: 0.72; }
.weather-chart__day-divider { stroke: var(--border-strong); stroke-width: 1; }
.weather-chart__temperature-band { fill: var(--accent-soft); }
.weather-chart__temperature-line {
    fill: none;
    stroke: var(--accent);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
}
.weather-chart__precipitation { fill: var(--weather-rain); opacity: 0.7; }
.weather-chart__now {
    stroke: var(--accent);
    stroke-width: 1.8;
    stroke-dasharray: 4 4;
}
.weather-chart__now-label {
    fill: var(--accent);
    font-size: 11px;
    font-weight: 600;
}
.weather-chart__icon {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: none;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.weather-chart__axis-label,
.weather-chart__axis-title,
.weather-chart__time {
    fill: var(--text-muted);
    font-family: var(--font);
    font-size: 11px;
}
.weather-chart__axis-title { font-size: 10px; }
.weather__source { margin: var(--space-4) 0 0; }

@media (min-width: 900px) {
    .weather__days {
        grid-auto-columns: minmax(8.5rem, 1fr);
        overflow-x: auto;
    }
    .weather-day { padding: var(--space-2); }
    .weather-day__date { flex-direction: column; gap: 0; }
}

@media (max-width: 760px) {
    .weather.card.weather--mobile-flat {
        display: flex;
        flex-direction: column;
        background: transparent;
        border-color: transparent;
        padding: 0;
        margin-inline: calc(var(--space-3) * -1);
    }

    .weather--mobile-flat .weather__head,
    .weather--mobile-flat .weather__days,
    .weather--mobile-flat .weather__detail,
    .weather--mobile-flat .weather__source {
        padding-inline: var(--space-2);
    }

    .weather--mobile-flat .weather__head {
        order: 1;
        margin-bottom: 0;
    }

    .weather--mobile-flat .weather__detail {
        order: 2;
        padding-top: var(--space-2);
        margin-top: 0;
        border-top: 0;
    }

    .weather--mobile-flat .weather__detail-head {
        padding-top: 0;
        margin-bottom: var(--space-2);
    }

    .weather--mobile-flat .weather-chart-frame {
        background: transparent;
        border-inline: 0;
    }

    .weather--mobile-flat .weather__days {
        order: 3;
        grid-auto-flow: row;
        grid-template-columns: minmax(0, 1fr);
        grid-auto-columns: auto;
        gap: 0;
        overflow: visible;
        margin-top: var(--space-3);
        padding-top: var(--space-2);
        padding-bottom: 0;
        border-top: 1px solid var(--border);
        scrollbar-width: none;
    }

    .weather--mobile-flat .weather__days::-webkit-scrollbar { display: none; }

    .weather--mobile-flat .weather-day {
        display: grid;
        grid-template-columns: minmax(3rem, 0.7fr) var(--space-7) minmax(0, 1.3fr) auto;
        grid-template-rows: auto auto;
        column-gap: var(--space-2);
        row-gap: var(--space-1);
        padding: var(--space-2);
        border-width: 0 0 1px;
        border-radius: 0;
        background: transparent;
    }

    .weather--mobile-flat .weather-day.is-selected {
        border-color: var(--accent);
        background: var(--accent-soft);
    }

    .weather--mobile-flat .weather-day__date {
        grid-column: 1;
        grid-row: 1 / 3;
        flex-direction: column;
        justify-content: center;
        align-items: flex-start;
        gap: 0;
    }

    .weather--mobile-flat .weather-day__icon {
        grid-column: 2;
        grid-row: 1 / 3;
    }

    .weather--mobile-flat .weather-day__condition {
        grid-column: 3;
        grid-row: 1;
        align-self: end;
    }

    .weather--mobile-flat .weather-day__temperatures {
        grid-column: 4;
        grid-row: 1;
        gap: var(--space-1);
        white-space: nowrap;
    }

    .weather--mobile-flat .weather-day__temperatures strong { font-size: 1rem; }

    .weather--mobile-flat .weather-day__facts {
        grid-column: 3 / 5;
        grid-row: 2;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        column-gap: var(--space-3);
        row-gap: var(--space-1);
    }

    .weather--mobile-flat .weather-day__confidence { order: 2; }
    .weather--mobile-flat .weather-day__fog {
        order: 3;
        grid-column: 1 / -1;
    }

    .weather--mobile-flat .weather__source {
        order: 4;
        margin-top: var(--space-3);
    }
}

@media (max-width: 419px) {
    .weather--mobile-flat .weather-day {
        grid-template-columns: var(--space-7) minmax(0, 1fr) auto;
    }
    .weather--mobile-flat .weather-day__date {
        grid-column: 1 / -1;
        grid-row: 1;
        flex-direction: row;
        justify-content: space-between;
    }
    .weather--mobile-flat .weather-day__icon { grid-column: 1; grid-row: 2; }
    .weather--mobile-flat .weather-day__condition { grid-column: 2; grid-row: 2; }
    .weather--mobile-flat .weather-day__temperatures { grid-column: 3; grid-row: 2; }
    .weather--mobile-flat .weather-day__facts { grid-column: 1 / -1; grid-row: 3; }
}

/* The location box: map, which photospot it is, and the ways out. Shared by
 * the photo page and the photospot page (components/location_box.html). The
 * card is already rounded and clipped, so the map needs no framing of its own. */
.location-box { display: flex; flex-direction: column; }
.location-box .mini-map { height: 300px; min-height: 260px; }

/* Two halves of one control, sitting flush in the bottom of the card: no outer
 * borders, one divider between them. */
.btn-split { display: grid; grid-template-columns: 1fr 1fr; }

.btn-split .btn {
    border-radius: 0;
    border-left: 0;
    border-right: 0;
    border-bottom: 0;
    text-align: center;
}

.btn-split .btn + .btn { border-left: 1px solid var(--border); }

.mini-map {
    height: 260px;
    border-radius: var(--radius-md);
    overflow: hidden;
    border: 1px solid var(--border);
}

/* The same box as a Google embed (ADR 0020): an iframe does not stretch on
 * its own the way a div does, so it is told to. Everything else -- height,
 * radius, hairline -- comes from .mini-map above. */
iframe.mini-map { display: block; width: 100%; }

/* The embed with something of ours over it (the owner, 2026-09-02): the frame fills the
 * box and the figure lies on top of it. */
.mini-map--embed { position: relative; }

.mini-map__frame {
    display: block;
    width: 100%;
    height: 100%;
    border: 0;
}

.mini-map__touch-action {
    position: absolute;
    top: var(--space-3);
    left: var(--space-3);
    z-index: 3;
}

.mini-map__touch-reveal {
    position: absolute;
    inset: 0;
    z-index: 2;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
}

@media (hover: hover) and (pointer: fine) {
    .mini-map__touch-action,
    .mini-map__touch-reveal { display: none; }
}

/* The sun figure over the embed: the vectors in one <svg>, the lettering in
 * spans after it. Never in the way of a pointer -- it is a drawing, not a
 * control, so the frame below it remains draggable. */
.mini-map__sun-svg {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.mini-map__sun-svg .sun-svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* Positioned by the surface, which writes left/top/width/height in pixels
 * and centres the label on its point exactly as Leaflet's divIcon anchor
 * did. Everything else -- colour, font, the white halo -- comes from the
 * .sun-card / .sun-deg / .sun-edge / .sun-hour / .sun-north rules further
 * down, which both surfaces share. */
.sun-label {
    position: absolute;
    display: block;
}

/* ------------------------------------------------------------- comments -- */

/* Comments carry weight (issue #14): someone wrote them, which costs more
 * than a click, so they read as their own object rather than as rows in a
 * list. Raised surface, accent edge, and the text at full size. */
.comment {
    margin-bottom: var(--space-3);
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
}

.comment:last-of-type { margin-bottom: 0; }

.comment__head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-2);
    font-size: 0.85rem;
}

.comment__author { font-weight: 600; }
.comment__time { color: var(--text-muted); }
.comment__text {
    margin: 0;
    overflow-wrap: anywhere;
    white-space: pre-line;
    font-size: 1rem;
}

/* ------------------------------------------------------------- activity -- */

/* One reaction to one of your photos. A like is a quiet line; a comment gets
 * the same treatment as on the photo page, because it is the same thing. */
.activity {
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}

.activity:last-of-type { border-bottom: 0; }

.activity--comment {
    margin: var(--space-3) 0;
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
}

/* Unread: a left edge rather than a background wash, which would fight with
 * the comment styling above. */
.activity--new { border-left: 3px solid var(--accent); padding-left: var(--space-3); }

.activity__head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-2);
    font-size: 0.9rem;
}

.activity__time { color: var(--text-muted); font-size: 0.8rem; margin-left: auto; }
.activity__text { margin: var(--space-2) 0 0; overflow-wrap: anywhere; white-space: pre-line; }

.activity--decision {
    margin: var(--space-3) 0;
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-left: 3px solid var(--danger);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
}

.activity__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-3);
}

/* Count beside a menu entry, and the bare dot on the trigger that says
 * "something is in there" without putting a number in the chrome. */
.badge {
    display: inline-block;
    min-width: 1.4em;
    padding: 0 0.4em;
    border-radius: var(--radius-pill);
    background: var(--accent);
    color: var(--bg-raised);
    font-size: 0.72rem;
    font-weight: 700;
    text-align: center;
}

.dot {
    display: inline-block;
    width: 7px;
    height: 7px;
    margin-left: 2px;
    border-radius: 50%;
    background: var(--accent);
    vertical-align: super;
}

/* --------------------------------------------------------------- notices -- */

.notice {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-md);
    background: var(--bg-raised);
    font-size: 0.9rem;
}

.notice--warn { border-left-color: var(--danger); }

/* A notice that IS a link: the whole box is the click target, not the few
 * words at the end of it (issue #210). Block, so the padding is inside the
 * anchor and there is no dead strip along the edges -- which is the entire
 * point of the change. The call to action keeps the accent; the sentence
 * before it stays body text, so the box does not read as one long link. */
a.notice--action {
    display: block;
    text-decoration: none;
    color: var(--text);
    transition: background 0.15s ease, border-color 0.15s ease;
}

a.notice--action:hover,
a.notice--action:focus-visible {
    border-color: var(--accent);
    background: var(--bg-sunken);
}

a.notice--action strong {
    display: block;
    margin-top: var(--space-1);
    color: var(--accent);
    font-weight: 600;
}

/* The field a link sent us to (issue #210). One flash, then out of the way --
 * a permanent marker on a form field reads as an error. photo_edit.js removes
 * the class after the animation. */
.is-flagged {
    animation: flag-field 1.6s ease;
}

@keyframes flag-field {
    0%, 60% { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
    100% { border-color: var(--border); box-shadow: none; }
}

@media (prefers-reduced-motion: reduce) {
    .is-flagged { animation: none; border-color: var(--accent); }
}

.notice p:last-child { margin-bottom: 0; }

/* ---------------------------------------------------------------- toasts -- */

/* Flash messages float over the page instead of being inserted at the top of
 * it (issue #254, the owner: "mach daraus ein Pop-Up").
 *
 * Three placement decisions, each one a thing it must not cover:
 *
 * - BELOW the header, and at z-index 1150, which is under the header (1200)
 *   and its dropdowns and over the map's sidebar (1100) and Leaflet's
 *   controls (1000). So a toast can never sit on the search box or an open
 *   menu, and is still readable on the locked fullscreen map.
 * - Top CENTRE, not top right: the map keeps its own furniture top left
 *   (search) and top right (settings, layers), and the middle is the one
 *   strip that is free on every surface the site has.
 * - Not at the bottom, where a phone's thumb has just been.
 *
 * The strip is full width so the toasts centre on it, and therefore lets
 * clicks through: only the toast itself is a target.
 *
 * On a narrow window a centred toast can clip the top corner of the map's
 * settings sidebar, and that is the deliberate trade: 1150 is ABOVE the
 * sidebar because a message hidden behind a panel is worse than a message
 * overlapping one for two seconds. */
.toasts {
    position: fixed;
    top: calc(var(--header-height) + var(--space-3));
    left: 0;
    right: 0;
    z-index: 1150;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    padding: 0 var(--space-4);
    pointer-events: none;
}

.toast {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    width: min(30rem, 100%);
    padding: var(--space-3) var(--space-2) var(--space-3) var(--space-4);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-md);
    background: var(--bg-raised);
    box-shadow: var(--shadow-lg);
    font-size: 0.9rem;
    animation: toast-in 0.18s ease;
}

/* Glass, because it floats over content -- and the plain --bg-glass, not the
 * nested one: the toast is a sibling of the header, not inside it, so it has
 * a real backdrop to blur (issue #113's rule). */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .toast {
        background: var(--bg-glass);
        -webkit-backdrop-filter: blur(14px) saturate(1.5);
        backdrop-filter: blur(14px) saturate(1.5);
    }
}

.toast--warn,
.toast--error { border-left-color: var(--danger); }

.toast__text {
    flex: 1 1 auto;
    margin: 0;
}

/* A toast with a second line under its message (the share link, issue #276).
 * The wrapper is what keeps the X at the top right, so it takes over the
 * `flex: 1 1 auto` the text carries in the one-line case; `min-width: 0` is
 * what lets a long URL wrap instead of pushing the button off the edge. Built
 * only when there is a URL, so a flash message's markup is unchanged. */
.toast__body {
    flex: 1 1 auto;
    min-width: 0;
}

/* The shared address, shown so it can be read and taken by hand even when the
 * clipboard refused it. `anywhere` rather than a truncation: a link the
 * visitor cannot see all of is not a link they can copy. */
.toast__url {
    display: block;
    margin-top: var(--space-1);
    overflow-wrap: anywhere;
    font-size: 0.85rem;
}

/* The X, top right of the toast (the owner's words). Sized to the 44px touch
 * target rule at its padding, quiet until it is hovered or focused -- the
 * shared :focus-visible ring is the only focus style on this site. */
.toast__close {
    flex: 0 0 auto;
    padding: 0 var(--space-2);
    border: 0;
    background: none;
    color: var(--text-muted);
    font-size: 1.25rem;
    line-height: 1.2;
    cursor: pointer;
}

.toast__close:hover { color: var(--text); }

.toast--leaving { animation: toast-out 0.2s ease forwards; }

@keyframes toast-in {
    from { opacity: 0; transform: translateY(-8px); }
    to   { opacity: 1; transform: translateY(0); }
}

@keyframes toast-out {
    from { opacity: 1; transform: translateY(0); }
    to   { opacity: 0; transform: translateY(-8px); }
}

/* No sliding for anybody who asked for no motion. The toast still appears and
 * still goes; toasts.js removes it on a timeout rather than on animationend
 * for exactly this reason. */
@media (prefers-reduced-motion: reduce) {
    .toast,
    .toast--leaving { animation: none; }
    .toast--leaving { opacity: 0; }
}

.empty-state {
    padding: var(--space-8) var(--space-4);
    text-align: center;
    color: var(--text-muted);
}

.error-text {
    margin: var(--space-2) 0 0;
    color: var(--danger);
    font-size: 0.875rem;
}

/* ----------------------------------------------------------------- forms -- */

.field { margin-bottom: var(--space-4); }

.field > label {
    display: block;
    margin-bottom: var(--space-1);
    font-size: 0.85rem;
    font-weight: 550;
    color: var(--text-muted);
}

/* EVERY text-like input the site uses, in one rule (issues #162, #163).
 * `date`, `search` and `url` were missing, so those fields fell back to the
 * browser's own styling -- a different font at a different size beside every
 * other field. The list is the one from the templates; a new type has to be
 * added here or it will look foreign. */
.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="number"],
.field input[type="date"],
.field input[type="datetime-local"],
.field input[type="search"],
.field input[type="url"],
.field input[type="tel"],
.field input[type="file"],
.field select,
.field textarea,
.input {
    width: 100%;
    padding: var(--space-3);
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    font: inherit;
    font-size: 0.9rem;
}

.field textarea { min-height: 110px; resize: vertical; }

/* A verified value that belongs to an external identity is visible but not
 * editable. Keep it legible instead of accepting the browser's inconsistent
 * disabled opacity, while the sunken surface makes the locked state clear. */
.field input:disabled {
    background: var(--bg-sunken);
    color: var(--text-muted);
    cursor: not-allowed;
    opacity: 1;
}

/* Date and search carry browser chrome inside the box (a picker icon, a
 * clear cross). Without a height they come out shorter than a text field
 * beside them, which is what made a row of filters look ragged. */
.field input[type="date"],
.field input[type="search"],
.input[type="date"],
.input[type="search"] {
    min-height: 2.75rem;
    font-variant-numeric: tabular-nums;
}
/* The picker icon in the same grey as the rest of the chrome. It is a
 * black glyph the browser draws, so in the dark it needs inverting -- and
 * the two dark selectors are written out separately, the same shape the
 * colour tokens use, because one of them must not leak into light mode. */
input[type="date"]::-webkit-calendar-picker-indicator {
    opacity: 0.6;
    cursor: pointer;
}
input[type="date"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) input[type="date"]::-webkit-calendar-picker-indicator {
        filter: invert(1);
    }
}
:root[data-theme="dark"] input[type="date"]::-webkit-calendar-picker-indicator {
    filter: invert(1);
}

/* Nothing here on purpose (owner's call, after two passes at it).
 *
 * A field used to get an accent border AND a 3px glow, which is the shape a
 * form uses to report an error -- so in the new red it read as a complaint
 * about what you had just typed. Removing it altogether went too far the
 * other way. What he pointed at as "beruhigt" is the plain `:focus-visible`
 * outline further up: 2px, 2px of offset, no glow. The report editor's
 * textarea sits outside `.field` and had been showing exactly that all along.
 *
 * Deleting the loud rule is therefore the whole fix: every field falls
 * through to the one outline the site already draws around links and
 * buttons. One focus style, everywhere. */

.field__hint { margin: var(--space-1) 0 0; font-size: 0.8rem; color: var(--text-muted); }

/* Checkbox rows read as one control: box and label on a line, normal weight. */
/* A checkbox and its text on one line. `label.field__check` rather than the
 * bare class: `.field > label` above is more specific than one class, so this
 * rule lost every property it shares with it -- the flex row, the gap and the
 * colour -- and the box has been sitting in a muted field-label typeface with
 * no gap since it was written. Found while adding the third one (issue #170);
 * the account page and the registration form get it back too. */
.field > label.field__check {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-1);
    font-size: 0.95rem;
    font-weight: 450;
    color: var(--text);
}

.field__check input { width: auto; }

.field .errorlist {
    margin: var(--space-1) 0 0;
    padding-left: var(--space-4);
    color: var(--danger);
    font-size: 0.85rem;
}

.field-row {
    display: grid;
    gap: var(--space-3);
    grid-template-columns: 1fr;
}

@media (min-width: 560px) {
    .field-row { grid-template-columns: 1fr 1fr; }
}

.auth-card {
    max-width: 420px;
    margin: var(--space-7) auto;
}

.auth-card h1 { text-align: center; }
.auth-card__switch { margin: var(--space-4) 0 0; text-align: center; font-size: 0.9rem; }
.auth-card__switch a { color: var(--accent); }

/* -------------------------------------------------------------- profiles -- */

.profile-head {
    display: grid;
    gap: var(--space-4);
    align-items: start;
    margin-bottom: var(--space-6);
}

@media (min-width: 640px) {
    .profile-head { grid-template-columns: auto 1fr; gap: var(--space-6); }
}

.avatar {
    width: 96px; height: 96px;
    border-radius: var(--radius-pill);
    object-fit: cover;
    background: var(--bg-sunken);
    border: 1px solid var(--border);
}

.avatar--placeholder {
    display: grid;
    place-items: center;
    font-size: 2rem;
    font-weight: 650;
    color: var(--text-muted);
}

/* Small avatar, for the photographer hits above search results. */
.avatar--sm {
    width: 40px; height: 40px;
    flex-shrink: 0;
}

.avatar--sm.avatar--placeholder { font-size: 1rem; }

/* Photographers matching a search, above the photos (issue #27). Chips
 * rather than a list: there are at most four, and they must not read as the
 * results themselves. */
.user-hits { margin-bottom: var(--space-5); }

.user-hits__heading {
    margin: 0 0 var(--space-3);
    font-size: 0.8rem;
    font-weight: 650;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.user-hits__list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

.user-hit {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-4) var(--space-2) var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--bg-raised);
    color: inherit;
    text-decoration: none;
}

.user-hit:hover {
    border-color: var(--accent);
    text-decoration: none;
}

.user-hit__name {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
    font-weight: 600;
}

.user-hit__count {
    font-size: 0.78rem;
    font-weight: 400;
    color: var(--text-muted);
}

/* ------------------------------------------------------- Reiseberichte -- */

.report-list { display: grid; gap: var(--space-4); }

/* Photo left, text right; stacked on a phone. A report without photos has no
 * image column at all, and the text simply fills the card. */
.report-card {
    display: grid;
    gap: var(--space-4);
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-raised);
}

@media (min-width: 700px) {
    .report-card:has(.report-card__image) {
        grid-template-columns: 320px 1fr;
        align-items: start;
    }
}

.report-card__image {
    display: block;
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--photo-mat);
}

.report-card__image img { display: block; width: 100%; aspect-ratio: 3 / 2; object-fit: cover; }

.report-card__title { margin: 0 0 var(--space-2); font-size: 1.25rem; }
.report-card__title a { color: inherit; }
.report-card__meta { margin: 0 0 var(--space-3); font-size: 0.85rem; color: var(--text-muted); }
.report-card__summary { margin: 0; color: var(--text-muted); }

/* The report itself. Prose width for the text, full column for the photos --
 * the photos are the point, the text is what carries you between them.
 *
 * Since issue #276 the page is the WIDE container (`.page`, not
 * `.page--narrow`): "Mach den Bericht breiter, genau gleich breit wie auch
 * alles andere". The line above this one had said "prose width for the text,
 * full column for the photos" since issue #154 -- the narrow container was
 * what stopped it being true, because at 720px the column and the prose cap
 * are the same number.
 *
 * This is NOT issue #177 returning. That moved every page to the wide column
 * and the owner's verdict was "das hat nun alles verschlimmert" -- forms,
 * prose and single cards do not read better stretched, and that is still a
 * decision. A Reisebericht is neither: it is photographs with text between
 * them, the one page on the site where the wide column buys the photos
 * something and costs the text nothing. */
.report-page__prose { max-width: var(--prose-width); }

/* `.report-lead` was the Kurzfassung at the top of the page. It is gone with
 * the paragraph (issue #276, "Den vorschautext braucht es im reisebericht
 * selber nicht"), rather than left behind as a rule nothing uses -- and this
 * margin is why the body now needs one of its own: the first block of a
 * report is usually a full-width photograph, and it sat straight under the
 * facts with 16px of air. */
.report-page .report-body { margin-top: var(--space-5); }

.report-text {
    margin: 0 0 var(--space-4);
    max-width: var(--prose-width);
    white-space: pre-line;
    overflow-wrap: anywhere;
}

.report-figure { margin: 0 0 var(--space-5); }

.report-figure img {
    display: block;
    width: 100%;
    border-radius: var(--radius-sm);
    background: var(--photo-mat);
}

.report-figure figcaption {
    margin-top: var(--space-2);
    font-size: 0.82rem;
    color: var(--text-muted);
    max-width: var(--prose-width);
}

/* A link and a mention inside the body (issue #276). The base rule is
 * `a { color: inherit; text-decoration: none }`, so an inline link in running
 * text would be invisible without this -- underlined here rather than only
 * coloured, because colour alone is not a signal everybody can see. The
 * mention follows `.comment__mention`, which is the same thing one surface
 * over. */
.report-link {
    color: var(--accent);
    text-decoration: underline;
    text-underline-offset: 0.15em;
}

.report-mention {
    color: var(--accent);
    font-weight: 550;
}

/* The editor, ONE full-width column since issue #273. It was two (issue
 * #154), writing on the left and the photos on the right: right that the
 * photos should not be stacked under the text, wrong about the price. The
 * text is the thing being written and it lost a fifth of the page
 * permanently to a panel that is only wanted at the moment of inserting.
 *
 * The photos are an overlay now, on the same recipe as the preview. Kept as
 * a grid with one column rather than collapsed to a block, so nothing that
 * relied on the gap between the form and what follows it moves. */
.report-editor {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-5);
    align-items: start;
}

/* The metadata autocomplete (issues #144, #157): a real dropdown, aligned
 * with the field it belongs to, in the same recipe as the header menus. The
 * first version used a native <datalist>, which the browser draws itself and
 * which cannot be styled at all. */
.suggest { position: relative; }

.suggest__panel {
    position: absolute;
    z-index: 40;
    top: calc(100% + 2px);
    left: 0;
    right: 0;
    max-height: 14rem;
    overflow-y: auto;
    padding: var(--space-1);
}

.suggest__option {
    display: block;
    width: 100%;
    padding: var(--space-2) var(--space-3);
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text);
    font: inherit;
    font-size: 0.9rem;
    text-align: left;
    cursor: pointer;
}
.suggest__option:hover,
.suggest__option.is-active { background: var(--bg-sunken); color: var(--accent); }

/* What the title offers, in grey inside the empty field. pointer-events off
 * so it never swallows the click that puts the cursor there. */
.suggest__ghost {
    position: absolute;
    left: calc(var(--space-3) + 1px);
    top: 50%;
    transform: translateY(-50%);
    color: var(--text-muted);
    font-size: 0.9rem;
    pointer-events: none;
}
.suggest__ghost::after {
    content: " ⇥";
    opacity: 0.7;
    font-size: 0.8em;
}

/* The statistics page (issue #165). Bars in a column, drawn with CSS: this
 * project has no build step, and a charting library would be its largest
 * dependency by a wide margin -- for rectangles. See docs/design.md. */
.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
    gap: var(--space-5) var(--space-6);
    margin-top: var(--space-6);
}

.stat-block { min-width: 0; }
.stat-block__title { margin: 0 0 var(--space-2); font-size: 1rem; }

.bar-chart {
    display: grid;
    gap: 3px;
    margin-top: var(--space-2);
}
.bar-chart__row {
    display: grid;
    grid-template-columns: 5.5rem 1fr 3rem;
    align-items: center;
    gap: var(--space-2);
    font-size: 0.82rem;
}
.bar-chart__label {
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.bar-chart__track {
    height: 0.85rem;
    border-radius: var(--radius-pill);
    background: var(--bg-sunken);
    overflow: hidden;
}
.bar-chart__bar {
    display: block;
    height: 100%;
    border-radius: var(--radius-pill);
    background: var(--accent);
}
.bar-chart__value {
    text-align: right;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

/* The headline figures, wider than the two on a profile. */
.stat-row--wide { gap: var(--space-6); }

.stat-list {
    margin: var(--space-2) 0 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: var(--space-2);
}
.stat-list li {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}
.stat-list a {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-width: 0;
    color: inherit;
    text-decoration: none;
}
.stat-list img {
    border-radius: var(--radius-sm);
    object-fit: cover;
    background: var(--photo-mat);
}
.stat-list__title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.stat-list a:hover .stat-list__title { color: var(--accent); }
.stat-list__value {
    margin-left: auto;
    color: var(--text-muted);
    font-size: 0.82rem;
    white-space: nowrap;
}

/* Equal-width social fields (issue #160), wrapping instead of shrinking
 * below a usable input width. */
.social-fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: var(--space-3);
}
.social-fields .field { margin: 0; min-width: 0; }
.social-fields label {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}
.social-fields__icon { display: inline-flex; color: var(--text-muted); }

/* The facts about the trip, as a block rather than five stacked rows: the
 * form was a scroll before the writing started. */
.editor-facts {
    margin: 0 0 var(--space-4);
    padding: var(--space-3) 0 0;
    border: 0;
    border-top: 1px solid var(--border);
}
.editor-facts legend {
    padding: 0 var(--space-2) 0 0;
    font-size: 0.78rem;
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}
.editor-facts__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: var(--space-3);
}
.editor-facts__grid .field { margin: 0; }
.editor-facts__wide { grid-column: 1 / -1; }

/* The marks the renderer understands, as buttons. */
.editor-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1);
    margin-bottom: var(--space-2);
    padding: var(--space-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-sunken);
}
.editor-toolbar .btn {
    min-width: 2.25rem;
    border-color: transparent;
    background: transparent;
}
.editor-toolbar .btn:hover {
    border-color: var(--border);
    background: var(--bg-raised);
}
.editor-toolbar [data-preview-toggle] {
    border-color: var(--border);
    background: var(--bg-raised);
}

/* Text and preview side by side while the preview is on. */
.editor-split { display: grid; gap: var(--space-3); }
.editor-split--preview { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
@media (max-width: 60rem) {
    .editor-split--preview { grid-template-columns: minmax(0, 1fr); }
}
.editor-split textarea { min-height: 24rem; resize: vertical; }
.report-editor__main textarea[data-report-body] { min-height: 24rem; }

.report-draft-visibility {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: 0.9rem;
    cursor: pointer;
}

.report-draft-visibility input { width: auto; }

/* The editor's floating panels: the preview (issue #162) and, since issue
 * #273, the photo picker. Same recipe as the lightbox -- a dimmed backdrop
 * and a sheet -- so the site has one idea of what an overlay is rather than
 * three. The two differ only in the sheet's width and what is inside it.
 *
 * Named `.editor-overlay` and not `.preview-overlay` because it stopped being
 * about the preview alone. The `data-preview-overlay` hook kept its name; the
 * JavaScript and a test both reference it. */
.editor-overlay {
    position: fixed;
    inset: 0;
    z-index: 1300;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}
.editor-overlay[hidden] { display: none; }

.editor-overlay__sheet {
    display: flex;
    flex-direction: column;
    width: min(48rem, 100%);
    max-height: 85vh;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-raised);
    box-shadow: var(--shadow-lg);
}
.editor-overlay__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border);
}
.editor-overlay__head h2 { margin: 0; font-size: 1rem; }
.editor-overlay__body {
    padding: var(--space-4);
    overflow-y: auto;
}
.editor-overlay__body img { max-width: 100%; height: auto; }

/* The picker's confirmation, OUTSIDE the scrolling body so it stays
 * reachable however far down the grid you are (issue #273). Mirrors the
 * head's border and padding, so the sheet reads as head / content / foot. */
.editor-overlay__foot {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--border);
}

/* The picker's sheet is wider than the preview's on purpose: the preview is
 * prose and wants a reading measure, the picker is a grid of photographs and
 * wants columns. Its body is the scroll container (the owner asked to be able
 * to scroll through the images), so the head and the pagination stay put
 * while the tiles move. */
.editor-overlay--picker .editor-overlay__sheet { width: min(64rem, 100%); }
.editor-preview {
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-raised);
    max-height: 32rem;
    overflow-y: auto;
}
.editor-preview img { max-width: 100%; height: auto; }

/* The facts about a trip, on its page. Capped at prose width with the rest
 * of the text since issue #276 widened the container. */
.report-facts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-5);
    margin: 0 0 var(--space-4);
    max-width: var(--prose-width);
}
.report-facts dt {
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}
.report-facts dd { margin: 0; }

/* The facts and the Teilen button on one line (issue #276, the owner: "Füge in
 * der Zeile wo auch Reisedatum ... ein Teilen hinzu").
 *
 * A wrapper rather than a fourth entry inside the <dl>, because a description
 * list holds terms and values and a button is neither. The declarations are
 * `.report-facts`' own, copied rather than approximated, so the button sits in
 * the same rhythm as the facts and one gap change moves both -- docs/design.md,
 * "Using the token is not the same as using the pattern".
 *
 * `align-items: center` and not `baseline`: the list is two lines tall (a
 * small label over its value) and the button is one, so a shared baseline
 * would pin the button to the row of labels and make it read as a third
 * label. The bottom margin moves up here from the list, or `center` would be
 * measured against a box with 16px of air under it. */
.report-facts-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-5);
    margin: 0 0 var(--space-4);
}

.report-facts-row .report-facts { margin-bottom: 0; }

.report-heading {
    margin: var(--space-5) 0 var(--space-2);
    max-width: var(--prose-width);
}

/* The report on the editor's surface (issue #276 follow-up, the owner: "Die
 * Darstellung des Editors ist super gelöst mit dem weissen hintergrund. Wende
 * die gleiche Logik für den Reisebericht selbst auch an").
 *
 * There is no new surface here on purpose. The markup carries the shared
 * `.card`, so the background, the hairline, the corner and the padding are the
 * editor's own declarations rather than a second thing that looks like them --
 * docs/design.md, "Using the token is not the same as using the pattern". The
 * consequence that matters: `--bg-raised` is defined in all three theme blocks,
 * so this is white on the light theme and #171a20 on the dark one, and it can
 * never come out as a white box on a dark page.
 *
 * The only thing this rule adds is the gap to the comments below it. */
.report-article { margin-bottom: var(--space-6); }

/* The measure, and why it is not `--prose-width` (the owner, second round:
 * "Zudem sind die Textblöcke nicht lange genug").
 *
 * `--prose-width` is shared with thirteen other places and stays at 720px.
 * The report gets its own dial, `--report-measure`, and `none` means "the card
 * is the measure": the paragraphs run the full width of the card's content box,
 * which is 1066px at a 1180px page.
 *
 * That number is not a taste call, it is a measurement. The editor's textarea
 * is 1066px at x=100 and the report's photographs are 1066px at x=100, so the
 * author has been WRITING at 1066 and reading it back at 720, centred, with
 * 173px of white on either side. Both of his complaints -- "apply the editor's
 * logic" and "the blocks are not long enough" -- describe that one mismatch,
 * and one column at the editor's own width answers both. It also puts the text
 * and the photographs on the same two edges, which is why the centring this
 * rule used to do is gone rather than merely widened.
 *
 * The honest cost, because it should be written down and not discovered: at
 * 16px system-ui, German prose averages 7.27px a character, so 1066px is about
 * 147 characters a line, against the 45-75 every typographic guideline asks
 * for. Two things make it liveable and neither is an accident -- the 1.55
 * line-height, and the fact that a Reisebericht's paragraphs are a few
 * sentences between photographs rather than pages of running text. If it reads
 * too long on real content, this token is the one place to change: a value of
 * 900px gives about 124 characters, 800px about 110.
 *
 * Scoped to `.report-page` so the editor's live preview, which has no card,
 * keeps the 720px cap it has always had inside its narrower sheet. */
.report-page .report-facts,
.report-page .report-facts-row,
.report-page .report-text,
.report-page .report-heading,
.report-page .report-figure figcaption,
.report-page .report-page__prose {
    max-width: var(--report-measure);
}

/* The comments sit OUTSIDE the card (see above) but line up with its text, by
 * wearing the card's own horizontal inset -- its padding plus its hairline.
 * Written from the same tokens the card uses rather than as the 1066 it comes
 * out as, so it follows if either ever changes. */
.report-actions,
.report-pager,
.report-comments {
    padding-left: calc(var(--space-5) + 1px);
    padding-right: calc(var(--space-5) + 1px);
}

/* Under the card: Bearbeiten/Löschen, then the neighbours, then the comments
 * (issue #436, the owner: "Vorheriger Bericht / Nächster Bericht wirken etwas
 * hereingequetscht. Das bearbeiten / Löschen klebt daran"). Measured on Dev
 * before the change: the two rows wore `row spaced-above`, and `spaced-above`
 * without a size suffix is not a rule in this file, so the pager started at
 * the card's margin, the buttons at the pager's last pixel and the comments
 * heading at theirs -- three things, no air. And the pager itself was two
 * pill buttons at the far corners of a 1148px column with nothing between
 * them to say where they led.
 *
 * Now each block carries its own margin, all three sit on the card's text
 * column (the inset above), and the pager is two cells between two hairlines
 * -- a small muted label and the neighbouring report's title beneath it, so
 * the link says where it goes. The next cell is right-aligned and stays in
 * the right column when it is the only one. */
.report-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-bottom: var(--space-6);
}

.report-pager {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-4) var(--space-6);
    padding-top: var(--space-4);
    padding-bottom: var(--space-4);
    margin-bottom: var(--space-6);
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
}

.report-pager__link {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}

.report-pager__link--next {
    grid-column: 2;
    text-align: right;
}

.report-pager__label {
    font-size: 0.8rem;
    color: var(--text-muted);
}

.report-pager__title {
    font-weight: 600;
    overflow-wrap: anywhere;
}

/* The base `a:hover` underlines the whole link; here only the title is the
 * name of the destination, so only it takes the underline. */
.report-pager__link:hover { text-decoration: none; }
.report-pager__link:hover .report-pager__title { text-decoration: underline; }

@media (max-width: 599px) {
    .report-pager { grid-template-columns: minmax(0, 1fr); }
    .report-pager__link--next { grid-column: 1; }
}

/* The selected title photo and the actions that open the shared photo picker.
 * The picker itself owns selection; this compact row only shows what will be
 * saved with the report. */
.report-cover-choice {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
}

.report-cover-choice__preview {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    width: 9rem;
    font-size: 0.8rem;
    color: var(--text-muted);
}

.report-cover-choice__preview img {
    display: block;
    width: 100%;
    aspect-ratio: 3 / 2;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--photo-mat);
}

.report-cover-choice__actions {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.report-picker { margin-top: 0; }
.report-picker h2 { margin: 0 0 var(--space-2); font-size: 1rem; }

/* Search and date range above the strip. Wraps to one field per row on a
 * phone; the reset button sits on the baseline of the inputs. */
.report-picker__filters {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-3);
    margin-top: var(--space-3);
}

.report-picker__filters .field { margin: 0; flex: 1 1 12rem; }
/* The two date fields share a row rather than taking one each (issue #273):
 * in the sidebar there was never room for that, in the overlay there is, and
 * on a phone the filters were eating 248px of a 690px sheet -- a third of the
 * space meant for the photos.
 *
 * `min-width: 0` on the field and the input is what actually does it. A flex
 * item defaults to `min-width: auto`, and a `type="date"` input has an
 * intrinsic width around 150px that it refuses to go below, so two of them
 * measured 322px against a 309px row and the second wrapped. The basis alone
 * had no effect for that reason. */
.report-picker__filters .field:not(:first-child) { flex: 1 1 8rem; min-width: 0; }
.report-picker__filters .field input { width: 100%; min-width: 0; }

/* Several columns since issue #273, because the panel is no longer a 20rem
 * sidebar. It was one column deliberately (issue #193, the owner: fewer
 * photos, bigger -- twelve big pictures beat forty stamps when the question
 * is "which shot was that"), and that reasoning is kept rather than
 * reversed: `minmax(11rem, 1fr)` holds the tiles at roughly the size they
 * had in the sidebar and uses the new width for MORE of them per row, not
 * for smaller ones.
 *
 * No max-height and no overflow here any more: the sheet's body is the
 * scroll container now, so the pagination underneath scrolls with the tiles
 * instead of being pinned below a second scrollbar. */
.report-picker__grid {
    display: grid;
    /* Four per row in the overlay, not five (issue #273, the owner). 14rem
     * against the sheet's ~61rem of inner width admits exactly four and
     * refuses a fifth; `min(100%, …)` collapses it to one column on a phone
     * rather than overflowing. Bigger tiles also stay true to issue #193 --
     * fewer photos, bigger. */
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 14rem), 1fr));
    gap: var(--space-3);
    margin-top: var(--space-3);
}

.report-picker__item {
    padding: 0;
    border: 2px solid transparent;
    border-radius: var(--radius-sm);
    background: none;
    cursor: pointer;
    line-height: 0;
    overflow: hidden;
}

.report-picker__item:hover,
.report-picker__item:focus-visible { border-color: var(--accent); }

/* A picked photo KEEPS its border (issue #273, the owner: "dann soll der
 * rote Rahmen bleiben").
 *
 * Byte-identical to `.upload-tile.is-active`, which is the same situation --
 * a grid of photo tiles with one picked -- and got there first. It was written
 * as 2px of solid `--accent` and corrected: the established pair is the accent
 * border plus 3px of `--accent-soft`, and a second, slightly different red ring
 * on another photo grid is exactly the drift that makes a site look assembled
 * rather than designed. If this treatment ever changes, change both. */
.report-picker__item.is-selected {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

.report-picker__item { width: 100%; }

.report-picker__item img {
    width: 100%;
    aspect-ratio: 3 / 2;
    height: auto;
    object-fit: cover;
    border-radius: 2px;
}

/* Terms of use: plain numbered sections at prose width. */
.terms section { margin-bottom: var(--space-6); max-width: var(--prose-width); }
.terms h2 { font-size: 1.05rem; margin-bottom: var(--space-2); }

/* A filter box with nothing to find: still readable (the count is the
 * information) but plainly not clickable. */
.filters__dates { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.filters__dates label { display: flex; align-items: center; gap: var(--space-2); font-size: 0.85rem; }

.filters__option--empty {
    opacity: 0.45;
    cursor: default;
}

/* Moderation queues (issue #23): a heading, why it matters, then rows of
 * "who is waiting" and one button each. */
.mod-section { margin-bottom: var(--space-7); }

.mod-section h2 { margin: 0 0 var(--space-2); font-size: 1.1rem; }

.mod-section > .muted { margin: 0 0 var(--space-4); max-width: var(--prose-width); }

.mod-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
}

.mod-row .muted { display: block; }

.mod-row__identity { min-width: 0; }

.mod-row__owner {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-1);
}

.mod-row__owner .muted { display: inline; }

/* A pending account is a moderation decision about a person, not just a
 * username. Keep the submitted profile together as one compact preview so a
 * moderator can judge it without opening several tabs. */
.mod-profile-row { align-items: flex-start; }

.mod-profile-preview {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-3);
    flex: 1;
    min-width: 0;
}

.mod-profile-preview__body { min-width: 0; }

.mod-profile-preview__heading,
.mod-profile-preview__meta {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.mod-profile-preview__bio {
    margin: var(--space-2) 0 0;
    max-width: var(--prose-width);
    white-space: pre-line;
}

.mod-profile-preview__chips {
    margin: var(--space-2) 0 0;
    gap: var(--space-2);
}

.mod-profile-preview__licence { margin: var(--space-2) 0 0; }

/* A thumbnail inside such a row: the deleted-photos list (issue #260) has to
 * say WHICH photo, and it cannot use a photo card, because a card links to
 * the photo's own page and a deleted photo no longer has one. Fixed square so
 * the rows line up whatever the aspect ratio. */
.mod-row__thumb {
    width: var(--space-8);
    height: var(--space-8);
    flex-shrink: 0;
    object-fit: cover;
    border-radius: var(--radius-sm);
}

/* A Fahrzeugfamilie's Zuordnungen (issue #257, reopened on presentation):
 * one rule per row, separated by hairlines. It shipped as a `.card` per rule
 * with all three field hints inside, which is a card inside a card in the
 * Verwaltung and was a card inside a floating panel on the family page --
 * "Zuordnung ist doppelt vorhanden" and "kein Fenster im Fenster" are the
 * same markup seen twice. No box per rule, no shadow, no nested surface: the
 * three fields share a row (docs/design.md, "fields that belong together"),
 * and the explanation is given once above the list. */
.rule-list { display: flex; flex-direction: column; }

.rule-row {
    padding: var(--space-3) 0;
    border-top: 1px solid var(--border);
}

.rule-row__fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: var(--space-3);
}

/* min-width: 0 or an input's default width wins over the grid track and the
 * three columns stop being equal. */
.rule-row__fields .field { margin: 0; min-width: 0; }

/* Only the trailing empty row is named, because only it needs saying which
 * one it is. Every filled row is identified by what is in it. */
.rule-row__mark {
    margin: 0 0 var(--space-2);
    font-size: 0.78rem;
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

/* FAQ: a native <details> per question, styled as a card so the closed
 * state still reads as a list of questions rather than as dense text. */
.faq {
    margin-bottom: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
}

.faq__question {
    padding: var(--space-4);
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}

/* The default marker sits oddly next to a padded summary; draw our own so it
 * lines up with the text and rotates when open. */
.faq__question::-webkit-details-marker { display: none; }

.faq__question::before {
    content: "›";
    display: inline-block;
    width: 1em;
    margin-right: var(--space-2);
    color: var(--text-muted);
    transition: transform 0.15s ease;
}

.faq[open] .faq__question::before { transform: rotate(90deg); }

.faq__question:hover { color: var(--accent); }

.faq__answer {
    padding: 0 var(--space-4) var(--space-4) calc(var(--space-4) + 1em + var(--space-2));
    max-width: var(--prose-width);
}

.faq__answer p { margin: 0 0 var(--space-3); }
.faq__answer p:last-child { margin-bottom: 0; }

/* The one answer that carries a list: the map's data sources, where each
 * provider's required wording has to stand on its own line rather than run
 * together in a paragraph. */
.faq__sources {
    margin: 0 0 var(--space-3);
    padding-left: var(--space-4);
}
.faq__sources li { margin-bottom: var(--space-2); }
.faq__sources li:last-child { margin-bottom: 0; }

.profile-bio { max-width: var(--prose-width); }
.profile-links { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.profile-links a { color: var(--accent); font-size: 0.9rem; overflow-wrap: anywhere; }

.stat-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-5);
    margin: var(--space-3) 0;
    padding: 0;
    list-style: none;
}

.stat__value { font-size: 1.25rem; font-weight: 650; display: block; color: var(--accent); }
.stat__label { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }

/* -------------------------------------------------------- keyword index -- */

.keyword-index {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 220px), 1fr));
    gap: var(--space-1) var(--space-4);
}

.keyword-index li { padding: var(--space-1) 0; }
.keyword-index a { font-weight: 550; }
.keyword-index a:hover { color: var(--accent); }
.keyword-index .count { color: var(--text-muted); font-size: 0.8rem; }

.keyword-index-tools {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

.keyword-index-search {
    width: min(20rem, 100%);
}

.keyword-index-search__control {
    display: flex;
    align-items: center;
    width: 100%;
    background: var(--bg-raised);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-sm);
}

.keyword-index-search input {
    min-width: 0;
    width: 100%;
    padding: var(--space-2) var(--space-3);
    border: 0;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 0.85rem;
    line-height: 1.2;
}

.keyword-index-search input::-webkit-search-cancel-button { display: none; }

.keyword-index-search__submit,
.keyword-index-search__clear {
    position: relative;
    flex: 0 0 auto;
    align-self: stretch;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 var(--space-3);
    border: 0;
    border-left: 1px solid var(--border);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
}

.keyword-index-search__submit:hover,
.keyword-index-search__clear:hover {
    color: var(--accent);
    background: var(--bg-sunken);
    text-decoration: none;
}

.keyword-index-search__clear {
    padding: 0 var(--space-4);
    border-left: 0;
}

.keyword-index-search__clear::before,
.keyword-index-search__clear::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--space-3);
    height: 1px;
    background: currentColor;
}

.keyword-index-search__clear::before {
    transform: translate(-50%, -50%) rotate(45deg);
}

.keyword-index-search__clear::after {
    transform: translate(-50%, -50%) rotate(-45deg);
}

.keyword-index-results {
    transition: opacity 0.15s ease;
}

.keyword-index-results[aria-busy="true"] { opacity: 0.55; }

.keyword-card,
.keyword-card__placeholder {
    min-height: 190px;
}

.keyword-card__placeholder {
    background: var(--photo-mat);
}

.keyword-page-heading {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3);
}

.keyword-page-heading h1 { margin-bottom: 0; }

/* Editing-only title-image preview. The public detail page deliberately has
 * no hero: this image represents the entry on overview cards. */
.keyword-cover-editor {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-5);
}

.keyword-cover-editor__body {
    flex: 1 1 24rem;
    min-width: 0;
}

.keyword-cover-editor__preview {
    flex: 0 1 18rem;
    width: min(18rem, 100%);
    margin: 0 0 0 auto;
    background: var(--photo-mat);
}

.keyword-cover-editor__preview picture { display: contents; }

.keyword-cover-editor__preview-button {
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    color: inherit;
    background: transparent;
    cursor: pointer;
}

.keyword-cover-editor__preview img {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 10;
    object-fit: contain;
}

.keyword-cover-editor__preview figcaption {
    padding: var(--space-2);
    font-size: 0.8rem;
    color: var(--text-muted);
    background: var(--bg-raised);
}

.keyword-cover-picker__item { line-height: 1.3; text-align: left; }

.keyword-cover-picker__item span {
    display: grid;
    gap: var(--space-1);
    padding: var(--space-2);
    color: var(--text-muted);
    font-size: 0.8rem;
}

.keyword-cover-picker__item strong { color: var(--text); }

/* ------------------------------------------------------------ pagination -- */

.pagination {
    grid-column: 1 / -1;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-6);
}

.pagination__info { font-size: 0.875rem; color: var(--text-muted); }

/* The numbered page links (2026-09-06). Quieter than the two .btn on either
 * side of them: those are the control most people use, these are the shortcut
 * into the middle of a long listing. `min-width` on a digit-width square keeps
 * "1" and "315" the same size, so the row does not jump as you page through. */
.pagination__pages {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    flex-wrap: wrap;
    justify-content: center;
}

.pagination__page {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2rem;
    padding: 0 var(--space-2);
    height: 2rem;
    border-radius: var(--radius-sm);
    font-size: 0.875rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    text-decoration: none;
}

a.pagination__page:hover,
a.pagination__page:focus-visible {
    background: var(--bg-sunken);
    color: var(--text);
}

.pagination__page.is-current {
    background: var(--accent);
    color: var(--accent-text);
    font-weight: 600;
}

.pagination__gap {
    color: var(--text-muted);
    padding: 0 var(--space-1);
    font-size: 0.875rem;
}

/* How many photos per page. Quieter than the pagination above it: this is a
 * setting people touch once, not a control they page through. */
.per-page {
    display: flex;
    justify-content: center;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-4);
    font-size: 0.8rem;
    color: var(--text-muted);
}

.per-page__label { margin-right: var(--space-1); }

/* The editor is as wide as the photo page (issue #341, the owner: "Mach
 * denn Editor bitte breiter, wie z.B. der Fotoview"), and the width goes to
 * the photograph rather than to the fields. Issue #177 stretched forms
 * across the full 1180px and the owner reverted it the same day, so the
 * form column keeps a form's measure and the picture takes the rest.
 *
 * One column below 1000px, in source order: the picture, then the form,
 * which is exactly the page this replaced. */
.photo-edit__layout {
    display: grid;
    gap: var(--space-5);
    align-items: start;
}

/* A field linked to by its id lands clear of the fixed header even with
 * no JavaScript (issue #339). The script additionally centres it, which
 * is what the owner asked for; this is the floor under that. */
.photo-edit .field input,
.photo-edit .field textarea,
.photo-edit .field select {
    scroll-margin-top: calc(var(--header-height) + var(--space-5));
}

@media (min-width: 1000px) {
    .photo-edit__layout {
        grid-template-columns: minmax(280px, 460px) minmax(0, 1fr);
        gap: var(--space-6);
    }
    /* The picture stays on screen while the form scrolls: the running
     * number is read off it at the bottom of the form as much as at the
     * top. The header is fixed, hence its height in the offset. */
    .photo-edit__aside {
        position: sticky;
        top: calc(var(--header-height) + var(--space-4));
    }
    /* Full height where there is room: a portrait photograph beside a long
     * form should use it rather than sit in a letterbox. */
    .photo-edit__aside .edit-preview img { max-height: 70vh; }
}

/* The photo beside the edit form: a working reference, so it is capped well
 * short of the viewport height -- the fields must stay reachable without
 * scrolling past a full-height image. */
.edit-preview {
    display: block;
    /* A button since issue #346, so it has to be told to stop looking like
     * one: no chrome of its own, the picture is the control. */
    width: 100%;
    padding: 0;
    text-align: inherit;
    cursor: zoom-in;
    margin-bottom: var(--space-5);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--photo-mat);
}

.edit-preview img {
    display: block;
    width: 100%;
    max-height: 45vh;
    object-fit: contain;
}

/* Handle and role side by side under the profile name. */
.profile-handle {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.per-page__option {
    padding: 2px var(--space-2);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    text-decoration: none;
}

.per-page__option:hover {
    background: var(--bg-sunken);
    color: var(--text);
    text-decoration: none;
}

.per-page__option.is-current {
    background: var(--bg-sunken);
    color: var(--text);
    font-weight: 600;
}

/* The map's layer panel: one toggle, two groups of radios behind it. Built by
 * map_layers.js as a Leaflet control, using the same [data-menu] contract as
 * the header menus so site.js closes it on Escape and on a click outside. */
.map-panel {
    position: relative;
    /* Leaflet's own control styling stops at the container, so this carries
     * the shadow and radius the map's other controls have. */
    border-radius: var(--radius-sm);
    box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
}

.map-panel__toggle {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: 0;
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
    color: var(--text);
    font: inherit;
    font-size: 0.85rem;
    font-weight: 550;
    cursor: pointer;
}

.map-panel__toggle::before {
    /* Stacked sheets, drawn rather than typeset so no font has to carry it. */
    content: "";
    width: 12px;
    height: 12px;
    border: 1.5px solid currentColor;
    border-radius: 2px;
    box-shadow: 3px 3px 0 -1px var(--bg-raised), 3px 3px 0 0.5px currentColor;
}

.map-panel__toggle:hover { color: var(--accent); }

/* On a phone the words and the search box were fighting over the top row
 * (issue #259, "search überschneidet sich mit Einstellungen"). Measured at
 * 375px: the search box ran to x=305 and this button began at x=232, so 73px
 * of it sat under the search.
 *
 * Below 560px the button is its own icon and nothing else -- 40px square,
 * so it is 4px taller than the labelled button was and still an easy tap,
 * and it leaves the search box the 295px it asks for with 20px to spare.
 * The words survive as the title and the aria-label, so a screen reader and
 * a long press both still get them. 560px is where the collision stops
 * mattering: above ~470px the box and the label clear each other anyway.
 *
 * Control sizes are written in px here like the rest of the map furniture
 * (.map-panel__toggle, .social-links__item): they are hit targets, not
 * rhythm. The asymmetric padding re-centres the icon, whose stacked-sheets
 * shadow adds 3px to its bottom right. */
@media (max-width: 559px) {
    .map-panel__toggle {
        width: 40px;
        height: 40px;
        gap: 0;
        justify-content: center;
        padding: 0 3px 3px 0;
    }
    .map-panel__toggle-label { display: none; }
}

/* .menu__panel already positions and styles itself; only the room the radios
 * need is different. */
.map-panel__body {
    min-width: 210px;
    padding: var(--space-3);
}

.map-panel__group {
    margin: 0;
    padding: 0;
    border: 0;
}

/* The separator between the groups is drawn by the group *above* it, as an
 * ::after inside its content box.
 *
 * Not a border on the fieldset: a <legend> is rendered in its fieldset's
 * border, so a border-top came out as a stub of line beside the heading. And
 * not a floated legend either, which is the usual answer to that -- a
 * 100%-wide float squeezed the flex rows next to it to zero width, so the
 * first two options of each group vanished while staying in the DOM. */
.map-panel__group:not(:last-of-type)::after {
    content: "";
    display: block;
    height: 1px;
    margin: var(--space-3) 0;
    background: var(--border);
}

.map-panel__heading {
    padding: 0 0 var(--space-2);
    font-size: 0.72rem;
    font-weight: 650;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.map-panel__option {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 2px 0;
    font-size: 0.875rem;
    cursor: pointer;
}

.map-panel__option:hover { color: var(--accent); }

/* The Deckkraft label inside the Gleisdarstellung group: same look as the
 * legends, but a div (a fieldset takes one legend) with room above, because
 * it follows the radio options instead of opening a section. */
.map-panel__subheading {
    padding-top: var(--space-2);
}

.map-panel__slider {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.map-panel__slider input[type="range"] {
    flex: 1;
    min-width: 0;
    accent-color: var(--accent);
}

.map-panel__slider input[type="range"]:disabled { opacity: 0.4; }

.map-panel__value {
    min-width: 3.2em;
    text-align: right;
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

/* A list of photospots that shows what is at each one: thumbnail, place,
 * distance and count. Used for "Fotospots in der Nähe". */
.spot-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
}

.spot-list__link {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-raised);
    color: inherit;
}

.spot-list__link:hover {
    border-color: var(--accent);
    text-decoration: none;
}

.spot-list__thumb {
    flex: 0 0 auto;
    display: block;
    width: 80px;
    aspect-ratio: 4 / 3;
    height: auto;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--photo-mat);
}

.spot-list__thumb--empty { background: var(--bg-sunken); }

.spot-list__text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.spot-list__place {
    font-weight: 550;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.spot-list__meta { font-size: 0.8rem; color: var(--text-muted); }

/* A saved spot is removed from the card itself: the small cross occupies its
 * top-right corner while the real link beneath stays the full card target. */
.spot-list__item { position: relative; }
.spot-list__item .spot-list__link { padding-right: var(--space-7); }
.spot-list__remove {
    position: absolute;
    top: var(--space-2);
    right: var(--space-2);
    z-index: 2;
}

/* One site-wide confirmation overlay. It reuses the established editor
 * overlay recipe and only narrows its sheet to a compact reading width. */
.confirm-dialog { z-index: 1400; }
.confirm-dialog__sheet { width: min(28rem, 100%); }
.confirm-dialog__message { margin: 0; padding: var(--space-4); }
.confirm-dialog__actions {
    justify-content: flex-end;
}
body.has-confirm-dialog { overflow: hidden; }

/* The public Dev warning is the same compact overlay as a confirmation. It
 * sits above the map tutorial so the two automatic introductions never
 * compete for focus. */
.development-notice { z-index: 1600; }
.development-notice .editor-overlay__body p { margin: 0; }
.development-notice .editor-overlay__body p + p {
    margin-top: var(--space-3);
}
body.has-development-notice { overflow: hidden; }

/* The full-screen map's guided introduction (issue #299). It uses the same
 * overlay recipe as every other modal surface, with a narrower reading sheet
 * and a footer that keeps progress and navigation reachable while the slide
 * itself scrolls. */
.map-tutorial { z-index: 1500; }
.map-tutorial__sheet { width: min(44rem, 100%); }
.map-tutorial__head h2 { font-size: 1.15rem; }
.map-tutorial__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 36px;
    height: 36px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    font: inherit;
    font-size: 1.5rem;
    cursor: pointer;
}
.map-tutorial__close:hover,
.map-tutorial__close:focus-visible {
    background: var(--bg-sunken);
    color: var(--text);
}
.map-tutorial__body { min-height: min(25rem, 58vh); }
.map-tutorial__slide {
    display: grid;
    grid-template-columns: 10rem minmax(0, 1fr);
    align-items: center;
    gap: var(--space-5);
    min-height: 20rem;
}
.map-tutorial__slide[hidden] { display: none; }
.map-tutorial__slide h3 {
    margin: 0 0 var(--space-3);
    font-size: clamp(1.3rem, 4vw, 1.8rem);
    line-height: 1.15;
}
.map-tutorial__slide p { margin: 0 0 var(--space-3); }
.map-tutorial__slide ul {
    margin: var(--space-3) 0 0;
    padding-left: 1.2rem;
    color: var(--text-muted);
}
.map-tutorial__slide li + li { margin-top: var(--space-2); }
.map-tutorial__illustration {
    display: grid;
    place-items: center;
    width: 10rem;
    aspect-ratio: 1;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent);
}
.map-tutorial__illustration svg {
    width: 72%;
    height: auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.map-tutorial__illustration--sun {
    background: var(--accent-2-soft);
    color: var(--accent-2);
}
.map-tutorial__illustration--shade svg path:last-child {
    fill: var(--accent-soft);
}
.map-tutorial__definitions { margin: var(--space-4) 0 0; }
.map-tutorial__definitions > div {
    padding: var(--space-3) 0;
    border-top: 1px solid var(--border);
}
.map-tutorial__definitions dt { font-weight: 650; }
.map-tutorial__definitions dd {
    margin: var(--space-1) 0 0;
    color: var(--text-muted);
    font-size: 0.9rem;
}
.map-tutorial__foot {
    flex-wrap: wrap;
    justify-content: space-between;
}
.map-tutorial__optout {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text-muted);
    font-size: 0.85rem;
    cursor: pointer;
}
.map-tutorial__progress {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    color: var(--text-muted);
    font-size: 0.8rem;
    font-variant-numeric: tabular-nums;
}
.map-tutorial__steps {
    display: inline-flex;
    gap: var(--space-1);
    margin-left: var(--space-2);
}
.map-tutorial__steps span {
    width: 18px;
    height: 3px;
    background: var(--border-strong);
}
.map-tutorial__steps span.is-current { background: var(--accent); }
.map-tutorial__actions {
    display: flex;
    gap: var(--space-2);
    margin-left: auto;
}
body.has-map-tutorial { overflow: hidden; }

@media (max-width: 640px) {
    .map-tutorial { padding: var(--space-2); }
    .map-tutorial__sheet { max-height: calc(100dvh - var(--space-4)); }
    .map-tutorial__body { min-height: 0; }
    .map-tutorial__slide {
        grid-template-columns: 1fr;
        align-content: start;
        gap: var(--space-3);
        min-height: 0;
    }
    .map-tutorial__illustration {
        width: 5rem;
        margin: 0 auto;
    }
    .map-tutorial__foot { gap: var(--space-3); }
    .map-tutorial__optout { order: 1; }
    .map-tutorial__progress { order: 2; margin-left: auto; }
    .map-tutorial__actions {
        order: 3;
        width: 100%;
        margin-left: 0;
    }
    .map-tutorial__actions .btn { flex: 1 1 0; }
}

/* A phone in landscape is wider than the portrait breakpoint but much
 * shorter. Keep the complete tutorial footer inside the viewport and use a
 * small side illustration instead of forcing the desktop slide height. */
@media (max-height: 500px) and (min-width: 641px) {
    .map-tutorial { padding: var(--space-2); }
    .map-tutorial__sheet { max-height: calc(100dvh - var(--space-4)); }
    .map-tutorial__head,
    .map-tutorial__foot { padding-block: var(--space-2); }
    .map-tutorial__body {
        min-height: 0;
        padding-block: var(--space-2);
    }
    .map-tutorial__slide {
        grid-template-columns: 5rem minmax(0, 1fr);
        gap: var(--space-3);
        min-height: 0;
    }
    .map-tutorial__illustration { width: 5rem; }
    .map-tutorial__slide h3 {
        margin-bottom: var(--space-2);
        font-size: 1.2rem;
    }
    .map-tutorial__slide p { margin-bottom: var(--space-2); }
    .map-tutorial__slide ul { margin-top: var(--space-2); }
    .map-tutorial__slide li + li { margin-top: var(--space-1); }
}

/* ---------------------------------------------------------------- filters -- */

/* The search results filter panel: a <details> drawer, closed until asked for. */
.filters {
    margin-bottom: var(--space-5);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-raised);
}

.filters__summary {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    font-weight: 550;
    cursor: pointer;
    list-style: none;
}
.filters__summary::-webkit-details-marker { display: none; }

.filters__summary:hover { color: var(--accent); }

.filters[open] .filters__summary { border-bottom: 1px solid var(--border); }

/* Two or three columns rather than one long list (issue #151). The groups
 * are of very different heights -- two countries beside forty operators --
 * so a masonry-ish column flow beats a grid: each group stays whole and the
 * columns fill themselves. One column on a phone. */
.filters__form {
    display: grid;
    gap: var(--space-4);
    padding: var(--space-4);
}

.filters__form--columns {
    display: block;
    columns: 3 16rem;
    column-gap: var(--space-6);
}
.filters__form--columns > .filters__group {
    break-inside: avoid;
    display: inline-block;
    width: 100%;
    margin-bottom: var(--space-4);
}
.filters__form--columns > .filters__actions {
    column-span: all;
    margin-top: var(--space-2);
}

/* The type-to-narrow box above a long option list. Its options hide until
 * the first letter is typed (filters.js reveals matches and, on load, any
 * that are already ticked). */
.filters__search {
    max-width: 280px;
    padding: var(--space-2) var(--space-3);
    margin-bottom: var(--space-2);
}

.filters__options--search .filters__option { display: none; }

/* The summary's arrow turns while the panel opens (owner's request). A
 * plain glyph in a span, not CSS content -- the escape variant reached the
 * browser mangled once. */
.filters__arrow {
    display: inline-block;
    transition: transform 0.15s ease;
    color: var(--text-muted);
}
.filters[open] .filters__arrow { transform: rotate(-180deg); }

/* Native date inputs default to the platform's monospace-ish UI font; the
 * .input class puts them in the site's type like every other field. Sized
 * to content, not the 100% every .input otherwise gets. */
.filters__dates .input { width: auto; padding: var(--space-2) var(--space-3); }
.filters__dates { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }

/* The apply button sits ON the collapsed bar (owner's request), submitting
 * the form inside the <details> via its form="" attribute. The summary keeps
 * room for it so the label and the "aktiv" chip never slide underneath. */
.filters-wrap { position: relative; }
.filters-wrap .filters__summary { padding-right: 11rem; }
.filters-wrap__apply {
    position: absolute;
    top: var(--space-2);
    right: var(--space-2);
    z-index: 1;
}

/* -------------------------------------------------- search suggestions -- */

/* The dropdown under the header search (search_suggest.js). Same panel
 * recipe as the menus, anchored to the form. */
.header-search { position: relative; }

.header-search__panel {
    position: absolute;
    left: 0;
    right: 0;
    top: calc(100% + var(--space-2));
    max-height: 60vh;
    overflow-y: auto;
    padding: var(--space-2);
    z-index: 950;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .header-search__panel {
        background: var(--bg-glass-nested);
        -webkit-backdrop-filter: blur(14px) saturate(1.5);
        backdrop-filter: blur(14px) saturate(1.5);
    }
}

.header-search__group {
    margin: var(--space-2) 0 0;
    padding: 0 var(--space-3);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}
.header-search__group:first-child { margin-top: 0; }

/* A row of the panel. An anchor when it navigates and a BUTTON when it acts
 * on this page -- on the big map a photospot hit moves the map instead of
 * leaving for its page (issue #279) -- so this resets what a button brings
 * with it and both kinds come out identical. */
.header-search__hit {
    display: block;
    width: 100%;
    padding: var(--space-2) var(--space-3);
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text);
    font: inherit;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
}
.header-search__hit:hover { background: var(--bg); color: var(--accent); }
.header-search__hit:focus-visible { background: var(--bg); color: var(--accent); }
.header-search__hit:disabled { color: var(--text-muted); cursor: default; }

/* The row that offers to look the text up as a place name, rather than
 * being an answer itself. Quieter, so it does not read as a hit. */
.header-search__hit--offer { color: var(--text-muted); font-size: 0.85rem; }

/* Header shortcuts share a comfortable target without covering the field. */
.header-search__filter {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: calc(var(--space-6) + var(--space-3));
    min-width: calc(var(--space-6) + var(--space-3));
    padding-block: var(--space-1);
    font-family: inherit;
    cursor: pointer;
}
.header-search input {
    min-height: calc(var(--space-6) + var(--space-3));
}
.filter-summary, .empty-recovery {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-block: var(--space-4);
}
.filter-summary {
    padding: var(--space-3) var(--space-4);
    border-left: 3px solid var(--accent);
    background: var(--bg-raised);
}
.filter-summary__label { font-weight: 650; }
.chip--removable { color: inherit; text-decoration: none; }
.chip--removable:hover { border-color: var(--accent); color: var(--accent); }
.filter-summary__reset { margin-left: auto; font-size: 0.85rem; }
.filters__actions [data-filters-state] { flex-basis: 100%; margin: 0; }

.filters__group {
    margin: 0;
    padding: 0;
    border: 0;
}

.filters__group legend {
    padding: 0 0 var(--space-2);
    font-size: 0.8rem;
    font-weight: 650;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.filters__modes {
    display: flex;
    gap: var(--space-4);
    margin-bottom: var(--space-2);
    font-size: 0.85rem;
}

.filters__modes label,
.filters__option {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    cursor: pointer;
}

.filters__options {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
}

.filters__option { font-size: 0.9rem; }

.filters__count { color: var(--text-muted); font-size: 0.8rem; }

.filters__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

.chip--sm { font-size: 0.7rem; padding: 1px var(--space-2); }

.chip--warn {
    border-color: var(--danger);
    color: var(--danger);
    background: color-mix(in srgb, var(--danger) 10%, transparent);
}

/* Above the card's overlay anchor, or the click target swallows it. */
.photo-card__missing {
    position: relative;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: var(--space-2) 0 0;
    font-size: 0.75rem;
    color: var(--text-muted);
}

/* ----------------------------------------------------------------- upload -- */

.dropzone {
    display: grid;
    place-items: center;
    gap: var(--space-2);
    padding: var(--space-8) var(--space-4);
    border: 2px dashed var(--border-strong);
    border-radius: var(--radius-md);
    text-align: center;
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease;
}

.dropzone:hover,
.dropzone.is-dragover {
    border-color: var(--accent);
    background: var(--accent-soft);
}

.dropzone__hint { color: var(--text-muted); font-size: 0.9rem; margin: 0; }

.upload-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 170px), 1fr));
    gap: var(--space-4);
    margin-top: var(--space-5);
}

.upload-tile {
    position: relative;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--bg-raised);
    cursor: pointer;
    text-align: left;
    padding: 0;
    font: inherit;
    color: inherit;
}

/* Exactly one tile is active: the one the editor is showing and the one the
 * arrow keys move away from. Every other tile stays fully coloured -- there is
 * no selection here, so nothing needs to look switched off. */
.upload-tile.is-active {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

.upload-tile:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* A file still on its way to the server. */
.upload-tile.is-busy {
    cursor: progress;
}

.upload-tile.is-busy img { opacity: 0.5; }

.upload-tile img {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    background: var(--photo-mat);
}

.upload-tile__name {
    display: block;
    padding: var(--space-2) var(--space-3);
    font-size: 0.8rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* "No location" marker: informational, not an error -- these photos upload
 * and publish normally, they just never reach the map. */
.upload-tile__nogeo {
    position: absolute;
    bottom: 34px;
    left: var(--space-2);
    padding: 2px var(--space-2);
    border-radius: var(--radius-pill);
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    font-size: 0.68rem;
}

.upload-tile__visibility {
    position: absolute;
    top: var(--space-2);
    left: var(--space-2);
}

.upload-tile__remove {
    position: absolute;
    top: var(--space-2); right: var(--space-2);
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    border-color: transparent;
}

.upload-tile__remove:hover { background: rgba(0, 0, 0, 0.85); }

/* Editor panel: a bottom sheet on phones, a side panel on desktop. */
.sheet {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 1000;
    max-height: 85vh;
    overflow-y: auto;
    padding: var(--space-5);
    background: var(--bg-raised);
    border-top: 1px solid var(--border);
    border-radius: var(--radius-md) var(--radius-md) 0 0;
    box-shadow: var(--shadow-lg);
    transform: translateY(101%);
    transition: transform 0.22s ease;
}

.sheet.is-open { transform: translateY(0); }

.sheet__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

.sheet__head h2 { margin: 0; }

/* Sticks to the bottom of the panel so the save action is always in reach,
 * however long the form gets. */
.sheet__foot {
    position: sticky;
    bottom: calc(var(--space-5) * -1);
    margin: var(--space-5) calc(var(--space-5) * -1) calc(var(--space-5) * -1);
    padding: var(--space-4) var(--space-5);
    background: var(--bg-raised);
    border-top: 1px solid var(--border);
}

.sheet__preview {
    width: 100%;
    max-height: 240px;
    object-fit: cover;
    border-radius: var(--radius-md);
    margin-bottom: var(--space-4);
    background: var(--photo-mat);
}

@media (min-width: 900px) {
    .sheet {
        inset: var(--header-height) 0 0 auto;
        width: 520px;
        max-height: none;
        border-radius: 0;
        border-top: 0;
        border-left: 1px solid var(--border);
        transform: translateX(101%);
    }
    .sheet.is-open { transform: translateX(0); }
}

.train-row {
    margin-bottom: var(--space-4);
}

/* The photo editor names every positional vehicle before its two smaller
 * field captions (issue #330). The heading deliberately copies the form's
 * established label treatment; the captions step down once so the grouping
 * reads before the individual fields. Upload keeps its existing compact row. */
.train-row__title {
    margin: 0 0 var(--space-2);
    color: var(--text-muted);
    font-size: 0.85rem;
    font-weight: 550;
}

.train-row__subtitle {
    display: block;
    margin: 0 0 var(--space-1);
    color: var(--text-muted);
    font-size: 0.8rem;
    font-weight: 550;
}

.train-row__identity {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-2);
    margin-bottom: var(--space-2);
}

.train-family-label {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin-bottom: var(--space-1);
}

.train-family-label__context {
    color: var(--text-muted);
}

/* -------------------------------------------------------------------- map -- */

/* The map is the page: it fills exactly what is left below the header, and
 * nothing scrolls.
 *
 * A flex column rather than calc(100dvh - header): the header's height plus its
 * bottom border is not the same as --header-height, and that 1px difference was
 * enough to make the document overflow. Letting flex distribute the space is
 * exact whatever the header does. dvh, not vh, so a mobile browser's collapsing
 * address bar cannot introduce a scrollbar either. */
/* body is already a flex column; the map page only caps it and stops the
 * page scrolling, so the map fills exactly what the header leaves. */
.map-page {
    height: 100vh;
    height: 100dvh;
    /* clip, not hidden: `hidden` still makes a scroll container, and the
     * settings panel parked off the right edge (translateX) gave it 336px
     * of scrollable overflow. A focus() or a stray drag on a phone scrolled
     * it, the page sat off-centre and a grey strip stood on the right (the
     * owner, 2026-09-06). `clip` cannot be scrolled by anything. */
    overflow: hidden;
    overflow: clip;
    /* A drag that reaches the edge of the map belongs to the map. Do not
     * hand it to Chrome's pull-to-refresh or Safari's page rubber-band. */
    overscroll-behavior: none;
}

/* Issue #229 gave the homepage a scrolling half below the map and issue
 * #251 took it away again: the front page is a locked fullscreen map, "not
 * scrolling on there under any circumstances". So there is one map-page
 * variant again and the rule above is all of it. */

.map-noscript {
    padding: var(--space-4);
    text-align: center;
}

/* The map page never scrolls, so the reserved scrollbar gutter (see html
 * above) would be a permanent empty strip beside the map. Give it back. */
html:has(.map-page) {
    scrollbar-gutter: auto;
    overscroll-behavior: none;
}

.map-page .site-footer { display: none; }

/* Issue #173: the map keeps the site's links after all, as one slim strip
 * under it. Desktop only -- on a phone the map IS the screen, every pixel of
 * it counts, and the drawer already carries the same destinations. The body
 * is a flex column, so the strip simply takes its own row and the map takes
 * what is left; it does not float over the map, where it would sit on the
 * attribution and the zoom buttons. */
@media (min-width: 900px) {
    .map-page .site-footer {
        display: block;
        flex: 0 0 auto;
        margin-top: 0;
        padding: var(--space-2) var(--space-4);
    }
    .map-page .site-footer__inner {
        gap: var(--space-1) var(--space-5);
        font-size: 0.8rem;
    }
}

.map-page > main {
    /* Shrink as well as grow, and allow shrinking below the content. */
    flex: 1 1 auto;
    min-height: 0;
    position: relative;
}

/* The attribution is NOT hidden on phones any more (it was, below 900px,
 * as "map furniture"): the OSM/OpenRailwayMap credits are licence
 * conditions and Google's own line must stay visible under its terms, so
 * since 2026-09-01 every width shows them. The scale bar the same media
 * block used to hide is gone entirely (owner's call, same day). */

/* The Google base (ADR 0020) brings its own chrome, and the mutant moves it
 * INTO Leaflet's bottom corners: the logo into bottom-left (which is the
 * logo's alone since the scale bar went), the copyright and terms row into
 * bottom-right -- absolutely positioned by Google at the corner's edge,
 * exactly where our attribution line sits, and it even wears Leaflet's own
 * .leaflet-control-attribution classes (the mutant adds them), so a margin
 * on that class moves both bars together. google_base.js therefore tags
 * Google's row `google-attribution`, and only that row is lifted clear:
 * ours stays flush at the very corner, Google's floats a bit above it
 * (the owner, 2026-09-01, second round -- the first cut stacked them
 * touching, and Google's line reads better with air under it). The row is
 * absolutely positioned with bottom: 0, so margin-bottom is what lifts it.
 * The second selector is the belt for the first: it recognises Google's
 * row by Google's own gm-style-cc markup inside it, in case the tag ever
 * misses. Below 900px our credits wrap to a second line, hence the larger
 * clearance there. */
.leaflet-bottom.leaflet-right .google-attribution,
.leaflet-bottom.leaflet-right .leaflet-control-attribution:has(.gm-style-cc) {
    margin-bottom: 20px;
}
@media (max-width: 899px) {
    .leaflet-bottom.leaflet-right .google-attribution,
    .leaflet-bottom.leaflet-right .leaflet-control-attribution:has(.gm-style-cc) {
        margin-bottom: 30px;
    }
}

/* OUR credit line, as small as it can stay readable (the owner, 2026-09-01:
 * "der Balken muss deutlich kleiner werden") -- and gone entirely when it
 * is empty. It only ever carries credits for third-party data actually on
 * the map: the layers bring their strings when added and take them when
 * removed (Leaflet's own mechanics), the shadow tool adds its DEM credit
 * only while it is on (map_tools.js), and the Google base contributes
 * nothing because Google's own row above is its attribution. So with the
 * Gleise off and no tool running, the bar has no content and no reason to
 * exist. The :not() guards keep Google's row out of these rules -- it
 * wears the same class (see above) but styles itself. */
/* It does NOT follow the theme (issue #334, the owner: "Im darkmode wird
 * das OSM Copyright dark dargestellt, google aber nicht [...] beides gleich
 * darstellen"). Google draws its own copyright row and gives us no way to
 * theme it, so ours turning dark beside a permanently light one is two
 * credit lines that do not match. The fixed pair below is Google's own
 * treatment: a translucent white plate with dark grey lettering, on every
 * theme. The map under it is the same map in both themes anyway -- Google's
 * tiles are not re-coloured either. */
.leaflet-container .leaflet-control-attribution:not(.google-attribution):not(:has(.gm-style-cc)) {
    padding: 1px 5px;
    background: var(--map-credit-bg);
    color: var(--map-credit-text);
    font-size: 0.625rem;
    line-height: 1.4;
}
.leaflet-container .leaflet-control-attribution:not(.google-attribution):not(:has(.gm-style-cc)) a {
    color: var(--map-credit-link);
}
.leaflet-container .leaflet-control-attribution:empty { display: none; }

/* Below the layout breakpoint the credit line folds to an (i) chip and one
 * tap unfolds it (the owner, 2026-09-02) -- the pattern the OSM
 * Foundation's attribution guideline accepts for small screens, with the
 * full linked wording one tap away. attribution_chip.js toggles
 * `attribution-open`; Google's own row is excluded here exactly as in the
 * rules above. The links stay in the DOM while folded (assistive tech can
 * still reach them) but must not be tappable through the chip. */
@media (max-width: 899px) {
    .leaflet-container:not(#photoMap) .leaflet-control-attribution:not(.google-attribution):not(:has(.gm-style-cc)):not(.attribution-open) {
        position: relative;
        width: 24px;
        height: 24px;
        padding: 0;
        overflow: hidden;
        border-radius: var(--radius-pill);
        color: transparent;
        cursor: pointer;
    }
    .leaflet-container:not(#photoMap) .leaflet-control-attribution:not(.google-attribution):not(:has(.gm-style-cc)):not(.attribution-open) a {
        pointer-events: none;
    }
    .leaflet-container:not(#photoMap) .leaflet-control-attribution:not(.google-attribution):not(:has(.gm-style-cc)):not(.attribution-open)::before {
        content: "ⓘ";
        position: absolute;
        inset: 0;
        display: grid;
        place-items: center;
        color: var(--map-credit-text);
        font-size: 14px;
    }
    .leaflet-container .leaflet-control-attribution.attribution-open {
        max-width: calc(100vw - var(--space-5));
    }
}

.leaflet-control-scale-line {
    padding: 2px var(--space-2) 1px;
    border: 0 !important;
    border-bottom: 2px solid var(--text) !important;
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
    background: color-mix(in srgb, var(--bg-raised) 88%, transparent);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
    color: var(--text);
    font-size: 0.7rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
    line-height: 1.5;
    text-shadow: none;
}

/* Leaflet stacks a second line when both units are on. Ours is metric only,
 * but keep the pairing correct in case that ever changes. */
.leaflet-control-scale-line:not(:first-child) {
    border-top: 0 !important;
    border-bottom: 2px solid var(--text) !important;
    border-radius: 0 0 var(--radius-sm) var(--radius-sm);
    margin-top: 2px;
}

#photoMap {
    height: 100%;
    width: 100%;
    position: relative;
    /* See .map-page: the parked sidebar must not make this scrollable. */
    overflow: hidden;
    overflow: clip;
    background: var(--bg-sunken);
    /* Leaflet adds this dynamically when touch support is detected. Keeping
     * it on the stable map node closes the first-paint and rotation gaps in
     * which the browser could claim a pinch for page zoom. */
    touch-action: none;
}

/* Google's own raster tiles seam at a device-pixel ratio other than 1
 * (owner's report 2026-09-03: "feine, weisse Linien in einem
 * Schachbrettmuster" on the satellite view). Rendered on Dev in headless
 * Chrome and measured: with EVERY overlay of ours removed, DPR 1.25 --
 * Windows at 125% -- shows a thin column about +65 brighter than its
 * neighbours every 256 CSS pixels; DPR 1.5 and 2.0 show it too, DPR 1.0
 * does not. It is Google positioning 256px tile images at fractional
 * device pixels, so each image's anti-aliased edge lets the map's light
 * background through. Light on dark imagery is the grid the owner saw; on
 * the road map the same leak is light on light and invisible.
 *
 * Neither fractional zoom, nor our overlay map types, nor any compositing
 * hint on the images changes it (all tested at the seam). What removes it
 * is the oldest tile trick there is: draw each tile one pixel larger than
 * its slot, so neighbours overlap the edge that would otherwise show. A
 * stretch of 1/256 is invisible; the seam went from +36 to +12, which is
 * the texture of the imagery itself.
 *
 * The attribute selector is what keeps this off Google's marker and
 * cluster images, which are also <img>s in .gm-style but never 256px wide.
 * It is Google's inline style being matched, which is theirs to change;
 * `map/tests_issue_349_round3.py` pins the rule, and the probe that
 * measured it is described in docs/handoff.md. Scoped to the Google engine,
 * where the picker's canvas carries the same class. */
.photo-map--google .gm-style img[style*="width: 256px"] {
    width: 257px !important;
    height: 257px !important;
}

/* The railway overlay's tiles on the Google engine (google_layers.js,
 * RailwayType). OpenRailwayMap serves 512px tiles and Google asks for 256px
 * cells, so each cell is a 256px `overflow: hidden` box holding a 512px
 * <img> shifted to show the right quarter. The global reset above says
 * `img { max-width: 100% }`, and max-width beats an inline width, so that
 * image was being clamped to 256px wide -- and 512px tall, because the
 * inline height wins over `height: auto`. Measured 2026-09-03: rendered
 * 256x512, and in every odd column shifted entirely out of its cell, so
 * half the railway cells were empty and the other half drew every track at
 * twice its height. A real bug, fixed here -- but NOT the owner's
 * "Schachbrettmuster", which this rule was first written up as. That is
 * Google's own tiles seaming at a device-pixel ratio other than 1 and has
 * its own rule above. The mistake was reasoning from the code instead of
 * rendering the page; the render came later and said otherwise.
 *
 * `none`, not a pixel value: the JS states the size, this only stops the
 * reset from overruling it. Google's own tiles are 256px images in 256px
 * boxes and were never affected. */
.map-railway-tile img {
    max-width: none;
    max-height: none;
}

/* -- The Google engine (ADR 0021) -------------------------------------
 *
 * With a key the big map runs on the Google Maps JavaScript API. Google
 * draws into a surface of its own inside #photoMap; the rail, the sidebar,
 * the time bar and the sun figure stay siblings of it, so their rules above
 * and below apply unchanged. */
.google-map-surface {
    position: absolute;
    inset: 0;
}

/* The time bar covers the bottom of the map. The Leaflet engine lifts its
 * corner controls above the bar; Google's logo and copyright row cannot be
 * moved, so the surface itself ends above the bar instead. The view keeps
 * the geographic centre through the resize (a ResizeObserver in
 * google_view.js).
 *
 * The inset is the dock's MEASURED height, published as
 * --map-timebar-height on #photoMap by map_tools.js (issue #352). It used
 * to be the two literals below, borrowed from `.leaflet-bottom`'s
 * clearance further down -- and the two uses are not the same measurement.
 * There the number lifts Leaflet's corner controls and a few pixels too
 * many only raise the attribution; here it is the edge of the drawn map,
 * so every pixel over the dock's real height is a strip of #photoMap's own
 * background showing between Google's map and the dock. Measured with the
 * literals in force: the dock was 69px against 76px reserved on a 1280px
 * desktop and 95px against 104px on a phone, so 7px and 9px of grey. The
 * literals stay as the fallback for the frame before the script runs, and
 * are what the dock of 2026-09-06 -- the tape alone, with the controls
 * floating over it -- measures at each width: 89px at 1280px wide and
 * 97px on a 393px phone (the tape's height plus its top border), read
 * off Dev in headless Chrome the day it shipped. */
#photoMap:has(.map-timebar:not([hidden])) .google-map-surface {
    bottom: var(--map-timebar-height, calc(88px + var(--space-2)));
}
@media (max-width: 700px) {
    #photoMap:has(.map-timebar:not([hidden])) .google-map-surface {
        bottom: var(--map-timebar-height, calc(96px + var(--space-2)));
    }
}

/* Where the sun figure draws on the Google engine: an element over the map,
 * the SVG surface's root, sized by map_tools.js so its centre is the
 * observer. Same z-index as the Leaflet screen pane (issue #311), below the
 * time bar (1000) and the rail (1120). A drawing, never a control. */
.map-screen-figure {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    z-index: 750;
    pointer-events: none;
    isolation: isolate;
}
.map-screen-figure .sun-svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* Our tile sources' licence credits (swisstopo, OpenRailwayMap, the terrain
 * model) as a control in Google's stack, in the position Google keeps clear
 * of its own copyright row. The credit tokens are fixed like the Leaflet
 * credit line's (issue #334): Google's row beside it does not follow the
 * theme either. */
.map-credits {
    margin: 0 0 var(--space-1);
    padding: 1px 5px;
    border-radius: var(--radius-sm);
    background: var(--map-credit-bg);
    color: var(--map-credit-text);
    font-size: 11px;
    line-height: 1.4;
}
.map-credits a { color: var(--map-credit-link); }
.map-credits[hidden] { display: none; }
/* Sits in Google's LEFT_BOTTOM slot, above the logo; the margin keeps it
 * off the map's edge the way Google's own row keeps off it. */
.map-credits { margin: 0 0 var(--space-1) var(--space-2); }

/* Below the layout breakpoint the credits fold to the same (i) chip the
 * Leaflet line uses (main.css above, attribution_chip.js toggles
 * `attribution-open`); the OSM Foundation's guideline accepts exactly this
 * for small screens. Google's own row is untouched: it is not ours. */
@media (max-width: 899px) {
    body:not(.map-page) .map-credits:not(.attribution-open) {
        position: relative;
        width: 24px;
        height: 24px;
        padding: 0;
        overflow: hidden;
        border-radius: var(--radius-pill);
        color: transparent;
        cursor: pointer;
    }
    body:not(.map-page) .map-credits:not(.attribution-open) a { pointer-events: none; }
    body:not(.map-page) .map-credits:not(.attribution-open)::before {
        content: "ⓘ";
        position: absolute;
        inset: 0;
        display: grid;
        place-items: center;
        color: var(--map-credit-text);
        font-size: 14px;
    }
    .map-credits.attribution-open { max-width: calc(100vw - var(--space-5)); }
}

/* The one navigation column, top left, on both engines (map_locate.js):
 * zoom in, zoom out, locate. 32px, a step down from Leaflet's 30/touch 30
 * and Google's 40 (the owner, 2026-09-06: "die icons bitte kleiner"). The
 * `.leaflet-bar` base gives it Leaflet's frame on that engine; these rules
 * make the Google engine's copy identical. */
.map-nav {
    display: grid;
    margin: 10px;
    background: var(--bg-raised);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-md);
    overflow: hidden;
}
.leaflet-container .map-nav.leaflet-bar { border: 0; }
.map-nav__button,
.leaflet-container .map-nav .map-nav__button {
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border: 0;
    border-bottom: 1px solid var(--border);
    border-radius: 0;
    background: transparent;
    color: var(--text-muted);
    text-decoration: none;
}
.map-nav__button:last-child,
.leaflet-container .map-nav .map-nav__button:last-child { border-bottom: 0; }
.map-nav__button:hover,
.map-nav__button:focus-visible,
.leaflet-container .map-nav .map-nav__button:hover {
    background: var(--bg-sunken);
    color: var(--text);
}
.map-nav__button svg { display: block; }

/* Google's InfoWindow in the site's theme. These are Google's own class
 * names -- undocumented, and theirs to change; if the popup comes up white
 * on the dark site again, this is where to look. The content inside is our
 * .map-popup, unchanged from the Leaflet engine. */
.photo-map--google .gm-style .gm-style-iw-c {
    padding: 0;
    border-radius: var(--radius-md);
    background: var(--bg-raised);
    color: var(--text);
    box-shadow: var(--shadow-lg);
}
.photo-map--google .gm-style .gm-style-iw-d {
    overflow: hidden !important;
    padding: 10px 12px;
}
.photo-map--google .gm-style .gm-style-iw-tc::after { background: var(--bg-raised); }
.photo-map--google .gm-style .gm-style-iw-chr { position: absolute; right: 0; top: 0; }

/* The close button gets a disc under it (issue #351, the owner: "the X to
 * close photospot previews is not visible when the background is dark").
 *
 * The rule above is what causes it: pulling the header row out of the flow
 * and pinning it to the bubble's top-right corner is what makes the popup
 * all photograph, and it puts the X over that photograph's corner -- the
 * bubble is 254px wide against a 230px full-bleed image, so roughly 28x30px
 * of the button sits on the picture. A glyph in --text then has whatever
 * contrast the photograph happens to give it, which for a dark corner in
 * the light theme (or a bright sky in the dark one) is none.
 *
 * The disc is --bg-raised carrying a --text glyph, which is a contrast pair
 * by construction in both themes, so the answer no longer depends on the
 * photograph at all. The hairline and the shadow are what separate the disc
 * itself from a busy picture.
 *
 * Not applied to the Leaflet engine's close button: measured on the keyless
 * map, that one clears the photo entirely (0px of horizontal overlap) and
 * sits on the popup's own raised background, so it has never had this
 * problem and does not need the ornament.
 *
 * !important because these are Google's own classes carrying Google's own
 * sizing, the same reason `.gm-style-iw-d` above needs it. */
.photo-map--google .gm-style .gm-ui-hover-effect {
    display: flex !important;
    align-items: center;
    justify-content: center;
    width: 28px !important;
    height: 28px !important;
    margin: 6px 6px 0 0 !important;
    padding: 0 !important;
    border-radius: 50%;
    background: var(--bg-raised) !important;
    border: 1px solid var(--border);
    box-shadow: var(--shadow-sm);
    opacity: 1 !important;
}
.photo-map--google .gm-style .gm-ui-hover-effect:hover {
    background: var(--bg-sunken) !important;
    border-color: var(--text-muted);
}
/* Google draws the cross as a mask on this span, so its colour is a
 * background colour. Its own margin would push it off the centre of a disc
 * this size. */
.photo-map--google .gm-style .gm-ui-hover-effect > span {
    background-color: var(--text);
    margin: 0 !important;
}

/* On a pointer, only while the box is under it (issue #351, second round --
 * the owner: "only show the x when hovering on desktop and always on
 * mobile"). The disc is deliberate furniture over the photograph, and the
 * photograph is what the box is for; on a device that can hover, there is
 * always a way to bring the disc back, so it does not have to sit there.
 *
 * `(hover: hover)` and not a width query: what decides this is whether the
 * input device can hover at all, not how wide the screen is. A touch
 * screen -- phone, tablet, or a laptop being used by touch -- keeps the
 * disc permanently, because there is no hovering to reveal it with and a
 * close button you cannot find is worse than one that is always there.
 *
 * Revealed by hovering the whole bubble, not the button: a 28px target you
 * have to find before it appears would be a worse control than the one
 * this issue started with. Focus counts as well, or the disc would be
 * invisible to anyone arriving by keyboard -- `:focus-visible` on the
 * button itself, since it is the only focusable thing in there. */
@media (hover: hover) {
    .photo-map--google .gm-style .gm-ui-hover-effect {
        opacity: 0 !important;
        transition: opacity 120ms ease-in-out;
    }
    .photo-map--google .gm-style .gm-style-iw-c:hover .gm-ui-hover-effect,
    .photo-map--google .gm-style .gm-ui-hover-effect:focus-visible {
        opacity: 1 !important;
    }
}
/* Motion is a preference, not a given; the disc still appears, at once. */
@media (hover: hover) and (prefers-reduced-motion: reduce) {
    .photo-map--google .gm-style .gm-ui-hover-effect { transition: none; }
}
.photo-map--google .map-popup { margin: 0; }

.map-unavailable {
    display: grid;
    place-content: center;
    height: 100%;
    padding: var(--space-6);
    text-align: center;
    color: var(--text-muted);
}


/* -- the point picker (issue #205; restored for #349) ------------------
 *
 * Deleted with the picker on 2026-09-02 and back with it. Two engines draw
 * inside .map-picker__map -- Leaflet without a Maps key, Google with one
 * (google_picker.js) -- and everything below is the shell around them, so
 * it is engine-agnostic apart from the one .leaflet-container rule, which
 * says so.
 */
.map-picker {
    position: fixed;
    inset: 0;
    z-index: 1300;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    background: rgba(0, 0, 0, 0.25);
}

.map-picker__dialog {
    display: flex;
    flex-direction: column;
    width: min(960px, 100%);
    height: min(80vh, 720px);
    background: var(--bg);
    border: 1px solid var(--border);
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35);
}

.map-picker[hidden] { display: none; }

/* The point being placed (issue #205, fourth round). The photospots around
 * it are now the big map's blue pins, so the point is a red dot: the two
 * must not be mistaken for each other. .leaflet-container prefixed like
 * every other override of Leaflet's -- its stylesheet loads after ours. */
.leaflet-container .map-picker__point {
    background: var(--accent);
    border: 3px solid #fff;
    border-radius: 50%;
    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.45);
    cursor: grab;
}
.leaflet-container .map-picker__point:active { cursor: grabbing; }

/* Over the map, top right: the "Ebenen" toggle. Solid background -- a
 * ghost button over an aerial photo is unreadable. */
.map-picker__layers {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    z-index: 1000;
    background: var(--bg-raised);
}

/* The panel that toggle opens: the big map's own layer groups and its
 * photospot filter, rendered by map_layers.js and map_spots.js into this
 * box exactly as they render into the map's sidebar (issue #349, third
 * round). The group, heading and option classes are theirs; this only
 * gives them a surface and a place. Solid rather than glass: it floats
 * over a map, and a glass panel over imagery is the same unreadable ghost
 * the toggle above avoids. */
.map-picker__panel {
    position: absolute;
    top: calc(var(--space-3) + 2.4rem);
    right: var(--space-3);
    z-index: 1000;
    width: min(300px, calc(100% - 2 * var(--space-3)));
    max-height: calc(100% - var(--space-3) - 2.4rem - var(--space-6));
    overflow: auto;
    padding: var(--space-2) var(--space-3) var(--space-3);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
}
.map-picker__panel[hidden] { display: none; }
.map-picker__panel .map-panel__heading { margin-top: var(--space-2); }

/* The photo being placed, bottom left over the map (second round: "still
 * show a preview of the image"). Small on purpose: it is a reminder of
 * where you stood, not a viewer. */
.map-picker__photo {
    position: absolute;
    bottom: var(--space-3);
    left: var(--space-3);
    z-index: 1000;
    width: min(220px, 40%);
    border: 1px solid var(--border);
    background: var(--bg-raised);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
    /* Clickable since issue #205's sixth round -- it used to be inert so it
     * could not swallow map clicks; now it is the control that enlarges it,
     * and the script stops the click reaching the map itself. */
    cursor: zoom-in;
    padding: 0;
    border-radius: 0;
}

.map-picker__photo img { display: block; width: 100%; height: auto; }

/* Enlarged: as big as the map allows, centred over it, still one click from
 * going back. Not a lightbox -- you are mid-task, and a second overlay to
 * dismiss would be one step too many. */
.map-picker__photo--large {
    width: auto;
    max-width: calc(100% - 2 * var(--space-3));
    top: var(--space-3);
    right: var(--space-3);
    cursor: zoom-out;
}

.map-picker__photo--large img {
    max-height: calc(100vh - 16rem);
    width: auto;
    max-width: 100%;
}

.map-picker-open, .map-picker-open body { overflow: hidden; }

.map-picker__row {
    display: flex;
    gap: var(--space-2);
    align-items: stretch;
}

.map-picker__row input { flex: 1 1 auto; }
.map-picker__open { flex: 0 0 auto; white-space: nowrap; }

.map-picker__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border);
}

.map-picker__map { position: relative; flex: 1 1 auto; }

.map-picker__search {
    position: absolute;
    top: var(--space-3);
    left: var(--space-3);
    z-index: 1000;
    width: min(320px, 55%);
}

.map-picker__results {
    margin-top: var(--space-1);
    background: var(--bg-raised);
    border: 1px solid var(--border);
}

.map-picker__result {
    display: block;
    width: 100%;
    padding: var(--space-2) var(--space-3);
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    font-size: 0.85rem;
    text-align: left;
    cursor: pointer;
}

.map-picker__result:hover { background: var(--accent-soft); }

.map-picker__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--border);
}

/* What the map is doing while there is no map (issue #349). The Google
 * runtime is fetched on the first open and not before, so for a moment
 * there is a dialog with an empty canvas in it -- and a click that appears
 * to do nothing is worse than a slow one that says so. Centred over the
 * canvas, above the map but below the search box and the photo corner,
 * and inert: it must never catch a click meant for the map. */
.map-picker__status {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 900;
    transform: translate(-50%, -50%);
    margin: 0;
    padding: var(--space-2) var(--space-4);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-sm);
    color: var(--text-muted);
    font-size: 0.85rem;
    text-align: center;
    pointer-events: none;
}
.map-picker__status[hidden] { display: none; }

/* ------------------------------------------------------------- home -- */

.home {
    display: grid;
    gap: var(--space-8);
}

.home__hero {
    display: grid;
    justify-items: start;
    gap: var(--space-3);
    padding-block: var(--space-6) var(--space-2);
}

.home__hero h1,
.home__hero p { margin: 0; }

.home__hero p {
    max-width: var(--prose-width);
    color: var(--text-muted);
    font-size: 1.1rem;
}

.home__section {
    display: grid;
    gap: var(--space-4);
}

.home__section .section-head { margin-bottom: 0; }
.home__section .section-head p { margin: var(--space-1) 0 0; }

.home-map-preview {
    position: relative;
    display: block;
    min-height: 360px;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-sunken);
    box-shadow: var(--shadow-sm);
}

.home-map-preview:hover {
    border-color: var(--accent);
    text-decoration: none;
}

.home-map-preview__canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    pointer-events: none;
}

/* A separate hit target above the iframe, including on touch browsers.
   An embedded browsing context must not be the child of its own link. */
.home-map-preview__link {
    position: absolute;
    inset: 0;
    z-index: 2;
}

.home-map-preview__link:focus-visible { outline-offset: calc(-1 * var(--space-1)); }

.home-map-preview__overlay {
    z-index: 1;
    opacity: 0;
    clip-path: inset(0 0 var(--space-6) 0);
}

.home-map-preview__overlay.is-ready { opacity: 1; }

.home-map-preview__marker.is-hidden,
.home-map-preview__marker[data-marker-kind="single"] .home-map-preview__cluster-visual,
.home-map-preview__marker:not([data-marker-kind="single"]) .home-map-preview__pin {
  display: none;
}

.home-map-preview__marker[data-marker-kind="small"] .home-map-preview__cluster-halo {
  fill: var(--map-cluster-small-halo);
}

.home-map-preview__marker[data-marker-kind="small"] .home-map-preview__cluster {
  fill: var(--map-cluster-small);
}

.home-map-preview__marker[data-marker-kind="medium"] .home-map-preview__cluster-halo {
  fill: var(--map-cluster-medium-halo);
}

.home-map-preview__marker[data-marker-kind="medium"] .home-map-preview__cluster {
  fill: var(--map-cluster-medium);
}

.home-map-preview__marker[data-marker-kind="large"] .home-map-preview__cluster-halo {
  fill: var(--map-cluster-large-halo);
}

.home-map-preview__marker[data-marker-kind="large"] .home-map-preview__cluster {
  fill: var(--map-cluster-large);
}

.home-map-preview__cluster-count {
    fill: var(--map-cluster-text);
    font-size: 12px;
    font-weight: 400;
    text-anchor: middle;
    dominant-baseline: central;
}

.home-map-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

@media (max-width: 700px) {
    .home { gap: var(--space-6); }
    .home-map-preview { min-height: 280px; }
    .home__section .section-head { align-items: flex-start; }
}

.map-popup { width: 230px; }

.map-popup img {
    display: block;
    width: 100%;
    aspect-ratio: 3 / 2;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--photo-mat);
}

.map-popup__title { display: block; margin-top: var(--space-2); font-size: 0.9rem; line-height: 1.3; }
.map-popup__place { margin: 2px 0 0; font-size: 0.82rem; font-weight: 600; color: var(--text); }
.map-popup__meta { margin: 2px 0 0; font-size: 0.78rem; color: var(--text-muted); }
.map-popup__link { display: inline-block; margin-top: var(--space-2); font-size: 0.82rem; font-weight: 600; color: var(--accent); }

/* Leaflet's popup chrome is tuned for text, not a photo -- and its own
 * stylesheet is light-only, which left a white card with dark grey text
 * floating on a dark site (issue #123).
 *
 * EVERY rule here is prefixed with .leaflet-container, and that is not
 * decoration (issue #126): Leaflet's and markercluster's stylesheets load
 * from the CDN in {% block extra_css %}, which comes AFTER main.css. At
 * equal specificity the later sheet wins, so `.leaflet-popup-content-wrapper`
 * on its own lost to Leaflet's `background: white` -- the text turned with
 * the theme and the card stayed white. The extra class buys the specificity
 * that source order takes away. */
.leaflet-container .leaflet-popup-content { margin: 10px 12px; color: var(--text); }
.leaflet-container .leaflet-popup-content-wrapper,
.leaflet-container .leaflet-popup-tip {
    background: var(--bg-raised);
    color: var(--text);
}
.leaflet-container .leaflet-popup-content-wrapper { border-radius: var(--radius-md); }

/* The clusters keep markercluster's own colour coding -- green, yellow,
 * orange by how many spots they hold. I recoloured them to the site accent
 * for dark mode and the owner wanted them back (issue #130): the colour
 * carries information, and losing it to match a palette is a bad trade. */

/* The action zone under a form: saves together on the left, the quiet way
 * out on the right, a hairline separating actions from fields. */
.form-actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}
.form-actions__cancel { margin-left: auto; }

@media (max-width: 559px) {
    .form-actions { flex-direction: column; align-items: stretch; }
    .form-actions .btn { width: 100%; }
    .form-actions__cancel { margin-left: 0; }
}

/* The theme options preview their own mode (owner's request): fixed
 * colours on purpose -- each button IS its mode's swatch, whatever theme
 * is active around it. System splits the difference. */
.theme-option {
    justify-content: center;
    border: 1px solid var(--border) !important;
    margin-bottom: var(--space-1);
}
.theme-option--light {
    background: #f7f7f5 !important;
    color: #16181d !important;
}
.theme-option--dark {
    background: #171a20 !important;
    color: #f2f4f7 !important;
}
.theme-option--system {
    background: linear-gradient(105deg, #f7f7f5 49.5%, #171a20 50.5%) !important;
    color: #16181d !important;
    text-shadow: 0 0 6px #f7f7f5, 0 0 2px #f7f7f5;
}

/* The little glyphs beside menu entries (issue #65): quiet by default,
 * they take the row's colour on hover. */
.menu__icon {
    flex: 0 0 auto;
    color: var(--text-muted);
}
.menu__panel a:hover .menu__icon,
.menu__panel button:hover .menu__icon { color: inherit; }

/* The moderation page's user search with its suggestion panel (#87). */
.user-search__field { position: relative; flex: 1 1 260px; }
.user-search__field .input { width: 100%; }
.user-search__panel { top: calc(100% + var(--space-1)); }

/* The Google sign-in button and its separator (issue #86). */
.auth-or {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin: var(--space-4) 0;
    color: var(--text-muted);
    font-size: 0.85rem;
}
.auth-or::before,
.auth-or::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--border);
}

.auth-google {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
}

/* The social profile icons on a public profile (issue #64). */
.social-links {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-3);
}
.social-links__item {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border: 1px solid var(--border);
    border-radius: 50%;
    color: var(--text-muted);
}
.social-links__item:hover {
    color: var(--accent);
    border-color: var(--accent);
}

/* -------------------------------------------- renaming beside a heading -- */

/* The moderators' entry point is a small edit icon behind the spot's name
 * (not a bar, the owner's call). Plain <details>, so no script: the summary
 * is the icon, the panel opens like a menu below the title row. */
/* Two pages wear this now: the photospot and the four vocabulary pages,
 * which edit their entry in place since issue #250 ("eben ähnlich wie die
 * Fotospots"). The `spot-` prefix stayed rather than renaming every class
 * across two templates for a name -- the rules are purely structural, and a
 * parallel set would be the thing that drifts. */
/* Inline flow, not flex: once a long title wraps, a flex h1 claims the full
 * row and pushes the icon to the far edge -- inline keeps the icon glued to
 * the last word of the name. The wrapper takes over the heading's margin. */
.spot-rename {
    position: relative;
    margin-bottom: var(--space-3);
}

.spot-rename h1 { display: inline; margin: 0; }

.spot-rename__details {
    display: inline-block;
    vertical-align: baseline;
    margin-left: var(--space-2);
}

/* The summary keeps its default display: WebKit refuses to toggle a
 * <summary> that is displayed as anything but a list item, which made the
 * pencil inert on Safari and iOS (issue #107). The box is on the span. */
.spot-rename__toggle {
    list-style: none;
    cursor: pointer;
    color: var(--text-muted);
}
.spot-rename__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: var(--radius-sm);
}
.spot-rename__toggle::-webkit-details-marker { display: none; }
.spot-rename__toggle::marker { content: ""; }
.spot-rename__toggle:hover .spot-rename__icon,
.spot-rename__details[open] .spot-rename__icon {
    color: var(--text);
    background: var(--bg-raised);
}

/* The keep button sits with the facts line it belongs to (issue #108). */
.spot-keep { margin-top: var(--space-2); }

/* A slider whose feature is switched off stays in place and dims (issue
 * #98) -- removing the row would make the panel jump under the cursor. */
.map-panel__slider--off { opacity: 0.45; }

/* The share button (issue #109): a .btn with its icon, on the photo,
 * photospot and profile pages. */
/* The link to the FAQ from the map panel (issue #149). */
.map-panel__help {
    display: block;
    margin: var(--space-1) 0 var(--space-2);
    font-size: 0.78rem;
    color: var(--text-muted);
}
.map-panel__help:hover { color: var(--accent); }

/* The info marker beside a label (issue #137): the explanation is in its
 * title, which is what a hover and a focus both show.
 *
 * ONE rule for the whole site, and the name is no longer map-specific
 * (issue #257): the owner asked for the same marker beside "Alias", so the
 * markup moved into `components/info_mark.html` and `map.js` builds the same
 * class rather than a second treatment of its own. `accounts/tests.py`
 * compares the two so they cannot drift. */
.info-mark {
    color: var(--text-muted);
    cursor: help;
    font-size: 0.9em;
}
.info-mark:hover,
.info-mark:focus { color: var(--accent); }

/* The to-do buttons above "Meine Fotos" (issue #140): one row, side by
 * side, wrapping only when they have to. They used to be a stack of
 * single-button rows, which read as unrelated things. */
.todo-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

/* ...and on the same line as the filter (owner, 2026-08-24). All three are
 * "narrow this list down", and stacked they ate a third of the page above
 * the photos. The filter takes what is left over and drops to its own line
 * only when there is no room; `align-items: flex-start` keeps the buttons
 * level with the closed filter instead of centring them against an opened
 * one. */
/* The version in the footer: present, not prominent. */
.site-footer__version { color: var(--text-muted); font-size: 0.8rem; }
.site-footer__version:hover { color: var(--accent); }

/* One released version (issue #174). */
.changelog-entry + .changelog-entry {
    border-top: 1px solid var(--border);
    padding-top: var(--space-6);
    margin-top: var(--space-6);
}

.changelog-entry h2 {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.changelog-entry__version {
    font-size: 0.9rem;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
}

.changelog-entry ul { max-width: var(--prose-width); }

/* Sorting above the filter bar, on every listing (issue #140). The space
 * below it is the gallery's, which the owner asked for everywhere. */
.sort-row {
    display: flex;
    justify-content: flex-end;
    margin-bottom: var(--space-3);
}

.todo-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--space-2);
    margin-bottom: var(--space-5);
}
.todo-bar .filters-wrap {
    flex: 1 1 22rem;
    min-width: 0;
}
/* Three columns of the same width (issue #147): different shapes read as
 * unrelated things. Below 40rem they stack, each full width. */
.todo-bar--even { flex-wrap: nowrap; }
.todo-bar--even > .todo-row {
    flex: 1 1 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
    gap: var(--space-2);
}
/* The buttons ARE the filter's box (issue #140, third round): same border,
 * same radius, same padding, same type size, so the three read as one row
 * of three equal things instead of two small buttons beside a panel. */
.todo-bar--even > .todo-row .btn {
    width: 100%;
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-raised);
    font-size: 0.8rem;
    font-weight: 550;
    justify-content: flex-start;
    text-align: left;
}
.todo-bar--even > .todo-row .btn:hover { border-color: var(--border-strong); }
.todo-bar--even > .filters-wrap { flex: 1 1 0; }
@media (max-width: 40rem) {
    .todo-bar--even { flex-wrap: wrap; }
    .todo-bar--even > .todo-row,
    .todo-bar--even > .filters-wrap { flex: 1 1 100%; }
    .todo-bar--even > .todo-row { grid-template-columns: 1fr; }
}
.todo-bar .filters { margin-bottom: 0; }
/* Same height as the buttons beside it: the summary's own padding is sized
 * for a full-width panel heading, which is taller than a small button. */
.todo-bar .filters__summary {
    padding-top: var(--space-2);
    padding-bottom: var(--space-2);
    font-size: 0.8rem;
}
.todo-bar .filters-wrap__apply { top: 3px; right: 3px; }

/* The mast-or-drone queue (issue #137): one photo, three answers. The photo
 * is capped so the buttons stay on screen without scrolling -- the whole
 * point is answering many in a row. */
.categorise__stage img {
    display: block;
    width: auto;
    max-width: 100%;
    max-height: min(52vh, 560px);
    margin: 0 auto;
    border-radius: var(--radius-sm);
}
.categorise__title { margin: 0; font-size: 1.05rem; }
/* The capture-time queue's two extra answers (2026-09-06): the shift slider
 * with its live preview, and the typed time beside its own button. The
 * slider wears the map panel's range styling by sharing its element rules
 * below, so the two sliders on the site look like one control. */
.capture-review__slider {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}
.capture-review__slider input[type="range"] { flex: 1 1 auto; min-width: 0; }
.capture-review__preview { margin: var(--space-1) 0 var(--space-2); font-weight: 600; }
.capture-review__manual {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}
.capture-review__manual input { flex: 1 1 14rem; }
.categorise__answers {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

/* The administrator's address on the password pages (issue #131). It is a
 * PNG, sized in em so it sits on the line with the text it follows. */
/* The contact details are pictures, and there are two of every one: dark
 * lettering and light (issue #338, the owner: "Durch den Darkmode sind die
 * PNG Texte nicht mehr lesbar"). The theme never reaches the server -- it is
 * the visitor's OS setting or their own override -- so the page carries both
 * and this shows one, in the three states every themed rule here handles:
 * light by default, dark when the OS says so and nothing overrides it, dark
 * when the visitor chose it. `display: none` also keeps the hidden one out
 * of the accessibility tree, so the shared alt is announced once. */
.contact-png--dark { display: none; }

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .contact-png--light { display: none; }
    :root:not([data-theme="light"]) .contact-png--dark { display: inline; }
}

:root[data-theme="dark"] .contact-png--light { display: none; }
:root[data-theme="dark"] .contact-png--dark { display: inline; }

.admin-contact { margin-top: var(--space-4); }
.admin-contact__mail {
    height: 1.5em;
    width: auto;
    vertical-align: -0.4em;
}

/* Title left, sort right, level with each other, above every stream
 * (issue #104). */
.photo-grid__head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-4);
}
.photo-grid__title { margin: 0; font-size: 1.15rem; }

/* The popup's close button (issue #122): a little more presence than
 * Leaflet's default thin grey cross, since a popup now stays until it is
 * closed. A little -- it is still a corner control, not a button. */
.leaflet-container .leaflet-popup-close-button {
    width: 26px;
    height: 26px;
    font-size: 20px;
    line-height: 24px;
    color: var(--text);
    opacity: 0.75;
}
.leaflet-container .leaflet-popup-close-button:hover { opacity: 1; }

/* The profile's two numbers are links (issue #116); they keep the stat
 * layout and only pick up a hover. */
.stat__link {
    display: block;
    color: inherit;
    text-decoration: none;
    border-radius: var(--radius-sm);
}
.stat__link:hover .stat__value { color: var(--accent); }

.share-button {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}

/* Two fields, on every page that wears the pencil: a display name and its
 * aliases. It briefly also held a Fahrzeugfamilie's Zuordnungen (#257) and
 * needed `max-height: 70vh; overflow-y: auto` to stay on screen, which was
 * the symptom the owner named when he sent it back: a panel inside a panel.
 * The rules are a section of the page now, and the scroll hack went with
 * them. Do not put a growing form back in here. */
.spot-rename__panel {
    position: absolute;
    left: 0;
    top: calc(100% + var(--space-2));
    width: min(360px, 100%);
    padding: var(--space-3);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    z-index: 950;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .spot-rename__panel {
        background: var(--bg-glass);
        -webkit-backdrop-filter: blur(14px) saturate(1.5);
        backdrop-filter: blur(14px) saturate(1.5);
    }
}

/* ------------------------------------------------------------- map tools -- */

/* The locate button (issue #50): a Leaflet bar link holding an inline
 * crosshair; the busy state dims it while the browser asks for consent. */
.map-locate {
    display: flex !important;
    align-items: center;
    justify-content: center;
}
.map-locate--busy { opacity: 0.4; }

/* The common switches stay visible in a narrow rail on the right. Detailed
 * settings use the established slide-in panel on desktop and mobile. */
.map-action-rail {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    z-index: 1120;
    display: grid;
    gap: var(--space-1);
    padding: var(--space-1);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-md);
    transition: right 0.2s ease;
}
.map-sidebar-open .map-action-rail {
    right: calc(min(320px, 88vw) + var(--space-3));
}

.map-action-rail__button {
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
}
.map-action-rail__button[hidden] { display: none; }
.map-action-rail__button:hover,
.map-action-rail__button:focus-visible {
    background: var(--bg-sunken);
    color: var(--text);
}
.map-action-rail__button.is-active {
    background: var(--accent-soft);
    color: var(--accent);
}
.map-action-rail__button:disabled {
    cursor: default;
    opacity: 0.45;
}
.map-action-rail__button--settings {
    margin-bottom: var(--space-1);
    border-bottom: 1px solid var(--border);
}
.map-action-icon,
.map-action-icon svg {
    display: block;
    width: 18px;
    height: 18px;
}
/* 32px buttons with 18px icons on every screen, down from 40/22 (the
 * owner, 2026-09-06, twice: "die icons bitte kleiner", then "mach alle
 * inklusive den einstellungen nochmals etwas kleiner"). The four quick
 * toggles stay visible on a phone too -- a first version hid them there
 * and he wanted them back ("möchte ich auf mobile weiterhin direkt sehen"). */

.map-sidebar {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 1110;
    width: 320px;
    max-width: 88vw;
    display: flex;
    flex-direction: column;
    background: var(--bg-raised);
    border-left: 1px solid var(--border);
    box-shadow: var(--shadow-lg);
    transform: translateX(105%);
    /* Hidden once the slide-out has finished, so the parked panel is out of
     * the layout for good (see .map-page on the grey strip). */
    visibility: hidden;
    transition: transform 0.2s ease, visibility 0s linear 0.2s;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .map-sidebar {
        background: var(--bg-glass);
        -webkit-backdrop-filter: blur(14px) saturate(1.5);
        backdrop-filter: blur(14px) saturate(1.5);
    }
}
.map-sidebar.is-open {
    transform: none;
    visibility: visible;
    transition: transform 0.2s ease;
}

/* Attribution and other right-side Leaflet furniture remain fully visible
 * when the settings panel covers the edge of the map. */
#photoMap.map-sidebar-open .leaflet-right { right: min(320px, 88vw); }

.map-sidebar__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border);
}
.map-sidebar__title { margin: 0; font-size: 1rem; }

.map-sidebar__close {
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
}
.map-sidebar__close:hover,
.map-sidebar__close:focus-visible {
    background: var(--bg-sunken);
    color: var(--text);
}

.map-sidebar__body {
    flex: 1;
    overflow-y: auto;
    padding: var(--space-3) var(--space-4) var(--space-5);
}

.map-sidebar__section + .map-sidebar__section {
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}

.map-sidebar__heading {
    margin: 0 0 var(--space-2);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

/* The sun figure's floating labels. The compass ones sit ON the dark
 * band and are therefore light; the hour marks float over the map and
 * carry a white halo instead (issue #74, modelled on Sun Surveyor). */
.railpics-sun-screen-pane {
    isolation: isolate;
}

/* Leaflet derives a marker's inline z-index from its geographic layer-point
 * y coordinate. The screen-fixed ring deliberately cancels the map pane's
 * translation, so a long pan can make those values strongly negative. The
 * SVG then painted over the letters even though the complete sun pane stayed
 * above the terrain shadow (#327). Keep vectors below every DivIcon inside
 * this one pane; !important is required to beat Leaflet's inline z-index. */
.railpics-sun-screen-pane > svg { z-index: 1; }
.railpics-sun-screen-pane .leaflet-marker-icon { z-index: 2 !important; }

.sun-deg, .sun-card, .sun-north {
    color: #f2f4f7;
    font: 600 10px/16px system-ui, sans-serif;
    text-align: center;
    pointer-events: none;
}
.sun-card { font-size: 14px; font-weight: 800; letter-spacing: 0.04em; }
.sun-north { color: #e0483a; font-size: 13px; line-height: 16px; }

.sun-hour {
    color: #3c434d;
    font: 700 11px/16px system-ui, sans-serif;
    text-align: center;
    text-shadow: 0 0 4px #ffffff, 0 0 2px #ffffff, 0 0 1px #ffffff;
    pointer-events: none;
}
.sun-hour {
    font-size: 10px;
    transform: translateY(-14px);
}

/* Sunrise and sunset written where their lines meet the ring. */
.sun-edge {
    font: 700 11px/16px system-ui, sans-serif;
    text-align: center;
    white-space: nowrap;
    text-shadow: 0 0 4px #ffffff, 0 0 2px #ffffff, 0 0 1px #ffffff;
    pointer-events: none;
}
.sun-edge--rise { color: #b06d10; }
.sun-edge--set { color: #a53b3b; }

/* The height and the bearing at the end of the sun's line (2026-09-06):
 * two rows, a glyph and a number each, with the hour marks' white halo,
 * because the label floats over the map as they do. Its colours are as
 * fixed as the rest of the figure's: it lies on tiles, not on a themed
 * surface. */
.sun-now {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 1px;
    color: #3c434d;
    font: 700 11px/16px system-ui, sans-serif;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    text-shadow: 0 0 4px #ffffff, 0 0 2px #ffffff, 0 0 1px #ffffff;
    pointer-events: none;
}
.sun-now__row { display: flex; align-items: center; gap: 3px; }
.sun-now svg {
    flex: none;
    width: 14px;
    height: 14px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-linejoin: round;
    filter: drop-shadow(0 0 1.5px #ffffff) drop-shadow(0 0 1px #ffffff);
}

/* A low full-width dock, flush with the bottom edge. Since 2026-09-06 (the
 * owner, in rounds) the dock IS the tape -- it spans the whole bar in both
 * directions -- and the controls lie near its BOTTOM edge with no box
 * around them: Now, the time, the date, and to their right the sun's height
 * over its bearing. The tape used to share a row with the controls and stop
 * short of the edges (issues #300, #315, #322), then had a row of its own
 * under them, then carried them in a glass pill at the top ("ultra
 * hässlich", 2026-09-07). The row is a sibling of the tape (map_tools.js
 * says why) and sits above it. */
.map-timebar {
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    z-index: 1000;
    box-sizing: border-box;
    width: 100%;
    padding: 0 0 env(safe-area-inset-bottom, 0px);
    /* The only place this shows is the safe-area strip below the tape on a
     * phone with a home indicator, plus the one frame before the canvas
     * paints. It was --bg-raised, which is WHITE in light mode: a bright
     * band under a slider whose own bottom edge is dark, the same defect
     * one edge over from the hairline below. #0b0d10 is the colour the
     * scrim is made of, so the strip reads as the slider continuing and
     * can never be the lighter of the two. Not verifiable from here --
     * headless Chrome reports no safe-area inset. */
    background: #0b0d10;
    /* No hairline against the map (the owner, 2026-09-07: "Aktuell trennt
     * noch ein kleiner weisser strich den slider mit dem rest, den bitte
     * auch noch raus"). It was a 1px --border top edge, which is what a
     * panel wears -- and the tape is not a panel, it is the bottom of the
     * map. The gradient's own top edge is the boundary now. The dock lost
     * that pixel of height with it: it measures the tape exactly. */
    border: 0;
    border-radius: 0;
    box-shadow: none;
}
.map-timebar[hidden] { display: none; }

/* The controls on the tape's lower edge. No surface of their own: what
 * keeps them readable is the scrim map_tools.js paints into the canvas
 * beneath this row -- a gradual darkening with no edge and no corners --
 * and light text on it.
 *
 * Their colours are therefore FIXED rather than themed, like the tape's own
 * phase colours and the sun figure's lettering: this row lies on that
 * scrim in both themes, so a theme token would be wrong half the time. */
.map-timebar__left-controls {
    position: absolute;
    bottom: calc(3px + var(--space-1) + env(safe-area-inset-bottom, 0px));
    left: 50%;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: var(--space-3);
    max-width: calc(100% - var(--space-3));
    transform: translateX(-50%);
    color: #f2f4f7;
}
.map-timebar__date-controls {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

/* Outlined on the scrim rather than filled chips, so the row reads as one
 * line of writing and not as furniture. `color-scheme: dark` makes the
 * browser draw its own date picker to match; the calendar glyph comes out
 * light on its own that way, so the site-wide invert-in-the-dark rule has
 * to be turned back off here. The width is the text's: ten digits, two
 * points and that glyph (the owner: "Die Datum Box scheint grösser zu sein
 * als sie muss"). */
.map-timebar__date,
.map-timebar__date.input[type="date"] {
    width: 6.75rem;
    min-height: 0;
    height: var(--space-6);
    padding: 0 var(--space-1) 0 var(--space-2);
    background: transparent;
    border-color: rgba(255, 255, 255, 0.5);
    color: #f2f4f7;
    color-scheme: dark;
    font-size: 0.8rem;
}
.map-timebar__date.input[type="date"]::-webkit-calendar-picker-indicator {
    filter: none;
    opacity: 0.85;
}
.map-timebar__now.btn {
    min-height: var(--space-6);
    padding: 0 var(--space-2);
    background: transparent;
    border-color: rgba(255, 255, 255, 0.5);
    color: #f2f4f7;
    font-size: 0.8rem;
}
.map-timebar__now.btn:hover {
    background: rgba(255, 255, 255, 0.18);
}
.map-timebar__readout {
    min-width: 3rem;
    font-size: 1rem;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    text-align: center;
    white-space: nowrap;
}

/* The sun's height over its bearing, right of the three controls, each
 * behind the glyph the figure uses for it. */
.map-timebar__sun {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 1px;
    padding-left: var(--space-3);
    border-left: 1px solid rgba(255, 255, 255, 0.28);
    font-size: 0.72rem;
    line-height: 1.15;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.map-timebar__sun[hidden] { display: none; }
.map-timebar__sun-row {
    display: flex;
    align-items: center;
    gap: 3px;
}
.map-timebar__sun svg {
    flex: none;
    width: 12px;
    height: 12px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* The coloured time axis slides under this fixed centre marker. The tape
 * runs edge to edge and is the dock's whole height, so it has no corners to
 * round; its lower band is under the controls, and map_tools.js keeps the
 * drawing and the marker above them. */
.map-timebar__tape {
    position: relative;
    width: 100%;
    height: 88px;
    /* Grow the background by 4px per edge; keep the drawing's original
     * height inside that inset (owner, 2026-09-08). */
    box-sizing: content-box;
    padding-block: var(--space-1);
    overflow: hidden;
    cursor: grab;
    touch-action: none;
}
.map-timebar__tape:active { cursor: grabbing; }
.map-timebar__tape canvas {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
}
.map-timebar__marker {
    position: absolute;
    top: var(--space-1);
    bottom: 0;
    left: 50%;
    width: 2px;
    margin-left: -1px;
    background: var(--accent);
    pointer-events: none;
}

/* Above the dock, not a row of it (issue #312, the owner: put the text "über
 * dem balken ... statt diesen jedesmal zu vergrössern"). As a grid row this
 * line made the whole dock grow the moment a shade calculation started and
 * shrink again when it finished -- and the sun ring grew and shrank with it,
 * because freeInset() in map_tools.js sizes the ring from the dock's measured
 * box. Out of flow the line overlays the map instead and the box stops moving.
 *
 * The Leaflet corner controls keep their 76px/104px clearance below: those
 * numbers describe the dock, which is now always the height they were written
 * for -- a status row used to make the dock TALLER than the clearance and hide
 * the attribution behind it outright. Lifting them when a message shows would
 * only move the flicker from the dock to the map's furniture, and the "weiter
 * hineinzoomen" message is not brief: it stays for as long as the visitor
 * stays zoomed out. So the label clears them instead. --space-6 is what that
 * costs: measured with that clearance in effect, the attribution's top edge
 * sits 24px above the dock, and below 900px both it and the scale bar are
 * hidden -- which is exactly where this label is at its widest. */
.map-timebar__status {
    position: absolute;
    inset-inline: 0;
    bottom: calc(100% + var(--space-6));
    width: fit-content;
    /* Never wider than the map, at any width: this is the phone's constraint. */
    max-width: calc(100% - var(--space-5));
    margin: 0 auto;
    padding: var(--space-1) var(--space-3);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    color: var(--text-muted);
    font-size: 0.8rem;
    text-align: center;
    /* It lies over the map and over the tape's drag area: it must not catch
     * a single pointer event. */
    pointer-events: none;
}
/* It floats over the map, not inside another glass surface -- the dock itself
 * is opaque -- so the plain --bg-glass is the right token here, not the
 * nested one. */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .map-timebar__status {
        background: var(--bg-glass);
        -webkit-backdrop-filter: blur(14px) saturate(1.5);
        backdrop-filter: blur(14px) saturate(1.5);
    }
}
.map-timebar__status[hidden] { display: none; }
.map-timebar__progress {
    display: block;
    width: 100%;
    height: var(--space-2);
    margin-top: var(--space-2);
    accent-color: var(--accent);
}
.map-timebar__progress[hidden] { display: none; }

/* The corner controls clear the dock's MEASURED height, published by
 * map_tools.js as --map-timebar-height (issue #352); the literal is the
 * fallback for the frame before the script runs and is what the dock
 * measures at each width. */
#photoMap:has(.map-timebar:not([hidden])) .leaflet-bottom {
    bottom: var(--map-timebar-height, calc(88px + var(--space-2)));
}

@media (max-width: 700px) {
    .map-sidebar-open .map-action-rail { display: none; }
    .map-timebar__left-controls { gap: var(--space-2); }
    .map-timebar__date-controls { gap: var(--space-1); }
    .map-timebar__date,
    .map-timebar__date.input[type="date"] {
        min-width: 0;
        width: 6.25rem;
        height: 40px;
    }
    .map-timebar__now.btn { min-height: 40px; }
    .map-timebar__readout { min-width: 2.75rem; }
    .map-timebar__sun { padding-left: var(--space-2); }
    .map-timebar__tape { height: 96px; }
    #photoMap:has(.map-timebar:not([hidden])) .leaflet-bottom {
        bottom: var(--map-timebar-height, calc(96px + var(--space-2)));
    }
}

.map-panel__note {
    margin: var(--space-2) 0 0;
    font-size: 0.75rem;
    color: var(--text-muted);
    max-width: 220px;
}

/* ---------------------------------------------------------------- footer -- */

.site-footer {
    border-top: 1px solid var(--border);
    background: var(--bg-raised);
    padding: var(--space-6) var(--space-4);
    margin-top: var(--space-8);
}

.site-footer__inner {
    max-width: var(--content-width);
    margin: 0 auto;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-4) var(--space-6);
    align-items: center;
    font-size: 0.875rem;
}

.site-footer a { color: var(--text-muted); }
.site-footer a:hover { color: var(--accent); }

/* Issue #283: a wrapping centred flex row made the phone footer land in
 * unrelated groups of three, two and two. Two fixed columns give every link
 * a stable place; the version is metadata and gets its own final row. */
@media (max-width: 559px) {
    .site-footer__inner {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--space-3) var(--space-4);
        align-items: start;
    }

    .site-footer__inner a { text-align: center; }
    .site-footer__version { grid-column: 1 / -1; }
}

/* The vehicle-family table (issue #166). One form per row, so a moderator
 * correcting one family cannot lose four others to a stray keystroke -- the
 * separator is what makes that visible. */
.prefill-row + .prefill-row {
    border-top: 1px solid var(--border);
    padding-top: var(--space-4);
    margin-top: var(--space-4);
}

.prefill-row__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
}

/* Pushes the count to the right of the two buttons. */
.prefill-row__foot .muted { margin-left: auto; }

/* The moderation page's collapsible groups (issue #175). Nine sections in one
 * column had become a page you scroll rather than read.
 *
 * `display` on the summary is never touched -- WebKit refuses to toggle a
 * <summary> whose display has been changed (issue #107), and this page's
 * whole navigation would then be dead on an iPhone. The row is laid out with
 * flex on a span INSIDE the summary instead. */
.mod-group {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    margin-bottom: var(--space-4);
}

.mod-group__head {
    padding: var(--space-4);
    font-size: 1.05rem;
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}

.mod-group__head::-webkit-details-marker { display: none; }

.mod-group__head::before {
    /* The literal character, like .faq__question above -- a CSS escape
     * written from a script came out as a control code and a stray "a". */
    content: "›";
    display: inline-block;
    width: 1em;
    margin-right: var(--space-2);
    color: var(--text-muted);
    transition: transform 0.15s ease;
}

.mod-group[open] .mod-group__head::before { transform: rotate(90deg); }

.mod-group__head:hover { color: var(--accent); }

/* The count sits at the right-hand end of the row. */
.mod-group__head .chip { float: right; }

.mod-group__body {
    padding: 0 var(--space-4) var(--space-4);
    border-top: 1px solid var(--border);
    padding-top: var(--space-4);
}

/* Inside a group the sections are separated by a line rather than by a gap
 * the size of a paragraph -- that gap is what made the page long. */
.mod-group__body .mod-section { margin-bottom: 0; }
.mod-group__body .mod-section + .mod-section,
.mod-group__body section + section {
    border-top: 1px solid var(--border);
    padding-top: var(--space-5);
    margin-top: var(--space-5);
}

/* ------------------------------------------- the statistics page, part two --
 *
 * The views curve and the date range above it (issue #165, second pass). The
 * curve leads at full width and everything below narrows to the same range,
 * which is the shape the owner asked for.
 */

.line-chart-block { margin: var(--space-6) 0; }

.line-chart__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-4);
}

.line-chart__total {
    margin: 0;
    text-align: right;
    display: flex;
    flex-direction: column;
}

/* The plot itself. A fixed height rather than an aspect ratio: the shape of
 * the curve should not change when the window does. */
.line-chart {
    position: relative;
    height: 220px;
    margin-top: var(--space-4);
    border-bottom: 1px solid var(--border);
}

.line-chart__svg {
    display: block;
    width: 100%;
    height: 100%;
    overflow: visible;
}

.line-chart__area { fill: var(--accent); opacity: 0.12; }

.line-chart__line {
    fill: none;
    stroke: var(--accent);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
}

.line-chart__point-hit {
    fill: transparent;
    cursor: crosshair;
}

.line-chart__point-dot {
    fill: var(--accent);
    stroke: var(--bg-raised);
    stroke-width: 2;
    vector-effect: non-scaling-stroke;
    pointer-events: none;
}

.line-chart__point:focus-within .line-chart__point-dot,
.line-chart__point:hover .line-chart__point-dot { fill: var(--text); }

.line-chart__tooltip {
    position: absolute;
    z-index: 1;
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    background: var(--text);
    color: var(--bg-raised);
    font-weight: 700;
    line-height: 1.2;
    white-space: nowrap;
    pointer-events: none;
    transform: translate(-50%, calc(-100% - var(--space-2)));
}

/* The one number the y axis needs: without it the curve has a shape but no
 * scale, and a peak of 3 looks exactly like a peak of 3000. */
.line-chart__peak {
    position: absolute;
    top: 0;
    right: 0;
    font-size: 0.75rem;
    color: var(--text-muted);
    background: var(--bg-raised);
    padding: 0 var(--space-1);
}

.line-chart__axis {
    position: relative;
    height: 1.4rem;
    margin-top: var(--space-2);
    font-size: 0.75rem;
    color: var(--text-muted);
}

.line-chart__axis span {
    position: absolute;
    transform: translateX(-50%);
    white-space: nowrap;
}

/* The first and last labels would otherwise hang off the ends. */
.line-chart__axis span:first-child { transform: none; }
.line-chart__axis span:last-child { transform: translateX(-100%); }

/* Six labels overlap on a phone, three do not. */
.line-chart__axis--narrow { display: none; }

@media (max-width: 599px) {
    .line-chart__axis--wide { display: none; }
    .line-chart__axis--narrow { display: block; }
}

/* -- the date range ------------------------------------------------------ */

.range-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3) var(--space-5);
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    margin-bottom: var(--space-5);
}

.range-bar__presets,
.range-bar__dates {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.range-bar__dates input[type="date"] { width: auto; }

/* Two overlapping range inputs, because the browser has no two-handled
 * slider. Both sit on top of the same track; only their thumbs take pointer
 * events, or the upper input would swallow every click meant for the lower
 * one. */
.range-bar__slider {
    position: relative;
    flex: 1 1 14rem;
    height: 1.6rem;
    display: flex;
    align-items: center;
}

.range-bar__track {
    position: absolute;
    left: 0;
    right: 0;
    height: 4px;
    border-radius: 999px;
    background: var(--border);
}

.range-bar__fill {
    position: absolute;
    height: 100%;
    border-radius: 999px;
    background: var(--accent);
}

.range-bar__slider input[type="range"] {
    position: absolute;
    left: 0;
    width: 100%;
    margin: 0;
    background: none;
    pointer-events: none;
    appearance: none;
    -webkit-appearance: none;
}

.range-bar__slider input[type="range"]::-webkit-slider-thumb {
    pointer-events: auto;
    appearance: none;
    -webkit-appearance: none;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    border: 2px solid var(--bg-raised);
    background: var(--accent);
    cursor: grab;
}

.range-bar__slider input[type="range"]::-moz-range-thumb {
    pointer-events: auto;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    border: 2px solid var(--bg-raised);
    background: var(--accent);
    cursor: grab;
}

.range-bar__slider input[type="range"]::-webkit-slider-runnable-track {
    background: none;
}

.range-bar__slider input[type="range"]::-moz-range-track { background: none; }

.range-bar__slider input[type="range"]:focus-visible::-webkit-slider-thumb {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Not on a phone. Two 16px thumbs a few pixels apart is a control nobody can
 * hit with a thumb, and the bar was taking a quarter of the screen before any
 * data appeared. The presets and the two date fields do the same job there,
 * and they are the ones that work by tapping. */
@media (max-width: 599px) {
    .range-bar__slider { display: none; }
    .range-bar { gap: var(--space-2) var(--space-3); padding: var(--space-3); }
}

/* Where the sun stood when the photo was taken (issue #179). One line under
 * the preview map: the ring says the direction, this says the numbers. */
.mini-map__sun {
    margin: 0;
    padding: var(--space-2) var(--space-3);
    border-top: 1px solid var(--border);
    font-size: 0.8rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* Empty until the script fills it, and then it should not have reserved a
 * line of nothing. */
.mini-map__sun:empty { display: none; }

/* Threads and mentions (issue #191). One level deep, always -- the server
 * flattens deeper replies -- so the indent never compounds. */
.comment--reply {
    margin-top: var(--space-3);
    margin-left: var(--space-5);
    padding-left: var(--space-4);
    border-left: 2px solid var(--border);
}

.comment__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    margin-top: var(--space-1);
}

.comment__reply {
    display: inline-block;
    font-size: 0.8rem;
    color: var(--text-muted);
}

.comment__reply:hover { color: var(--accent); }

/* "Löschen" under a comment (issue #277). Deleting is a POST, so its trigger
 * is a <button> where "Antworten" and "Bearbeiten" are links -- and it has to
 * read as the SAME small quiet action, not as a third kind of control beside
 * them. So it wears `.comment__reply` and this rule only takes back the
 * chrome a <button> brings by default; every declaration that describes the
 * look is still the one above, which is what docs/design.md means by copying
 * the pattern rather than the token. Deliberately not red at rest: the site
 * has one accent, and a red word in a row of grey ones would be a new
 * treatment for a state that already has one. */
button.comment__reply {
    padding: 0;
    border: 0;
    background: none;
    font-family: inherit;
    cursor: pointer;
}

/* The form around that button must not break the row of actions. */
.comment__action { display: inline; }

.comment__mention {
    color: var(--accent);
    font-weight: 550;
}

/* "Antwort an X · Abbrechen" above the form while ?antwort= is set. */
.comment-replying {
    margin: 0;
    font-size: 0.85rem;
    color: var(--text-muted);
}

/* The "Neue erstellen" entry at the bottom of a Definitionen dropdown
 * (issue #192): separated by a hairline, so it reads as the action it is
 * rather than as another match. */
.definition-create {
    border-top: 1px solid var(--border);
    color: var(--accent);
}

/* The one statistics chart when it is bars (issue #165, third pass): full
 * width like the line, not squeezed into the old three-column grid. */
.stat-chart-single { margin: var(--space-6) 0; }
/* Issue #332: the old rule made the label itself 12rem wide but left its
 * grid column at 5.5rem, so long vehicle names painted over the bar. Reserve
 * the room in the grid instead. 24ch fits the meaningful part of most class
 * and family names; exceptional names wrap inside that column instead of
 * being shortened or painted over the bar. */
.stat-chart-single .bar-chart__row {
    grid-template-columns: minmax(0, 24ch) minmax(var(--space-8), 1fr) 3rem;
}
.stat-chart-single .bar-chart__label {
    min-width: 0;
    white-space: normal;
    overflow-wrap: anywhere;
}

@media (max-width: 599px) {
    .stat-chart-single .bar-chart__row {
        grid-template-columns: minmax(0, 13ch) minmax(var(--space-7), 1fr) 2.5rem;
    }
}

.range-bar__chart select { width: auto; min-width: 14rem; }

/* The moderation-only activity block on a profile (issue #168). The rule
 * above it separates it from the public page it hangs under. */
.mod-activity {
    margin-top: var(--space-7);
    padding-top: var(--space-5);
    border-top: 1px solid var(--border);
}

.mod-activity h3 { margin-top: var(--space-5); }

.user-directory__search { flex: 1 1 16rem; width: auto; }

/* The chart's own head: the selector stands where the title stood (issue
 * #203), sized like a heading rather than like a form control. */
.stat-chart-head { margin: var(--space-6) 0 var(--space-2); }

.stat-chart-head__pick {
    width: auto;
    min-width: 16rem;
    font-size: 1.15rem;
    font-weight: 700;
    letter-spacing: -0.015em;
    border: 0;
    border-bottom: 1px solid var(--border);
    border-radius: 0;
    /* The page colour, NOT transparent (issue #206). The browser paints the
     * open option list in the select's own background colour, so a
     * transparent select got a white list while the options kept our light
     * dark-mode text -- white on white. `--bg` is what the page behind it
     * uses, so closed it looks exactly as unframed as before. */
    background: var(--bg);
    padding-left: 0;
}

/* An open task, set apart from the filters below it (issue #202): the tinted
 * field is the highlight, the primary button is the way in. */
.todo-callout {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--space-4);
    background: var(--accent-soft);
    border: 1px solid var(--accent);
}

/* What the filters are hiding, at the end of the results (issue #243). */
.feed-unfiltered {
    margin-top: var(--space-5);
    text-align: center;
}

/* The shadow tool's info page (map/templates/shadow_info.html, 2026-09-06):
   prose sections and two data tables. Tokens only; the table scrolls inside
   its own wrapper so the page never scrolls sideways on a phone. */
.section-head__link {
    color: var(--text-muted);
    font-size: 0.9375rem;
    text-decoration: underline;
    text-underline-offset: 0.15em;
}
.info-section { margin-top: var(--space-6); }
.info-section h2 { margin: 0 0 var(--space-2); }
.info-list { padding-left: 1.25rem; margin: var(--space-3) 0 0; }
.info-list li + li { margin-top: var(--space-2); }
.info-note { color: var(--text-muted); font-size: 0.9375rem; margin-top: var(--space-3); }
.info-table-wrap { overflow-x: auto; margin-top: var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-md); }
/* Source disclosures grow downwards without redistributing the columns (#395). */
.info-table { width: 100%; min-width: var(--prose-width); table-layout: fixed; border-collapse: collapse; font-size: 0.9375rem; }
.info-table th, .info-table td { padding: var(--space-2) var(--space-3); text-align: left; vertical-align: top; border-top: 1px solid var(--border); overflow-wrap: anywhere; }
.info-table thead th { border-top: 0; background: var(--bg-raised); font-weight: 600; }
.info-table thead th:first-child { width: 28%; }
.info-table thead th:nth-child(2) { width: 32%; }
.info-table tbody th { font-weight: 600; }
.info-table tbody th small { display: block; color: var(--text-muted); font-weight: 400; }
.info-table__row--none td, .info-table__row--none th small { color: var(--text-muted); }

/* An option the live counts say would empty the page (issue #359). Beats the
   type-to-narrow inline display of searchable lists on purpose; a ticked
   option never gets the class. */
.filters__option--unavailable { display: none !important; }

/* Audit map controls: a single status surface between the existing corners. */
.map-presets {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}
.map-feedback__status {
    margin: var(--space-2) 0;
}
.map-feedback__status:empty { display: none; }
.map-tutorial__actions { flex-wrap: wrap; }
/* The main map has one info link in its navigation column. Keep the actual
 * provider credits readable and clickable at every width, including OSM. */
#photoMap .leaflet-control-attribution:not(.google-attribution):not(:has(.gm-style-cc)),
.map-page .map-credits {
    max-width: calc(100vw - var(--space-5));
}
